Compare commits

..

71 Commits

Author SHA1 Message Date
Andrej730 4fb34c0816 base autogenerated opening thickness on wall thickness #4710 2024-05-21 17:54:51 +05:00
Andrej730 9b562669d2 fix root.create_entity skipping setting up defaults on ifc4x3 2024-05-21 17:54:08 +05:00
Andrej730 295d0a677e cost schedule - prohibit adding multiple quantity types on 1 cost item
All IfcCostItem.CostQuantities supposed to use only 1 type of quantity, so they can be added up.

Changed UI to reflect that requirement - dropdown for selecting quantity type is now only visible if cost item has no quantities, otherwise it just reuses already used quantity type.

See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcCostItem.htm

Before - https://i.imgur.com/gFlwHEk.png
After - https://i.imgur.com/pk3wXfe.png
2024-05-21 17:54:07 +05:00
Andrej730 9aacafd575 fix bug with cost schedules confusing 0 quantity with no quantities
a bit related to #4704

1) fixed util.cost.get_total_quantity
2) fixed similar issue in cost.data that calculates the final value that user will see in UI
3) changed UI, "-" is shown when there are no quantities and "0" is when quantities are there but they just equal to zero.

Before - https://i.imgur.com/EO53DhM.png
After - https://i.imgur.com/H6rK4sP.png

fyi @myoualid
2024-05-21 17:54:07 +05:00
Andrej730 ce6ce1b975 bim.assign_cost_item_type name change 2024-05-21 17:54:07 +05:00
Andrej730 d064ef1237 black format 2024-05-21 17:53:46 +05:00
Andrej730 2db9bd9c39 fix errors adding occurrences from type manager #4702
Errors occurred if occurrence type wasn't matching current tool's type. E.g. if you would add an IfcRoofType being in a Slab Tool.
2024-05-21 17:53:42 +05:00
Andrej730 7622504d68 avoid even more invalid enum warnings...
warnings like `pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'`
2024-05-21 17:53:42 +05:00
Andrej730 221dd9a7a2 typing 2024-05-21 17:53:42 +05:00
Andrej730 1b74a99667 use delete_ifc_object removing a type
It was acting a bit different - e.g. leaving mesh representation from type on occurrences leaving to lots of errors. Though there is still #3931
2024-05-21 17:07:32 +05:00
Dion Moult 9122f006c7 Deprecate material attributes panel in favour of new material manager. 2024-05-20 23:21:13 +10:00
Andrej730 678dbaa66a nest api to maintain order of related objects #4698
order of objects in .RelatedObjects is important (e.g. for cost items, it's the order of their appearance), so we should maintain it and cannot use sets for .RelatedObjects
2024-05-20 17:19:00 +05:00
Andrej730 fb88499197 ifc5d - check for mandatory fields in csv header 2024-05-20 17:19:00 +05:00
Andrej730 f84adf67cd typing 2024-05-20 17:19:00 +05:00
Andrej730 cacb38a251 maintain "bim." as operators bl_idname prefix for BlenderBIM
1) removed unused operator import_ifc.bim
2) marked export_ifc.bim as deprecated
2024-05-20 17:19:00 +05:00
Andrej730 7c6c01e6b9 fix python 3.10 loading issue after bb8e84e
The error is below.

It was failing because Callable was checking if `ifcopenshell.entity_instance` is a callable and at runtime actually it's not, it's a module.
In python 3.11 they've removed that check and therefore it's not throwing an error. Enabling forward annotations fixes it for python 3.10.
Not sure if there is a need to ensure `ifcopenshell.entity_instance` should be recgonized by python as a class at runtime rather than a module since we need it just for type checking and type checking seems to be clever enough to prioritize module classes over submodules.

  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\bim\__init__.py", line 25, in <module>
    from . import handler, ui, prop, operator, helper
  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\bim\handler.py", line 23, in <module>
    import ifcopenshell.api.owner.settings
  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\__init__.py", line 85, in <module>
    from .file import file
  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\file.py", line 179, in <module>
    class file:
  File "C:\Users\user_name\AppData\Roaming\Blender Foundation\Blender\4.0\scripts\addons\blenderbim\libs\site\packages\ifcopenshell\file.py", line 382, in file
    def __getattr__(self, attr) -> Union[Any, Callable[..., ifcopenshell.entity_instance]]:
  File "C:\Program Files\Blender Foundation\Blender 4.0\4.0\python\lib\typing.py", line 1206, in __getitem__
    return self.__getitem_inner__(params)
  File "C:\Program Files\Blender Foundation\Blender 4.0\4.0\python\lib\typing.py", line 312, in inner
    return func(*args, **kwds)
  File "C:\Program Files\Blender Foundation\Blender 4.0\4.0\python\lib\typing.py", line 1212, in __getitem_inner__
    result = _type_check(result, msg)
  File "C:\Program Files\Blender Foundation\Blender 4.0\4.0\python\lib\typing.py", line 176, in _type_check
    raise TypeError(f"{msg} Got {arg!r:.100}.")
TypeError: Callable[args, result]: result must be a type. Got <module 'ifcopenshell.entity_instance' from 'C:\\Users\\user_name\\AppData\\Roaming\\Blender F.
2024-05-20 13:17:17 +05:00
Andrej730 4e462a3cf7 use .alert in fatal loading error UI to draw more attention (3320acc)
https://i.imgur.com/5ZukO0b.png
we probably will need to use 1 method for both error UIs at some point 😅
2024-05-20 12:47:09 +05:00
Andrej730 e7f4c6c9cc update error UI similar to 7bf8c0bbf 2024-05-20 12:34:56 +05:00
Dion Moult 7bf8c0bbfb Fix #4686. Automatically derive correct download URI if add-on fails to install. 2024-05-20 16:32:42 +10:00
Dion Moult c4157673f5 Fix #4696. Null properties are now purged by default to prevent cruft build up. Add support for purging empty enum props. 2024-05-20 16:04:27 +10:00
E Shattow 16285e0625 Update installation.rst refering to System not Import-Export
Blender BIM addon is listed by Blender 4.1 as `System: BlenderBIM`
2024-05-20 10:57:38 +05:00
Dion Moult a07d3b9669 Fix #4633. Only update the text value of the object, not try to be clever with types and occurrences.
Previously, we were clever in detecting types and occurrences because if you edited a type, you wanted all occurrences to be updated. Now, we no longer need to be clever because like any other UI element, the annotations shown in the active view are updated after every IFC operation.
2024-05-20 14:12:03 +10:00
Dion Moult 2d0b356203 See #4690. User can now configure font and font-scaling in schedules. 2024-05-20 12:00:59 +10:00
Dion Moult f666053b63 Use pip instead of hardcoding bcf packages during build. See #4640. 2024-05-19 23:50:57 +10:00
Dion Moult 3bdc969efb Fix #4690. User can now configure the font used to render viewport text. 2024-05-19 23:36:38 +10:00
Bruno Postle 932817877c Fix CPack source tarball generation
This was broken, now a IfcOpenShell-0.7.0.tar.gz file can be created
like so:

  mkdir build
  cd build
  cmake ../cmake/ -DEXTRA_VERSION=
  make package_source
2024-05-19 13:29:26 +01:00
Andrej730 cb255a18b9 fix error editing pset templates when you just opened ifc file
It was failing with an error below if you'd try to edit any property since `primary_measure_type()` returned `[]` because it was executed before `pset_templates()` and `IfcStore.pset_template_file` wasn't yet set.

```python
Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\pset_template\operator.py", line 146, in execute
    props.active_prop_template.primary_measure_type = template.PrimaryMeasureType
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: bpy_struct: item.attr = val: enum "IfcLabel" not found in ()
```
2024-05-18 11:24:10 +05:00
Andrej730 183028570e bump xsdata in blenderbim after 87c9bbe2a #4640 2024-05-18 01:49:56 +05:00
Andrej730 341cdd4ef7 fix error updating curve representations after 3fa573e #4685 #4682 2024-05-17 17:40:13 +05:00
Andrej730 fe68e16ca3 bim.update_representation - error if add_representation didn't worked 2024-05-17 17:31:01 +05:00
Andrej730 a8f0c90d47 set_element_value - fix issues with classification, similar to 52ae759 2024-05-17 17:31:01 +05:00
Andrej730 39f550529b experimental - set_element_value to throw exception for invalid queries 2024-05-17 17:31:01 +05:00
Andrej730 25c1a14e64 set_element_value - pass attr value to the next part of the query #4495
E.g. previously set_element_value with query "material.item.Material.Name" would fail with error "Material property is expecting an IFC entity and not a string".

But now it will detect that "Material" property is not a last key in the query and will pass `.Material` value forward and try to set it's `.Name` attribute with value.

The main goal is to make sure get_element_value and set_element_value would have a same result for same queries.
2024-05-17 17:31:01 +05:00
Andrej730 9f7d710119 fix set_element_value bug #4495 2024-05-17 17:31:01 +05:00
Andrej730 ea592775e4 fix set_element_value setting bool attributes with "False" str value 2024-05-17 17:31:01 +05:00
Andrej730 42be4eb064 typing 2024-05-17 17:31:01 +05:00
Hilko 8662e20c58 remove unused include #4620 2024-05-17 10:26:04 +02:00
Andrej730 da4ce3773f bcf - fix xsdata warning about deprecated argument in SerializerConfig
`pretty_print` was deprecated in 24.2 (https://github.com/tefra/xsdata/commit/2b01dbf6cf586e7d64e93ec7045dc17d2585e0de) and replaced with `indent`
2024-05-17 11:57:46 +05:00
Andrej730 87c9bbe2a7 Fix bcf due xsdata update #4680
After https://github.com/tefra/xsdata/commit/93a8ca0548d1f7badb628ad9fd0327672abe140b

`parser.register_namespace("xs", "http://www.w3.org/2001/XMLSchema")` started failing with `TypeError: PushParser.register_namespace() missing 1 required positional argument: 'uri'` since xsdata added a new argument `ns_map` (previously it was always using `parse.ns_map` under the hood, now it allows to provide some external dictionary). We just restore the original behaviour with internal `ns_map` by providing it explicitly.

Specified xsdata version after that change in pyproject.toml.

Also `serialize()` is now using `parser.ns_map` as a fallback value (otherwise why we do `parser.register_namespace` if we never used the `parser.ns_map`?)
2024-05-17 11:46:53 +05:00
Andrej730 a66ee06e3d dev environment - add bcf 2024-05-17 11:42:06 +05:00
Andrej730 9969de58cb add a console toggle to error message section 2024-05-16 18:14:07 +05:00
Andrej730 6995cafe63 bim.split_along_edge fixes #4675
1) description to emphasize that active object will be the one that's cutting
2) skip non-ifc elements, previously they failed to process with error `AttributeError: 'NoneType' object has no attribute 'RepresentationType'`. Also skip objects without representations.
3) add an info message at the end

Here's a short gif in case if anyone doesn't about that feature: https://imgur.com/a/338w7jx
2024-05-16 18:14:06 +05:00
Andrej730 eaa80ad08a fix issue getting styles if object had empty mat slots
Occurred only with only_assigned_to_faces = True
A bit related to #4675

Error was
    styles = [style for style, usage in zip(styles, usage_count, strict=True) if usage > 0]

ValueError: zip() argument 2 is longer than argument 1
2024-05-16 18:14:06 +05:00
Andrej730 d001180082 remove unused code 2024-05-16 18:14:06 +05:00
Andrej730 3884403231 typing 2024-05-16 18:14:06 +05:00
Andrej730 8d4b5d83e1 same as e1250f217 2024-05-16 16:13:22 +05:00
Andrej730 e1250f2177 fix saving debug info to clipboard #4675
now we just rely on blender to make it crossplatform
2024-05-16 16:00:38 +05:00
Andrej730 a2ee920a5f fix group.update_group_products to work with multiple rels 2024-05-15 17:18:34 +05:00
Andrej730 fdbe74a432 maintain snake in api calls
there were only two methods using camel case for arguments
2024-05-15 17:18:34 +05:00
Andrej730 4e39fb3edd fix issue adding layers (name is not optional in ifc) 2024-05-15 17:18:34 +05:00
Andrej730 c2049246cd fix issue adding classification failing to use a None value for the date 2024-05-15 17:18:34 +05:00
Andrej730 335e2b7a78 ifc2x3 tests 2024-05-15 17:18:34 +05:00
Andrej730 bb8e84e5ec typing 2024-05-15 17:18:33 +05:00
Andrej730 2fa30be0d0 small optimization for da1bdc802
On large projects predict dense mesh stage can take 20s+ and reusing attribute value can save up to half of this time.
Using indices also helps but it's not that significant and sometimes it's the same time as using attribute names.
2024-05-14 18:19:13 +05:00
Bruno Perdigão c50149ad87 added 'product' parameter to a few usages of 'pset.remove_pset' after ebd03e9 2024-05-14 17:48:05 +05:00
Dion Moult da1bdc802d Accommodate invalid models coming from Cadwork 2024-05-14 16:16:18 +10:00
Andrej730 a9cba30da6 ifc2x3 tests
removed part of test_append_two_type_products_sharing_the_same_material_indirectly_via_a_material_set for ifc2x3 compatibility and removed part is already tested in test_append_two_type_products_sharing_the_same_material_with_properties
2024-05-13 18:01:18 +05:00
Andrej730 3665dda87c library.remove_library, remove_reference to support ifc2x3 2024-05-13 18:01:18 +05:00
Andrej730 f2696d5352 avoid confusing TypeErrors from api calls
After ab5ea4c85 it was always throwing wrong singature errors like below even if TypeError was caused by some internal issues inside API - it was adding couple extra steps to traceback making errors more noisy.

TypeError: Incorrect function arguments provided for library.edit_library
attribute 'VersionDate' for entity 'IFC2X3.IfcLibraryInformation' is expecting value of type 'ENTITY INSTANCE', got 'str'.. You specified args (<ifcopenshell.file.file object at 0x0000027EB8E6BCD0>,) and settings {'library': #1=IfcLibraryInformation('Name','Version',$,$,$), 'attributes': {'Name': 'Name', 'Version': 'Version', 'VersionDate': 'VersionDate', 'Location': 'Location', 'Description': 'Description'}}
E
Correct signature is (file: ifcopenshell.file.file, library: ifcopenshell.entity_instance.entity_instance, attributes: dict[str, typing.Any]) -> None
See help(ifcopenshell.api.library.edit_library) for documentation.
2024-05-13 18:01:18 +05:00
Andrej730 5e394e1576 library.edit_library - support datetime for VersionDate attribute 2024-05-13 18:01:18 +05:00
Andrej730 ebd03e9290 typing 2024-05-13 18:01:17 +05:00
Andrej730 c3bfd7354f pset.add_pset - throw an error if entity doesn't support adding a pset 2024-05-13 18:01:17 +05:00
Andrej730 1d046eaadf material.remove_material and copy_material to handle ifc2x3 props 2024-05-13 18:01:17 +05:00
Andrej730 2f554db54b owner.remove_person to remove IfcInventory in ifc2x3 2024-05-13 18:01:17 +05:00
Andrej730 54b4cef25e fix errors appending related materials in ifc2x3
it wasn't processing materials properties correctly because it was expecting material to have class IfcMaterialDefinition
2024-05-13 18:01:17 +05:00
Andrej730 c6106e6636 fix edit_pset error for editing material properties in ifc2x3 2024-05-13 18:01:17 +05:00
Andrej730 865dbab3ec fix error removing array pset after c50d1ea2f 2024-05-13 18:01:17 +05:00
Andrej730 76e6c63a01 ifc2x3 tests - prevent using removed user/application
When some test was creating an element and then removing it, it would also remove user and application as they wasn't used anywhere else.
`ifcopenshell.util.element.remove_deep2(file, history)` we use in every api for element deletion can possibly remove user and application which can be unsafe if `get_user` is returning some specific entity that then will become invalid.

Also fixed tests breaking due ifcownerhistory and user/application appearing in ifc2x3.
2024-05-13 18:01:16 +05:00
Gorgious56 3320accc7a The error box is now drawn in red to draw attention to it 2024-05-13 12:00:01 +02:00
Gorgious56 ac686db298 Prevent error when user deletes all spatial structure elements and the spatial manager panel is expanded 2024-05-13 11:57:48 +02:00
Dion Moult aca26c7597 Fix #4659. 2024-05-12 17:16:50 +10:00
247 changed files with 2943 additions and 1512 deletions
+6 -3
View File
@@ -36,7 +36,9 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
# use extra version to make pre-release using eg semver
set(EXTRA_VERSION "-alpha.3")
if(NOT DEFINED EXTRA_VERSION)
set(EXTRA_VERSION "-alpha.3")
endif()
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
option(WASM_BUILD "Build a WebAssembly binary." OFF)
@@ -1147,9 +1149,10 @@ endif()
# Packaging
list(APPEND CPACK_SOURCE_IGNORE_FILES
.git
.gitignore
"/\\\\.git"
"/build/"
)
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}${EXTRA_VERSION}")
SET(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}")
+1 -1
View File
@@ -13,7 +13,7 @@ readme = "README.md"
requires-python = ">=3.8"
keywords = ["IFC", "BCF", "BIM"]
dependencies = [
"xsdata",
"xsdata>=24.4",
"numpy",
"ifcopenshell",
]
+7 -5
View File
@@ -10,14 +10,14 @@ from xsdata.formats.dataclass.serializers.config import SerializerConfig
def build_xml_parser(context: Optional[XmlContext] = None) -> XmlParser:
"""Return a parser for an XML file."""
parser = XmlParser(context=context or XmlContext())
parser.register_namespace("xs", "http://www.w3.org/2001/XMLSchema")
parser.register_namespace(ns_map=parser.ns_map, prefix="xs", uri="http://www.w3.org/2001/XMLSchema")
return parser
def build_serializer(context: Optional[XmlContext] = None) -> XmlSerializer:
"""Return a serializer for an XML file."""
return XmlSerializer(
config=SerializerConfig(pretty_print=True),
config=SerializerConfig(indent=" "),
context=context or XmlContext(),
)
@@ -36,8 +36,9 @@ class AbstractXmlParserSerializer(Protocol):
xml: The XML file as bytes.
clazz: The class to parse to.
"""
...
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
def serialize(self, obj: object, ns_map: Optional[dict[str, str]] = None) -> str:
"""
Serialize an object to XML.
@@ -48,6 +49,7 @@ class AbstractXmlParserSerializer(Protocol):
Returns:
The XML as string.
"""
...
class XmlParserSerializer:
@@ -68,7 +70,7 @@ class XmlParserSerializer:
"""
return self.parser.from_bytes(xml, clazz)
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
def serialize(self, obj: object, ns_map: Optional[dict[Optional[str], str]] = None) -> str:
"""
Serialize an object to XML.
@@ -79,5 +81,5 @@ class XmlParserSerializer:
Returns:
The XML as string.
"""
ns_map = ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
ns_map = ns_map or self.parser.ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
return self.serializer.render(obj, ns_map)
+6 -29
View File
@@ -277,34 +277,6 @@ endif
cp -r dist/working/pyparsing-2.4.5/pyparsing.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/5b/ef/f97c3e1a7efa00e989a793fe15297214fc95ad7d9e3810586bd08ce9f0f3/xmlschema-2.0.2.tar.gz
cd dist/working && tar -xzvf xmlschema*
cp -r dist/working/xmlschema-2.0.2/xmlschema dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/ad/c7/17c9d16320d8e2cfdb27d2fd298b5e8f7a3f211025b0c1bc7a39a85bd690/xsdata-22.11.tar.gz
cd dist/working && tar -xzvf xsdata*
cp -r dist/working/xsdata-22.11/xsdata dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/11/bc/5afb61dd5d863e5cf77cd952445c50c17e65953405986f19e97e4389692a/elementpath-3.0.2.tar.gz
cd dist/working && tar -xzvf elementpath*
cp -r dist/working/elementpath-3.0.2/elementpath dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/21/9f/b251f7f8a76dec1d6651be194dfba8fb8d7781d10ab3987190de8391d08e/six-1.14.0.tar.gz
cd dist/working && tar -xzvf six*
cp -r dist/working/six-1.14.0/six.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFCCSV and ifcopenshell.util.selector
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/18/4d/8d522136c37d9e1ea74062b41b8d5e1318ebf45063ae46ce72ed60af223b/lark-parser-0.8.5.tar.gz
@@ -366,7 +338,12 @@ endif
# Provides Brickschema functionality
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
# Required for SVG to DXF conversion
cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install ezdxf --target=./site-packages
# Required by bcf
cd dist/working && . env/bin/activate && $(PIP) install xsdata --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install xmlschema --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install elementpath --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install six --target=./site-packages
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
+25 -12
View File
@@ -120,11 +120,16 @@ if sys.modules.get("bpy", None):
bl_context = "scene"
def draw(self, context):
layout = self.layout
layout.label(text="BlenderBIM could not load.", icon="ERROR")
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
info = get_debug_info()
layout = self.layout
layout.alert = True
layout.label(text="BlenderBIM could not load.", icon="ERROR")
if info["os"] == "Windows":
layout.operator("wm.console_toggle", text="View the console for full logs.", icon="CONSOLE")
else:
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
py = ".".join(info["python_version"].split(".")[0:2])
b3d = ".".join(info["blender_version"].split(".")[0:2])
box.label(text=f"Blender {b3d} {info['os']} {info['machine']}", icon="BLENDER")
@@ -133,6 +138,21 @@ if sys.modules.get("bpy", None):
op = layout.operator("bim.open_uri", text="How Can I Fix This?")
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html#installation-issues"
layout.label(text="Try Reinstalling:", icon="IMPORT")
op = layout.operator("bim.open_uri", text="Re-download Add-on")
bbim_date = info["blenderbim_version"].split(".")[-1]
py_tag = py.replace(".", "")
if "Linux" in info["os"]:
os = "linux"
elif "Darwin" in info["os"]:
if "arm64" in info["machine"]:
os = "macosm1"
else:
os = "macos"
else:
os = "win"
op.uri = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-{bbim_date}/blenderbim-{bbim_date}-py{py_tag}-{os}.zip"
class OpenUri(bpy.types.Operator):
bl_idname = "bim.open_uri"
bl_label = "Open URI"
@@ -149,14 +169,7 @@ if sys.modules.get("bpy", None):
def execute(self, context):
info = format_debug_info(get_debug_info())
if platform.system() == "Windows":
command = "echo | set /p nul=" + info
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
context.window_manager.clipboard = info
return {"FINISHED"}
class HiddenPanel:
@@ -0,0 +1,45 @@
/*
* BlenderBIM Add-on - OpenBIM Blender Add-on
* Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
*
* This file is part of BlenderBIM Add-on.
*
* BlenderBIM Add-on is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* You may copy this `schedule.css` template alongside your input schedule
* document with the same filename. For example if you have a schedule called
* `door_types.ods`, you can create a `door_types.css` in the same folder to
* style that schedule.
*/
/**
* If you specify a font size in CSS, such as text { font-size: 5; }, all fonts
* will be overriden to match that size.
*
* If your CSS, ODS, XLSX does not specify a font size, the variables below
* will specify the default font size.
*
* If your ODS, XLSX does specify a font size, they will scale linearly based
* on the variables below.
*/
:root {
--font-size-pt: 12;
--font-size-px: 4.13;
--font-width: 0.45;
}
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; }
+6 -2
View File
@@ -29,7 +29,7 @@ from mathutils import geometry
from mathutils import Vector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from typing import Optional, Callable, Any
from typing import Optional, Callable, Any, Union
def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None):
@@ -91,7 +91,11 @@ def import_attributes(ifc_class, props, data, callback=None):
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(element, props, callback=None):
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
props: bpy.types.PropertyGroup,
callback: Optional[Callable] = None,
) -> None:
if isinstance(element, str):
attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes()
info = {a.name(): None for a in attributes}
+21 -13
View File
@@ -26,6 +26,7 @@ import bmesh
import logging
import mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
@@ -303,22 +304,22 @@ class IfcImporter:
self.update_progress(100)
bpy.context.window_manager.progress_end()
def is_element_far_away(self, element):
def is_element_far_away(self, element: ifcopenshell.entity_instance) -> bool:
try:
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
point = placement[:, 3][0:3]
return self.is_point_far_away(point, is_meters=False)
except:
pass
return False
def is_point_far_away(self, point, is_meters=True):
def is_point_far_away(
self, point: Union[ifcopenshell.entity_instance, npt.NDArray[np.float64]], is_meters: bool = True
) -> bool:
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
limit = self.ifc_import_settings.distance_limit
limit = limit if is_meters else (limit / self.unit_scale)
coords = point
if hasattr(point, "Coordinates"):
coords = point.Coordinates
coords = getattr(point, "Coordinates", point)
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
def process_context_filter(self):
@@ -608,7 +609,9 @@ class IfcImporter:
threshold = 10000 # Just from experience.
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
# The check for CfsFaces/Faces/CoordIndex accommodates invalid data from Cadwork
# 0 IfcClosedShell.CfsFaces
faces = [len(faces) for e in self.file.by_type("IfcClosedShell") if (faces := e[0])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
@@ -616,12 +619,14 @@ class IfcImporter:
if self.file.schema == "IFC2X3":
return
faces = [len(e.Faces) for e in self.file.by_type("IfcPolygonalFaceSet")]
# 2 IfcPolygonalFaceSet.Faces
faces = [len(faces) for e in self.file.by_type("IfcPolygonalFaceSet") if (faces := e[2])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
faces = [len(e.CoordIndex) for e in self.file.by_type("IfcTriangulatedFaceSet")]
# 3 IfcTriangulatedFaceSet.CoordIndex
faces = [len(index) for e in self.file.by_type("IfcTriangulatedFaceSet") if (index := e[3])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
@@ -679,7 +684,7 @@ class IfcImporter:
props.blender_orthogonal_height = str(offset_point[2])
props.has_blender_offset = True
def get_offset_point(self):
def get_offset_point(self) -> Union[npt.NDArray[np.float64], None]:
elements_checked = 0
# If more than these elements aren't far away, the file probably isn't absolutely positioned
element_checking_threshold = 10
@@ -714,7 +719,7 @@ class IfcImporter:
if self.is_point_far_away(point, is_meters=False):
return point
def does_element_likely_have_geometry_far_away(self, element):
def does_element_likely_have_geometry_far_away(self, element: ifcopenshell.entity_instance) -> bool:
for representation in element.Representation.Representations:
items = []
for item in representation.Items:
@@ -731,13 +736,14 @@ class IfcImporter:
if subelement.is_a("IfcCartesianPoint"):
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
return False
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
if obj.data and obj.data.get("has_cartesian_point_offset", None):
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
elif self.is_point_far_away((matrix[0, 3], matrix[1, 3], matrix[2, 3])):
elif self.is_point_far_away((matrix[:3, 3])):
obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
@@ -750,7 +756,9 @@ class IfcImporter:
return mathutils.Matrix(matrix.tolist())
def find_decomposed_ifc_class(self, element, ifc_class):
def find_decomposed_ifc_class(
self, element: ifcopenshell.entity_instance, ifc_class: str
) -> Union[ifcopenshell.entity_instance, None]:
if element.is_a(ifc_class):
return element
rel_aggregates = element.IsDecomposedBy
@@ -100,7 +100,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
@@ -27,15 +27,12 @@ classes = (
operator.CopyAttributeToSelection,
prop.BIMAttributeProperties,
ui.BIM_PT_object_attributes,
ui.BIM_PT_material_attributes,
)
def register():
bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
bpy.types.Material.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
def unregister():
del bpy.types.Object.BIMAttributeProperties
del bpy.types.Material.BIMAttributeProperties
@@ -23,7 +23,6 @@ import blenderbim.tool as tool
def refresh():
AttributesData.is_loaded = False
MaterialAttributesData.is_loaded = False
class AttributesData:
@@ -51,28 +50,3 @@ class AttributesData:
key = "STEP ID"
results.append({"name": key, "value": str(value)})
return results
class MaterialAttributesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def ifc_definition_id(cls):
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
@classmethod
def attributes(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
data = element.get_info()
for key, value in data.items():
if value is None or isinstance(value, ifcopenshell.entity_instance) or key in ["id", "type"]:
continue
results.append({"name": key, "value": str(value)})
return results
@@ -40,14 +40,10 @@ class EnableEditingAttributes(bpy.types.Operator):
bl_label = "Enable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
self.file = IfcStore.get_file()
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.attributes.clear()
@@ -85,13 +81,9 @@ class DisableEditingAttributes(bpy.types.Operator):
bl_label = "Disable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
return {"FINISHED"}
@@ -102,14 +94,10 @@ class EditAttributes(bpy.types.Operator, Operator):
bl_label = "Edit Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
@@ -126,7 +114,7 @@ class EditAttributes(bpy.types.Operator, Operator):
attributes = blenderbim.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
bpy.ops.bim.disable_editing_attributes(obj=obj.name)
return {"FINISHED"}
@@ -19,28 +19,25 @@
import blenderbim.bim.helper
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.attribute.data import AttributesData, MaterialAttributesData
from blenderbim.bim.module.attribute.data import AttributesData
def draw_ui(context, layout, obj_type, attributes):
obj = context.active_object if obj_type == "Object" else context.active_object.active_material
def draw_ui(context, layout, attributes):
obj = context.active_object
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
if props.is_editing_attributes:
row = layout.row(align=True)
op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
op.obj_type = obj_type
op.obj = obj.name
op = row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
op.obj_type = obj_type
op.obj = obj.name
blenderbim.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
else:
row = layout.row()
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
op.obj_type = obj_type
op.obj = obj.name
for attribute in attributes:
@@ -72,32 +69,4 @@ class BIM_PT_object_attributes(Panel):
def draw(self, context):
if not AttributesData.is_loaded:
AttributesData.load()
draw_ui(context, self.layout, "Object", AttributesData.data["attributes"])
class BIM_PT_material_attributes(Panel):
bl_label = "Material Attributes"
bl_idname = "BIM_PT_material_attributes"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
@classmethod
def poll(cls, context):
if not IfcStore.get_file():
return False
try:
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
except:
return False
def draw(self, context):
if not MaterialAttributesData.is_loaded:
MaterialAttributesData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialAttributesData.data["ifc_definition_id"]
):
MaterialAttributesData.load()
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
draw_ui(context, self.layout, AttributesData.data["attributes"])
@@ -19,10 +19,13 @@
import bpy
import ifcopenshell
import ifcopenshell.util.cost
import ifcopenshell.util.date
import ifcopenshell.util.element
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.bim.schema
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
from typing import Any
def refresh():
@@ -34,6 +37,7 @@ def refresh():
class CostSchedulesData:
data = {}
is_loaded = False
_cost_values: dict[int, dict[str, Any]]
@classmethod
def load(cls):
@@ -136,7 +140,7 @@ class CostSchedulesData:
data["UnitBasisUnitSymbol"] = "U"
if cost_value.Category == "*":
is_sum = True
cost_quantity = data["TotalCostQuantity"] or 1
cost_quantity = 1 if data["TotalCostQuantity"] is None else data["TotalCostQuantity"]
if has_unit_basis:
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity / data["UnitBasisValueComponent"]
else:
@@ -155,6 +159,7 @@ class CostSchedulesData:
data["UnitSymbol"] = "-"
if cost_item.CostQuantities:
quantity = cost_item.CostQuantities[0]
data["QuantityType"] = quantity.is_a()
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
if unit:
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
@@ -223,7 +223,7 @@ class EditCostItem(bpy.types.Operator, tool.Ifc.Operator):
class AssignCostItemType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_cost_item_type"
bl_label = "Assign Cost Item Type Product"
bl_label = "Assign Cost Item To Product Types"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
prop_name: bpy.props.StringProperty()
@@ -509,7 +509,7 @@ class SelectCostScheduleProducts(bpy.types.Operator):
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "import_cost_schedule_csv.bim"
bl_idname = "bim.import_cost_schedule_csv"
bl_label = "Import Cost Schedule CSV"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
@@ -21,6 +21,7 @@ import blenderbim.bim.module.cost.prop as CostProp
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.cost.data import CostSchedulesData
from typing import Any
class BIM_PT_cost_schedules(Panel):
@@ -53,7 +54,7 @@ class BIM_PT_cost_schedules(Panel):
row.label(text="No Cost Schedules found.", icon="TEXT")
row.operator("bim.add_cost_schedule", icon="ADD", text="")
row.operator("import_cost_schedule_csv.bim",icon="IMPORT",text="")
row.operator("bim.import_cost_schedule_csv",icon="IMPORT",text="")
for schedule in CostSchedulesData.data["schedules"]:
self.draw_cost_schedule_ui(schedule)
@@ -178,24 +179,33 @@ class BIM_PT_cost_schedules(Panel):
"active_cost_item_index",
)
if self.props.active_cost_item_id:
cost_item = CostSchedulesData.data["cost_items"][ifc_definition_id]
if self.props.cost_item_editing_type == "ATTRIBUTES":
self.draw_editable_cost_item_attributes_ui()
elif self.props.cost_item_editing_type == "QUANTITIES":
self.draw_editable_cost_item_quantities_ui()
self.draw_editable_cost_item_quantities_ui(cost_item)
elif self.props.cost_item_editing_type == "VALUES":
self.draw_editable_cost_item_values_ui()
def draw_editable_cost_item_attributes_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.cost_item_attributes, self.layout)
def draw_editable_cost_item_quantities_ui(self):
def draw_editable_cost_item_quantities_ui(self, cost_item: dict[str, Any]):
quantities = CostSchedulesData.data["cost_quantities"]
row = self.layout.row(align=True)
row.prop(self.props, "quantity_types", text="")
# In IFC, all quantities of IfcCostTime should have 1 type.
if quantities:
quantity_class = cost_item["QuantityType"]
row.label(text=quantity_class)
else:
row.prop(self.props, "quantity_types", text="")
quantity_class = self.props.quantity_types
op = row.operator("bim.add_cost_item_quantity", text="", icon="ADD")
op.cost_item = self.props.active_cost_item_id
op.ifc_class = self.props.quantity_types
op.ifc_class = quantity_class
for quantity in CostSchedulesData.data["cost_quantities"]:
for quantity in quantities:
row = self.layout.row(align=True)
row.label(text=quantity["name"])
row.label(text=quantity["value"])
@@ -634,7 +644,7 @@ class BIM_UL_cost_items_trait:
layout.label(text=cost_item["UnitBasisUnitSymbol"])
def draw_total_quantity_column(self, layout, cost_item):
if cost_item["TotalCostQuantity"]:
if cost_item["TotalCostQuantity"] is not None:
label = "{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '-'}"
layout.label(text=label)
else:
@@ -60,13 +60,7 @@ class CopyDebugInformation(bpy.types.Operator):
print(text)
print("-" * 80)
if platform.system() == "Windows":
command = "echo | set /p nul=" + text
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
context.window_manager.clipboard = text
return {"FINISHED"}
@@ -159,9 +159,7 @@ class BaseDecorator:
objecttype = "NOTDEFINED"
def __init__(self):
self.font_id = blf.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
)
self.font_id = 0 # 0 is the default font
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
@@ -404,7 +402,6 @@ class BaseDecorator:
line_no += 1 if multiline_to_bottom else -1
return
# 0 is the default font, but we're fancier than that
font_id = self.font_id
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
@@ -2014,6 +2011,13 @@ 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"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
drawing_font_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", drawing_font)
if os.path.exists(drawing_font_path):
font_id = blf.load(drawing_font_path)
for decorator in self.decorators.values():
decorator.font_id = font_id
def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance?
@@ -315,9 +315,7 @@ class RasterStyleProperty(enum.Enum):
SPACE_SHADING = "space.shading"
RASTER_STYLE_PROPERTIES_EXCLUDE = (
"scene.render.filepath",
)
RASTER_STYLE_PROPERTIES_EXCLUDE = ("scene.render.filepath",)
class DocProperties(PropertyGroup):
@@ -371,6 +369,7 @@ class DocProperties(PropertyGroup):
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")
class BIMCameraProperties(PropertyGroup):
@@ -16,22 +16,21 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
import os
import re
import bpy
import string
import svgwrite
import openpyxl
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
from odf.opendocument import load as load_ods
from odf.table import Table, TableRow, TableColumn, TableCell
from odf.text import P
from odf.style import Style
from textwrap import wrap
from pathlib import Path
import string
FONT_SIZE = 4.13
FONT_WIDTH = lambda size: size * 0.45
FONT_SIZE_PT = 12
FONT_FAMILY = "OpenGost Type B TT"
DEBUG = False
@@ -63,11 +62,29 @@ class Scheduler:
)
self.padding = 1
self.margin = 1
self.parse_css(infile)
if infile.endswith("ods"):
self.schedule_ods(infile, outfile)
elif infile.endswith("xlsx"):
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile):
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "schedule.css")
with open(stylesheet_path, "r") as stylesheet:
css = stylesheet.read()
matches = re.search("--font-size-pt:\s*([0-9.]+);", css)
self.font_size_pt = float(matches.groups()[0]) if matches else 12 # Default to 12pt
matches = re.search("--font-size-px:\s*([0-9.]+);", css)
self.font_size_px = float(matches.groups()[0]) if matches else 4.13 # Magic number 4.13px ~= 12pt
matches = re.search("--font-width:\s*([0-9.]+);", css)
self.font_width = float(matches.groups()[0]) if matches else 0.45 # A magic number for OpenGost
self.svg.defs.add(self.svg.style(css))
def schedule_xlsx(self, infile, outfile):
workbook = openpyxl.open(infile, data_only=True)
sheet = workbook.active
@@ -144,8 +161,8 @@ class Scheduler:
x += unmerged_width
continue
font_size = cell.font.size or 11 # 11pt default
font_size = font_size / FONT_SIZE_PT * FONT_SIZE # Magic?
font_size = cell.font.size or self.font_size_pt # 12pt default
font_size = font_size / self.font_size_pt * self.font_size_px # Magic?
text_position = [0.0, 0.0]
if cell.alignment.horizontal == "left":
@@ -424,9 +441,9 @@ class Scheduler:
italic_text = final_cell_style.get("font-style", None) == "italic"
# NOTE: very naive since we're scaling text proportionally
font_size = (
float(final_cell_style.get("font-size", f"{FONT_SIZE_PT}pt")[:-2])
/ FONT_SIZE_PT
* FONT_SIZE
float(final_cell_style.get("font-size", f"{self.font_size_pt}pt")[:-2])
/ self.font_size_pt
* self.font_size_px
)
if p_tags:
@@ -523,10 +540,7 @@ class Scheduler:
"""
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
text_params = {
"font-size": font_size,
"font-family": FONT_FAMILY,
}
text_params = {"font-size": font_size}
if bold:
text_params["font-weight"] = "bold"
if italic:
@@ -546,13 +560,13 @@ class Scheduler:
text_tag = self.svg.text("", **(text_params | {"font-size": "0"} | box_alignment_params))
# TODO: should be done in less naive way
# without using magic number for FONT_WIDTH
# currently it might not work for all fonts and font sizes
# TODO: Should be done without using magic number for self.font_width
if wrap_text:
wrapped_lines = []
for line in text_lines:
wrapped_line = wrap(line, width=int(cell_width // FONT_WIDTH(font_size)), break_long_words=False)
wrapped_line = wrap(
line, width=int(cell_width // (font_size * self.font_width)), break_long_words=False
)
wrapped_lines.extend(wrapped_line)
else:
wrapped_lines = text_lines
@@ -400,7 +400,11 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
representation_data["profile_set_usage"] = tool.Geometry.get_profile_set_usage(product)
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
# TODO: replace with core.add_representation?
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
if new_representation is None:
self.report({"ERROR"}, "Error creating representation for Blender object.")
return {"CANCELLED"}
if tool.Geometry.is_body_representation(new_representation):
[
@@ -938,7 +942,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0],pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
@@ -1050,7 +1054,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if self.group_name in product_groups_name:
return
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name)
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.group_name)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
@@ -23,6 +23,7 @@ import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.misc as core
import blenderbim.core.geometry as core_geometry
import blenderbim.core.root
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector, Matrix, Euler
@@ -134,6 +135,10 @@ class ResizeToStorey(bpy.types.Operator, Operator):
class SplitAlongEdge(bpy.types.Operator, Operator):
bl_idname = "bim.split_along_edge"
bl_label = "Split Along Edge"
bl_description = (
"Active object is considered to be a cutting object."
"Will unassign element from a type if type has a representation."
)
bl_options = {"REGISTER", "UNDO"}
@classmethod
@@ -144,16 +149,25 @@ class SplitAlongEdge(bpy.types.Operator, Operator):
cutter = context.active_object
objs = [o for o in context.selected_objects if o != cutter]
objs_to_cut = []
# Splitting only works on meshes
for obj in objs:
# You cannot split meshes if the representation is mapped.
element = tool.Ifc.get_entity(obj)
if element:
relating_type = tool.Root.get_element_type(element)
if relating_type and tool.Root.does_type_have_representations(relating_type):
bpy.ops.bim.unassign_type(related_object=obj.name)
if not element:
continue
relating_type = tool.Root.get_element_type(element)
if relating_type and tool.Root.does_type_have_representations(relating_type):
bpy.ops.bim.unassign_type(related_object=obj.name)
# refresh representation
representation = tool.Geometry.get_active_representation(obj)
# skip empty objects that might get in the way
if not representation:
continue
core_geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -168,11 +182,13 @@ class SplitAlongEdge(bpy.types.Operator, Operator):
if not tool.Geometry.is_meshlike(representation):
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="IfcTessellatedFaceSet")
new_objs = tool.Misc.split_objects_with_cutter(objs, cutter)
objs_to_cut.append(obj)
new_objs = tool.Misc.split_objects_with_cutter(objs_to_cut, cutter)
for obj in new_objs:
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
bpy.ops.bim.update_representation(obj=obj.name)
for obj in objs:
for obj in objs_to_cut:
bpy.ops.bim.update_representation(obj=obj.name)
representation = tool.Geometry.get_active_representation(obj)
@@ -187,6 +203,8 @@ class SplitAlongEdge(bpy.types.Operator, Operator):
apply_openings=True,
)
self.report({"INFO"}, f"Splitting finished, {len(new_objs)} new objects created.")
class GetConnectedSystemElements(bpy.types.Operator, Operator):
bl_idname = "bim.get_connected_system_elements"
@@ -83,17 +83,8 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props = obj.BIMArrayProperties
relating_obj = props.relating_array_object
if relating_obj:
element = tool.Ifc.get_entity(relating_obj)
parent_globalid = ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Parent")
parent_element = tool.Ifc.get().by_guid(parent_globalid)
data = json.loads(ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array", "Data"))[self.item]
else:
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props.count = data["count"]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props.x = data["x"] * si_conversion
@@ -102,9 +93,7 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
props.use_local_space = data.get("use_local_space", False)
props.sync_children = data.get("sync_children", False)
props.method = data.get("method", "OFFSET")
props.is_editing = self.item
return {"FINISHED"}
@@ -148,10 +137,6 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
#clears the relating_array_object so it doesn't show again next time
props.relating_array_object = None
return {"FINISHED"}
@@ -208,7 +193,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
pset = tool.Ifc.get().by_id(pset["id"])
if len(data) == 1:
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
else:
del data[self.item]
data = tool.Ifc.get().createIfcText(json.dumps(data))
@@ -53,7 +53,7 @@ class AuthoringData:
cls.data["ifc_element_type"] = cls.ifc_element_type
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["relating_type_id"] = cls.relating_type_id() # only after .ifc_classes()
cls.data["predefined_type"] = cls.predefined_type()
cls.data["predefined_type"] = cls.predefined_type() # only after .relating_type_id()
cls.data["type_class"] = cls.type_class()
# only after .type_class()
@@ -251,6 +251,8 @@ class AuthoringData:
@classmethod
def predefined_type(cls):
if not tool.Blender.enum_property_has_valid_index(cls.props, "relating_type_id", cls.data["relating_type_id"]):
return
relating_type_id = cls.props.relating_type_id
if not relating_type_id:
return
@@ -644,6 +644,6 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMDoorProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -44,6 +44,7 @@ from bpy.types import SpaceView3D
from bpy.props import FloatProperty
from bpy_extras.object_utils import AddObjectHelper, object_data_add
from gpu_extras.batch import batch_for_shader
from typing import Union, Optional, Any
class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
@@ -59,9 +60,15 @@ class AddFilledOpening(bpy.types.Operator, tool.Ifc.Operator):
class FilledOpeningGenerator:
def generate(self, filling_obj, voided_obj, target=None):
def generate(
self,
filling_obj: Union[bpy.types.Object, None],
voided_obj: Union[bpy.types.Object, None],
target: Optional[Vector] = None,
) -> None:
props = bpy.context.scene.BIMModelProperties
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
opening_thickness_si = None
filling = tool.Ifc.get_entity(filling_obj)
element = tool.Ifc.get_entity(voided_obj)
@@ -92,6 +99,7 @@ class FilledOpeningGenerator:
# In this prototype, we assume openings are only added to axis-based elements
layers = tool.Model.get_material_layer_parameters(element)
if layers["layer_set_direction"] == "AXIS2":
opening_thickness_si = layers["thickness"] * 2
axis = tool.Model.get_wall_axis(voided_obj, layers=layers)["base"]
new_matrix = voided_obj.matrix_world.copy()
point_on_axis = tool.Cad.point_on_edge(target, axis)
@@ -145,7 +153,9 @@ class FilledOpeningGenerator:
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
else:
representation = self.generate_opening_from_filling(filling, filling_obj)
representation = self.generate_opening_from_filling(
filling, filling_obj, opening_thickness_si=opening_thickness_si
)
opening = ifcopenshell.api.run(
"root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement", predefined_type="OPENING"
)
@@ -187,7 +197,7 @@ class FilledOpeningGenerator:
should_sync_changes_first=False,
)
def regenerate_from_type(self, usecase_path, ifc_file, settings):
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
relating_type = settings["relating_type"]
for related_object in settings["related_objects"]:
@@ -252,10 +262,15 @@ class FilledOpeningGenerator:
should_sync_changes_first=False,
)
def generate_opening_from_filling(self, filling, filling_obj):
def generate_opening_from_filling(
self,
filling: ifcopenshell.entity_instance,
filling_obj: bpy.types.Object,
opening_thickness_si: Optional[float] = None,
) -> ifcopenshell.entity_instance:
# Since openings are reused later, we give a default thickness of 1.2m
# which should cover the majority of curved, or super thick walls.
thickness = 1.2
thickness = 1.2 if opening_thickness_si is None else opening_thickness_si
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -346,7 +361,9 @@ class FilledOpeningGenerator:
return True
return False
def get_existing_opening_occurrence_if_any(self, filling):
def get_existing_opening_occurrence_if_any(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
filling_type = ifcopenshell.util.element.get_type(filling)
if filling_type:
filling_occurrences = ifcopenshell.util.element.get_types(filling_type)
@@ -138,7 +138,10 @@ class AddConstrTypeInstance(bpy.types.Operator):
if not relating_type_id:
return {"FINISHED"}
if self.from_invoke:
# Check relating_type_id enum_items since it's possible
# that we're adding e.g. IfcRoofType being in a Slab Tool
# and roof type id won't be present in the relating_type_id enum.
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
props.relating_type_id = str(self.relating_type_id)
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
@@ -931,13 +931,11 @@ class PatchNonParametricMepSegment(bpy.types.Operator, tool.Ifc.Operator):
return context.active_object
def _execute(self, context):
styles = tool.Geometry.get_styles(context.active_object)
blenderbim.core.material.patch_non_parametric_mep_segment(
tool.Ifc, tool.Material, tool.Profile, obj=context.active_object
)
bpy.ops.bim.enable_editing_extrusion_axis()
bpy.ops.bim.edit_extrusion_axis()
styles = tool.Geometry.get_styles(context.active_object)
class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
@@ -87,21 +87,6 @@ def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
def update_relating_array_from_object(self, context):
bpy.ops.bim.enable_editing_array(item=self.is_editing)
return
def is_object_array_applicable(self, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return False
return ifcopenshell.util.element.get_pset(element, "BBIM_Array")
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
relating_type_id: bpy.props.EnumProperty(
@@ -218,14 +203,6 @@ class BIMArrayProperties(PropertyGroup):
description="Regenerate all children based on the parent object",
default=False,
)
relating_array_object: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Copy Array Properties",
update=update_relating_array_from_object,
poll=is_object_array_applicable,
)
class BIMStairProperties(PropertyGroup):
@@ -535,5 +535,5 @@ class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMRailingProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -757,7 +757,7 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMRoofProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -337,6 +337,6 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMStairProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -223,8 +223,6 @@ class BIM_PT_array(bpy.types.Panel):
row = col.row(align=True)
row.prop(props, "z")
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "relating_array_object", icon="COPYDOWN")
else:
row = box.row(align=True)
name = f"{array['count']} Items ({array.get('method', 'OFFSET').capitalize()})"
@@ -590,6 +590,6 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMWindowProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -36,8 +36,8 @@ classes = (
operator.EnableCulling,
operator.EnableEditingHeader,
operator.ExportIFC,
operator.ExportIFCDeprecated,
operator.FlipClippingPlane,
operator.ImportIFC,
operator.LinkIfc,
operator.LoadLink,
operator.LoadLinkedProject,
@@ -97,7 +97,7 @@ def register():
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
kmi = km.keymap_items.new("export_ifc.bim", "S", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.export_ifc", "S", "PRESS", ctrl=True)
kmi.properties.should_save_as = False
addon_keymaps.append((km, kmi))
@@ -1106,8 +1106,8 @@ class ToggleLinkVisibility(bpy.types.Operator):
]
class ExportIFC(bpy.types.Operator):
bl_idname = "export_ifc.bim"
class ExportIFCBase:
bl_idname = "bim.export_ifc"
bl_label = "Save IFC"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
@@ -1224,14 +1224,20 @@ class ExportIFC(bpy.types.Operator):
return "Save the IFC file. Will save both .IFC/.BLEND files if synced together"
class ImportIFC(bpy.types.Operator):
bl_idname = "import_ifc.bim"
bl_label = "Import IFC"
bl_options = {"REGISTER", "UNDO"}
class ExportIFC(ExportIFCBase, bpy.types.Operator):
bl_idname = "bim.export_ifc"
# TODO: remove as deprecated, better wait couple releases since
# this operator is used for saving IFC files in user scripts.
class ExportIFCDeprecated(ExportIFCBase, bpy.types.Operator):
bl_idname = "export_ifc.bim"
def execute(self, context):
bpy.ops.bim.load_project("INVOKE_DEFAULT")
return {"FINISHED"}
msg = f"'{ExportIFCDeprecated.bl_idname}' operator name is deprecated, use '{ExportIFC.bl_idname}'."
self.report({"WARNING"}, msg)
print(msg)
return super().execute(context)
class LoadLinkedProject(bpy.types.Operator):
@@ -68,9 +68,9 @@ def file_menu(self, context):
op = self.layout.operator("bim.load_project", text="Open IFC Project", icon="FILEBROWSER")
op.should_start_fresh_session = True
self.layout.separator()
op = self.layout.operator("export_ifc.bim", icon="FILE_TICK", text="Save IFC Project")
op = self.layout.operator("bim.export_ifc", icon="FILE_TICK", text="Save IFC Project")
op.should_save_as = False
op = self.layout.operator("export_ifc.bim", text="Save IFC Project As...")
op = self.layout.operator("bim.export_ifc", text="Save IFC Project As...")
op.should_save_as = True
self.layout.separator()
self.layout.operator("bim.revert_project")
@@ -19,6 +19,7 @@
import bpy
import blenderbim.bim.schema
import ifcopenshell
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import blenderbim.tool as tool
from blenderbim.bim.prop import Attribute, StrProperty
@@ -21,6 +21,7 @@ import bpy
import pathlib
import ifcopenshell
import ifcopenshell.util.attribute
import ifcopenshell.util.doc
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
@@ -36,10 +37,14 @@ class PsetTemplatesData:
@classmethod
def load(cls):
cls.is_loaded = True
cls.data["pset_template_files"] = cls.pset_template_files()
# after pset_template_files
cls.data["pset_templates"] = cls.pset_templates()
# after pset_template_files because it loads IfcStore.pset_template_file
cls.data["primary_measure_type"] = cls.primary_measure_type()
cls.data["property_template_type"] = cls.property_template_type()
cls.data["pset_template_files"] = cls.pset_template_files()
cls.data["pset_templates"] = cls.pset_templates()
cls.data["pset_template"] = cls.pset_template()
cls.data["prop_templates"] = cls.prop_templates()
@@ -328,7 +328,7 @@ class EditResourceQuantity(bpy.types.Operator, tool.Ifc.Operator):
class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "import_resources.bim"
bl_idname = "bim.import_resources"
bl_label = "Import Resources"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
@@ -51,7 +51,7 @@ class BIM_PT_resources(Panel):
row.operator("bim.disable_resource_editing_ui", text="", icon="CANCEL")
else:
row.operator("bim.load_resources", text="", icon="GREASEPENCIL")
row.operator("import_resources.bim", text="", icon="IMPORT")
row.operator("bim.import_resources", text="", icon="IMPORT")
if not self.props.is_editing:
return
@@ -20,6 +20,7 @@ import re
import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.selector
@@ -214,7 +215,7 @@ class SaveSearch(Operator, tool.Ifc.Operator):
group = group[0]
group.Description = description
else:
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description)
if results:
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=list(results), group=group)
@@ -367,7 +368,7 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
description = json.dumps(
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -628,7 +628,7 @@ class DisableEditingWorkCalendar(bpy.types.Operator):
class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "import_csv.bim"
bl_idname = "bim.import_csv"
bl_label = "Import CSV"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
@@ -653,7 +653,7 @@ class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class ImportP6(bpy.types.Operator, ImportHelper):
bl_idname = "import_p6.bim"
bl_idname = "bim.import_p6"
bl_label = "Import P6"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
@@ -679,7 +679,7 @@ class ImportP6(bpy.types.Operator, ImportHelper):
class ImportP6XER(bpy.types.Operator, ImportHelper):
bl_idname = "import_p6xer.bim"
bl_idname = "bim.import_p6xer"
bl_label = "Import P6 XER"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xer"
@@ -705,7 +705,7 @@ class ImportP6XER(bpy.types.Operator, ImportHelper):
class ImportPP(bpy.types.Operator, ImportHelper):
bl_idname = "import_pp.bim"
bl_idname = "bim.import_pp"
bl_label = "Import Powerproject .pp"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".pp"
@@ -731,7 +731,7 @@ class ImportPP(bpy.types.Operator, ImportHelper):
class ImportMSP(bpy.types.Operator, ImportHelper):
bl_idname = "import_msp.bim"
bl_idname = "bim.import_msp"
bl_label = "Import MSP"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
@@ -757,7 +757,7 @@ class ImportMSP(bpy.types.Operator, ImportHelper):
class ExportMSP(bpy.types.Operator, ImportHelper):
bl_idname = "export_msp.bim"
bl_idname = "bim.export_msp"
bl_label = "Export MSP"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
@@ -787,7 +787,7 @@ class ExportMSP(bpy.types.Operator, ImportHelper):
class ExportP6(bpy.types.Operator, ImportHelper):
bl_idname = "export_p6.bim"
bl_idname = "bim.export_p6"
bl_label = "Export P6"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
@@ -44,6 +44,8 @@ def update_elevation(self, context):
def update_active_container_index(self, context):
if self.active_container_index < 0:
return
self.active_container_id = self.containers[self.active_container_index].ifc_definition_id
self.container_name = self.containers[self.active_container_index].name
self.elevation = self.containers[self.active_container_index].elevation
@@ -116,7 +116,7 @@ class BIM_PT_SpatialManager(Panel):
self.props = context.scene.BIMSpatialManagerProperties
row = self.layout.row()
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="Load Spatial Structure")
if self.props.active_container_index < len(self.props.containers):
if 0 <= self.props.active_container_index < len(self.props.containers):
ifc_definition_id = self.props.containers[self.props.active_container_index].ifc_definition_id
row = self.layout.row()
row.alignment = "RIGHT"
@@ -134,7 +134,7 @@ class BIM_PT_SpatialManager(Panel):
"active_container_index",
)
row = self.layout.row()
if self.props.active_container_index < len(self.props.containers):
if 0 <= self.props.active_container_index < len(self.props.containers):
row.prop(self.props, "container_name", text="")
row.prop(self.props, "elevation", text="")
op = row.operator("bim.edit_container_attributes", icon="CHECKMARK", text="Apply")
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.attribute
import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.core.structural as core
@@ -512,10 +512,7 @@ class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
element = tool.Ifc.get().by_id(self.element)
obj = tool.Ifc.get_object(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
if obj:
tool.Ifc.unlink(obj=obj)
bpy.data.objects.remove(obj)
tool.Geometry.delete_ifc_object(obj)
class RenameType(bpy.types.Operator, tool.Ifc.Operator):
+19 -14
View File
@@ -19,6 +19,7 @@
import os
import bpy
import addon_utils
import platform
from pathlib import Path
from bpy.types import Panel
from bpy.props import StringProperty, IntProperty, BoolProperty
@@ -290,8 +291,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row = layout.row()
row.prop(self, "spatial_elements_unselectable")
row = layout.row()
row.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save")
row = layout.row()
@@ -320,28 +319,30 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.prop(context.scene.BIMProperties, "data_dir")
row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.BIMProperties, "pset_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "sheets_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "layouts_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "titleblocks_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "drawings_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "stylesheet_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "markers_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "symbols_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "patterns_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "shadingstyles_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "shadingstyle_default")
row = self.layout.row()
row.prop(context.scene.DocProperties, "drawing_font")
# Scene panel groups
@@ -406,10 +407,14 @@ class BIM_PT_tabs(Panel):
if blenderbim.last_error:
box = self.layout.box()
box.alert=True
row = box.row(align=True)
row.label(text="BlenderBIM experienced an error :(", icon="ERROR")
row.operator("bim.close_error", text="", icon="CANCEL")
box.label(text="View the console for full logs.", icon="CONSOLE")
if platform.system() == "Windows":
box.operator("wm.console_toggle", text="View the console for full logs.", icon="CONSOLE")
else:
box.label(text="View the console for full logs.", icon="CONSOLE")
box.operator("bim.copy_debug_information", text="Copy Error Message To Clipboard")
op = box.operator("bim.open_uri", text="How Can I Fix This?")
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html"
+93 -58
View File
@@ -1,88 +1,117 @@
def add_cost_schedule(ifc, name, predefined_type):
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def add_cost_schedule(ifc: tool.Ifc, name, predefined_type):
ifc.run("cost.add_cost_schedule", name=name, predefined_type=predefined_type)
def edit_cost_schedule(ifc, cost, cost_schedule):
def edit_cost_schedule(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance):
attributes = cost.get_cost_schedule_attributes()
ifc.run("cost.edit_cost_schedule", cost_schedule=cost_schedule, attributes=attributes)
cost.disable_editing_cost_schedule()
def disable_editing_cost_schedule(cost):
def disable_editing_cost_schedule(cost: tool.Cost):
cost.disable_editing_cost_schedule()
def remove_cost_schedule(ifc, cost_schedule):
def remove_cost_schedule(ifc: tool.Ifc, cost_schedule: ifcopenshell.entity_instance):
ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule)
def enable_editing_cost_schedule_attributes(cost, cost_schedule):
def enable_editing_cost_schedule_attributes(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance):
cost.load_cost_schedule_attributes(cost_schedule)
cost.enable_editing_cost_schedule_attributes(cost_schedule)
def enable_editing_cost_items(cost, cost_schedule):
def enable_editing_cost_items(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance):
cost.enable_editing_cost_items(cost_schedule)
cost.load_cost_schedule_tree()
cost.play_sound()
def add_summary_cost_item(ifc, cost, cost_schedule):
def add_summary_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance):
ifc.run("cost.add_cost_item", cost_schedule=cost_schedule)
cost.load_cost_schedule_tree()
# cost.play_sound()
def add_cost_item(ifc, cost, cost_item):
def add_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
ifc.run("cost.add_cost_item", cost_item=cost_item)
cost.load_cost_schedule_tree()
# cost.enable_editing_cost_schedule_attributes(cost_schedule)
def expand_cost_item(cost, cost_item):
def expand_cost_item(cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
cost.expand_cost_item(cost_item)
cost.load_cost_schedule_tree()
def expand_cost_items(cost):
def expand_cost_items(cost: tool.Cost):
cost.expand_cost_items()
cost.load_cost_schedule_tree()
def contract_cost_item(cost, cost_item):
def contract_cost_item(cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
cost.contract_cost_item(cost_item)
cost.load_cost_schedule_tree()
def contract_cost_items(cost):
def contract_cost_items(cost: tool.Cost):
cost.contract_cost_items()
cost.load_cost_schedule_tree()
def remove_cost_item(ifc, cost, cost_item_id):
def remove_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_item_id: int):
cost_item = ifc.get().by_id(cost_item_id)
ifc.run("cost.remove_cost_item", cost_item=cost_item)
cost.clean_up_cost_item_tree(cost_item_id)
cost.load_cost_schedule_tree()
def enable_editing_cost_item_attributes(cost, cost_item):
def enable_editing_cost_item_attributes(cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
cost.enable_editing_cost_item_attributes(cost_item)
cost.load_cost_item_attributes(cost_item)
def disable_editing_cost_item(cost):
def disable_editing_cost_item(cost: tool.Cost):
cost.disable_editing_cost_item()
def edit_cost_item(ifc, cost):
def edit_cost_item(ifc: tool.Ifc, cost: tool.Cost):
attributes = cost.get_cost_item_attributes()
ifc.run("cost.edit_cost_item", cost_item=cost.get_active_cost_item(), attributes=attributes)
cost.disable_editing_cost_item()
cost.load_cost_schedule_tree()
def assign_cost_item_type(ifc, cost, spatial, cost_item, prop_name):
def assign_cost_item_type(
ifc: tool.Ifc, cost: tool.Cost, spatial: tool.Spatial, cost_item: ifcopenshell.entity_instance, prop_name
):
product_types = spatial.get_selected_product_types()
[
ifc.run("control.assign_control", relating_control=cost_item, related_object=product_type)
@@ -91,7 +120,9 @@ def assign_cost_item_type(ifc, cost, spatial, cost_item, prop_name):
cost.load_cost_item_types(cost_item)
def unassign_cost_item_type(ifc, cost, spatial, cost_item, product_types):
def unassign_cost_item_type(
ifc: tool.Ifc, cost: tool.Cost, spatial: tool.Spatial, cost_item: ifcopenshell.entity_instance, product_types
):
if not product_types:
product_types = spatial.get_selected_product_types()
[
@@ -101,83 +132,85 @@ def unassign_cost_item_type(ifc, cost, spatial, cost_item, product_types):
cost.load_cost_item_types(cost_item)
def load_cost_item_types(cost):
def load_cost_item_types(cost: tool.Cost):
cost_item = cost.get_active_cost_item()
cost.load_cost_item_types(cost_item)
def assign_cost_item_quantity(ifc, cost, cost_item, related_object_type, prop_name):
def assign_cost_item_quantity(
ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance, related_object_type, prop_name
):
products = cost.get_products(related_object_type)
if products:
ifc.run("cost.assign_cost_item_quantity", cost_item=cost_item, products=products, prop_name=prop_name)
cost.load_cost_item_quantity_assignments(cost_item, related_object_type=related_object_type)
def load_cost_item_quantities(cost):
def load_cost_item_quantities(cost: tool.Cost):
cost.load_cost_item_quantities()
def load_cost_item_element_quantities(cost):
def load_cost_item_element_quantities(cost: tool.Cost):
cost_item = cost.get_highlighted_cost_item()
cost.load_cost_item_quantity_assignments(cost_item, related_object_type="PRODUCT")
def load_cost_item_task_quantities(cost):
def load_cost_item_task_quantities(cost: tool.Cost):
cost_item = cost.get_highlighted_cost_item()
cost.load_cost_item_quantity_assignments(cost_item, related_object_type="PROCESS")
def load_cost_item_resource_quantities(cost):
def load_cost_item_resource_quantities(cost: tool.Cost):
cost_item = cost.get_highlighted_cost_item()
cost.load_cost_item_quantity_assignments(cost_item, related_object_type="RESOURCE")
def assign_cost_value(ifc, cost_item, cost_rate):
def assign_cost_value(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, cost_rate):
ifc.run("cost.assign_cost_value", cost_item=cost_item, cost_rate=cost_rate)
def load_schedule_of_rates(cost, schedule_of_rates):
def load_schedule_of_rates(cost: tool.Cost, schedule_of_rates):
cost.load_schedule_of_rates_tree(schedule_of_rates)
def unassign_cost_item_quantity(ifc, cost, cost_item, products):
def unassign_cost_item_quantity(ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance, products):
ifc.run("cost.unassign_cost_item_quantity", cost_item=cost_item, products=products)
cost.load_cost_item_quantities()
def enable_editing_cost_item_quantities(cost, cost_item):
def enable_editing_cost_item_quantities(cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
cost.enable_editing_cost_item_quantities(cost_item)
def enable_editing_cost_item_values(cost, cost_item):
def enable_editing_cost_item_values(cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
cost.enable_editing_cost_item_values(cost_item)
def add_cost_item_quantity(ifc, cost_item, ifc_class):
def add_cost_item_quantity(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, ifc_class):
ifc.run("cost.add_cost_item_quantity", cost_item=cost_item, ifc_class=ifc_class)
def remove_cost_item_quantity(ifc, cost_item, physical_quantity):
def remove_cost_item_quantity(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, physical_quantity):
ifc.run("cost.remove_cost_item_quantity", cost_item=cost_item, physical_quantity=physical_quantity)
def enable_editing_cost_item_quantity(cost, physical_quantity):
def enable_editing_cost_item_quantity(cost: tool.Cost, physical_quantity):
cost.load_cost_item_quantity_attributes(physical_quantity)
cost.enable_editing_cost_item_quantity(physical_quantity)
def disable_editing_cost_item_quantity(cost):
def disable_editing_cost_item_quantity(cost: tool.Cost):
cost.disable_editing_cost_item_quantity()
def edit_cost_item_quantity(ifc, cost, physical_quantity):
def edit_cost_item_quantity(ifc: tool.Ifc, cost: tool.Cost, physical_quantity):
attributes = cost.get_cost_item_quantity_attributes()
ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes)
cost.disable_editing_cost_item_quantity()
cost.load_cost_item_quantities()
def add_cost_value(ifc, cost, parent, cost_type, cost_category):
def add_cost_value(ifc: tool.Ifc, cost: tool.Cost, parent, cost_type, cost_category):
value = ifc.run("cost.add_cost_value", parent=parent)
ifc.run(
"cost.edit_cost_value",
@@ -186,82 +219,84 @@ def add_cost_value(ifc, cost, parent, cost_type, cost_category):
)
def remove_cost_value(ifc, parent, cost_value):
def remove_cost_value(ifc: tool.Ifc, parent, cost_value):
ifc.run("cost.remove_cost_value", parent=parent, cost_value=cost_value)
def enable_editing_cost_item_value(cost, cost_value):
def enable_editing_cost_item_value(cost: tool.Cost, cost_value):
cost.load_cost_item_value_attributes(cost_value)
cost.enable_editing_cost_item_value(cost_value)
def disable_editing_cost_item_value(cost):
def disable_editing_cost_item_value(cost: tool.Cost):
cost.disable_editing_cost_item_value()
def enable_editing_cost_item_value_formula(cost, cost_value):
def enable_editing_cost_item_value_formula(cost: tool.Cost, cost_value):
cost.load_cost_item_value_formula_attributes(cost_value)
cost.enable_editing_cost_item_value_formula(cost_value)
def edit_cost_item_value_formula(ifc, cost, cost_value):
def edit_cost_item_value_formula(ifc: tool.Ifc, cost: tool.Cost, cost_value):
formula = cost.get_cost_item_value_formula()
ifc.run("cost.edit_cost_value_formula", cost_value=cost_value, formula=formula)
cost.disable_editing_cost_item_value()
def edit_cost_value(ifc, cost, cost_value):
def edit_cost_value(ifc: tool.Ifc, cost: tool.Cost, cost_value):
attributes = cost.get_cost_value_attributes()
ifc.run("cost.edit_cost_value", cost_value=cost_value, attributes=attributes)
cost.disable_editing_cost_item_value()
# cost.load_cost_item_values(cost.get_highlighted_cost_item())
def copy_cost_item_values(ifc, cost, source, destination):
def copy_cost_item_values(ifc: tool.Ifc, cost: tool.Cost, source, destination):
ifc.run("cost.copy_cost_item_values", source=source, destination=destination)
def select_cost_item_products(cost, spatial, cost_item):
def select_cost_item_products(cost: tool.Cost, spatial: tool.Spatial, cost_item: ifcopenshell.entity_instance):
is_deep = cost.show_nested_cost_item_elements()
products = cost.get_cost_item_products(cost_item, is_deep)
spatial.select_products(products)
def select_cost_schedule_products(cost, spatial, cost_schedule):
def select_cost_schedule_products(cost: tool.Cost, spatial: tool.Spatial, cost_schedule: ifcopenshell.entity_instance):
products = cost.get_cost_schedule_products(cost_schedule)
spatial.select_products(products)
def import_cost_schedule_csv(cost, file_path, is_schedule_of_rates):
def import_cost_schedule_csv(cost: tool.Cost, file_path, is_schedule_of_rates):
cost.import_cost_schedule_csv(file_path, is_schedule_of_rates)
def add_cost_column(cost, name):
def add_cost_column(cost: tool.Cost, name):
cost.add_cost_column(name)
def remove_cost_column(cost, name):
def remove_cost_column(cost: tool.Cost, name):
cost.remove_cost_column(name)
def expand_cost_item_rate(cost, cost_item):
def expand_cost_item_rate(cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
cost.expand_cost_item_rate(cost_item)
def contract_cost_item_rate(cost, cost_item):
def contract_cost_item_rate(cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
cost.contract_cost_item_rate(cost_item)
def calculate_cost_item_resource_value(ifc, cost_item):
def calculate_cost_item_resource_value(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance):
ifc.run("cost.calculate_cost_item_resource_value", cost_item=cost_item)
def export_cost_schedules(cost, filepath, format, cost_schedule=None):
def export_cost_schedules(cost: tool.Cost, filepath, format, cost_schedule=None):
cost.play_sound()
return cost.export_cost_schedules(filepath, format, cost_schedule)
def clear_cost_item_assignments(ifc, cost, cost_item, related_object_type):
def clear_cost_item_assignments(
ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance, related_object_type
):
products = cost.get_cost_item_assignments(cost_item, filter_by_type=related_object_type)
if products:
ifc.run("cost.unassign_cost_item_quantity", cost_item=cost_item, products=products)
@@ -269,7 +304,7 @@ def clear_cost_item_assignments(ifc, cost, cost_item, related_object_type):
cost.load_cost_schedule_tree()
def select_unassigned_products(ifc, cost, spatial):
def select_unassigned_products(ifc: tool.Ifc, cost: tool.Cost, spatial: tool.Spatial):
spatial.deselect_objects()
products = ifc.get().by_type("IfcElement")
cost_schedule = cost.get_active_cost_schedule()
@@ -277,11 +312,11 @@ def select_unassigned_products(ifc, cost, spatial):
spatial.select_products(selection)
def load_product_cost_items(cost, product):
def load_product_cost_items(cost: tool.Cost, product):
cost.load_product_cost_items(product)
def highlight_product_cost_item(spatial, cost, cost_item):
def highlight_product_cost_item(spatial: tool.Spatial, cost: tool.Cost, cost_item: ifcopenshell.entity_instance):
cost_schedule = cost.get_cost_schedule(cost_item)
is_cost_schedule_active = cost.is_cost_schedule_active(cost_schedule)
if is_cost_schedule_active:
@@ -290,7 +325,7 @@ def highlight_product_cost_item(spatial, cost, cost_item):
return "Cost schedule is not active"
def change_parent_cost_item(ifc, cost, new_parent):
def change_parent_cost_item(ifc: tool.Ifc, cost: tool.Cost, new_parent):
cost_item = cost.get_active_cost_item()
if cost_item and cost.is_root_cost_item(cost_item):
return "Cannot change root cost item"
@@ -300,7 +335,7 @@ def change_parent_cost_item(ifc, cost, new_parent):
cost.load_cost_schedule_tree()
def copy_cost_item(ifc, cost):
def copy_cost_item(ifc: tool.Ifc, cost: tool.Cost):
cost_item = cost.get_highlighted_cost_item()
if cost_item:
cost_item = ifc.run("cost.copy_cost_item", cost_item=cost_item)
@@ -308,7 +343,7 @@ def copy_cost_item(ifc, cost):
cost.load_cost_schedule_tree()
def add_currency(ifc, cost):
def add_currency(ifc: tool.Ifc, cost: tool.Cost):
unit = ifc.run("unit.add_monetary_unit")
attributes = cost.get_currency_attributes()
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
+71 -35
View File
@@ -16,36 +16,44 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from pathlib import Path
import ifcopenshell
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def enable_editing_text(drawing, obj=None):
def enable_editing_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.enable_editing_text(obj)
drawing.import_text_attributes(obj)
def disable_editing_text(drawing, obj=None):
def disable_editing_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.disable_editing_text(obj)
def edit_text(drawing, obj=None):
def edit_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.synchronise_ifc_and_text_attributes(obj)
drawing.update_text_size_pset(obj)
drawing.update_text_value(obj)
drawing.disable_editing_text(obj)
def enable_editing_assigned_product(drawing, obj=None):
def enable_editing_assigned_product(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.enable_editing_assigned_product(obj)
drawing.import_assigned_product(obj)
def disable_editing_assigned_product(drawing, obj=None):
def disable_editing_assigned_product(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.disable_editing_assigned_product(obj)
def edit_assigned_product(ifc, drawing, obj=None, product=None):
def edit_assigned_product(
ifc: tool.Ifc, drawing: tool.Drawing, obj: bpy.types.Object, product: Optional[ifcopenshell.entity_instance] = None
) -> None:
element = ifc.get_entity(obj)
existing_product = drawing.get_assigned_product(element)
if existing_product != product:
@@ -58,16 +66,16 @@ def edit_assigned_product(ifc, drawing, obj=None, product=None):
drawing.disable_editing_assigned_product(obj)
def load_sheets(drawing):
def load_sheets(drawing: tool.Drawing) -> None:
drawing.import_sheets()
drawing.enable_editing_sheets()
def disable_editing_sheets(drawing):
def disable_editing_sheets(drawing: tool.Drawing) -> None:
drawing.disable_editing_sheets()
def add_sheet(ifc, drawing, titleblock: ifcopenshell.entity_instance):
def add_sheet(ifc: tool.Ifc, drawing, titleblock: ifcopenshell.entity_instance) -> None:
sheet = ifc.run("document.add_information")
layout = ifc.run("document.add_reference", information=sheet)
titleblock_reference = ifc.run("document.add_reference", information=sheet)
@@ -93,7 +101,7 @@ def add_sheet(ifc, drawing, titleblock: ifcopenshell.entity_instance):
drawing.import_sheets()
def regenerate_sheet(drawing, sheet=None):
def regenerate_sheet(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
drawing.create_svg_sheet(sheet, drawing.sanitise_filename(Path(titleblock_uri).stem))
try:
@@ -104,11 +112,11 @@ def regenerate_sheet(drawing, sheet=None):
drawing.delete_file(path_layout)
def open_sheet(drawing, sheet=None):
def open_sheet(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
drawing.open_layout_svg(drawing.get_document_uri(sheet, "LAYOUT"))
def remove_sheet(ifc, drawing, sheet=None):
def remove_sheet(ifc: tool.Ifc, drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
for reference in drawing.get_document_references(sheet):
if drawing.get_reference_description(reference) in ("LAYOUT", "SHEET", "REVISION", "RASTER"):
uri = ifc.resolve_uri(drawing.get_document_uri(reference))
@@ -118,7 +126,7 @@ def remove_sheet(ifc, drawing, sheet=None):
drawing.import_sheets()
def rename_sheet(ifc, drawing, sheet: ifcopenshell.entity_instance, identification: str, name: str) -> None:
def rename_sheet(ifc: tool.Ifc, drawing, sheet: ifcopenshell.entity_instance, identification: str, name: str) -> None:
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": identification, "Name": name}
else:
@@ -144,30 +152,32 @@ def rename_sheet(ifc, drawing, sheet: ifcopenshell.entity_instance, identificati
drawing.move_file(old_location, ifc.resolve_uri(new_location))
def rename_reference(ifc, drawing, reference=None, identification=None):
def rename_reference(
ifc: tool.Ifc, drawing: tool.Drawing, reference: ifcopenshell.entity_instance, identification: str
) -> None:
attributes = drawing.generate_reference_attributes(reference, Identification=identification)
ifc.run("document.edit_reference", reference=reference, attributes=attributes)
def load_schedules(drawing):
def load_schedules(drawing: tool.Drawing) -> None:
drawing.import_documents("SCHEDULE")
drawing.enable_editing_schedules()
def load_references(drawing):
def load_references(drawing: tool.Drawing) -> None:
drawing.import_documents("REFERENCE")
drawing.enable_editing_references()
def disable_editing_schedules(drawing):
def disable_editing_schedules(drawing: tool.Drawing) -> None:
drawing.disable_editing_schedules()
def disable_editing_references(drawing):
def disable_editing_references(drawing: tool.Drawing) -> None:
drawing.disable_editing_references()
def add_document(ifc, drawing, document_type, uri=None):
def add_document(ifc: tool.Ifc, drawing: tool.Drawing, document_type: tool.Drawing.DOCUMENT_TYPE, uri: str) -> None:
document = ifc.run("document.add_information")
reference = ifc.run("document.add_reference", information=document)
name = drawing.get_path_filename(uri)
@@ -180,34 +190,43 @@ def add_document(ifc, drawing, document_type, uri=None):
drawing.import_documents(document_type)
def remove_document(ifc, drawing, document_type, document=None):
def remove_document(
ifc: tool.Ifc,
drawing: tool.Drawing,
document_type: tool.Drawing.DOCUMENT_TYPE,
document: ifcopenshell.entity_instance,
) -> None:
ifc.run("document.remove_information", information=document)
drawing.import_documents(document_type)
def open_schedule(drawing, schedule=None):
def open_schedule(drawing: tool.Drawing, schedule: ifcopenshell.entity_instance) -> None:
drawing.open_spreadsheet(drawing.get_document_uri(schedule))
def open_reference(drawing, reference=None):
def open_reference(drawing: tool.Drawing, reference: ifcopenshell.entity_instance) -> None:
drawing.open_svg(drawing.get_document_uri(reference))
def update_document_name(ifc, drawing, document=None, name=None):
def update_document_name(
ifc: tool.Ifc, drawing: tool.Drawing, document: ifcopenshell.entity_instance, name=None
) -> None:
if drawing.get_name(document) != name:
ifc.run("document.edit_information", information=document, attributes={"Name": name})
def load_drawings(drawing):
def load_drawings(drawing: tool.Drawing) -> None:
drawing.import_drawings()
drawing.enable_editing_drawings()
def disable_editing_drawings(drawing):
def disable_editing_drawings(drawing: tool.Drawing) -> None:
drawing.disable_editing_drawings()
def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
def add_drawing(
ifc: tool.Ifc, collector: tool.Collector, drawing: tool.Drawing, target_view=None, location_hint=None
) -> None:
drawing_name = drawing.ensure_unique_drawing_name(drawing.generate_drawing_name(target_view, location_hint))
drawing_matrix = drawing.generate_drawing_matrix(target_view, location_hint)
camera = drawing.create_camera(drawing_name, drawing_matrix, location_hint)
@@ -265,7 +284,12 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
drawing.import_drawings()
def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotations=False):
def duplicate_drawing(
ifc: tool.Ifc,
drawing_tool: tool.Drawing,
drawing: ifcopenshell.entity_instance,
should_duplicate_annotations: bool = False,
) -> ifcopenshell.entity_instance:
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
new_drawing = ifc.run("root.copy_class", product=drawing)
drawing_tool.copy_representation(drawing, new_drawing)
@@ -302,7 +326,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati
return new_drawing
def remove_drawing(ifc, drawing_tool, drawing=None):
def remove_drawing(ifc: tool.Ifc, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance) -> None:
if drawing_tool.is_active_drawing(drawing):
drawing_tool.run_drawing_activate_model()
@@ -330,7 +354,9 @@ def remove_drawing(ifc, drawing_tool, drawing=None):
drawing_tool.import_drawings()
def update_drawing_name(ifc, drawing_tool, drawing=None, name=None):
def update_drawing_name(
ifc: tool.Ifc, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance, name=None
) -> None:
if drawing_tool.get_name(drawing) != name:
ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name})
group = drawing_tool.get_drawing_group(drawing)
@@ -364,7 +390,13 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None):
drawing_tool.import_sheets()
def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None):
def add_annotation(
ifc: tool.Ifc,
collector: tool.Collector,
drawing_tool: tool.Drawing,
drawing: ifcopenshell.entity_instance,
object_type: str,
) -> None:
target_view = drawing_tool.get_drawing_target_view(drawing)
context = drawing_tool.get_annotation_context(target_view, object_type)
if not context:
@@ -387,12 +419,14 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
drawing_tool.enable_editing(obj)
def build_schedule(drawing, schedule=None):
def build_schedule(drawing: tool.Drawing, schedule: ifcopenshell.entity_instance) -> None:
drawing.create_svg_schedule(schedule)
drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg"))
def sync_references(ifc, collector, drawing_tool, drawing=None):
def sync_references(
ifc: tool.Ifc, collector: tool.Collector, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance
) -> None:
if not drawing_tool.has_annotation(drawing):
return
@@ -437,11 +471,13 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
drawing_tool.sync_object_representation(reference_obj)
def select_assigned_product(drawing, context):
def select_assigned_product(drawing: tool.Drawing, context: bpy.types.Context) -> None:
drawing.select_assigned_product(context)
def activate_drawing_view(ifc, blender, drawing_tool, drawing):
def activate_drawing_view(
ifc: tool.Ifc, blender: tool.Blender, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance
) -> None:
camera = ifc.get_object(drawing)
if not camera:
camera = drawing_tool.import_drawing(drawing)
+21 -13
View File
@@ -18,10 +18,11 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import blenderbim.core.tool as tool
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def edit_object_placement(
@@ -37,8 +38,15 @@ def edit_object_placement(
def add_representation(
ifc, geometry, style, surveyor, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None
):
ifc: tool.Ifc,
geometry: tool.Geometry,
style: tool.Style,
surveyor: tool.Surveyor,
obj: bpy.types.Object,
context: ifcopenshell.entity_instance,
ifc_representation_class: Optional[str] = None,
profile_set_usage: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
element = ifc.get_entity(obj)
if not element:
return
@@ -47,7 +55,7 @@ def add_representation(
data = geometry.get_object_data(obj)
if not data and ifc_representation_class != "IfcTextLiteral":
raise IncompatibleRepresentationError()
return
representation = ifc.run(
"geometry.add_representation",
@@ -89,15 +97,15 @@ def add_representation(
def switch_representation(
ifc,
geometry,
obj=None,
representation=None,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
):
ifc: tool.Ifc,
geometry: tool.Geometry,
obj: bpy.types.Object,
representation: ifcopenshell.entity_instance,
should_reload: bool = True,
is_global: bool = True,
should_sync_changes_first: bool = False,
apply_openings: bool = True,
) -> None:
"""Function can switch to representation that wasn't yet assigned to that object. See #2766.
`should_sync_changes_first` - sync ifc representation with current state of `obj.data`;
@@ -85,6 +85,7 @@ def enable_editing_material(material_tool, material):
def edit_material(ifc, material_tool, material):
attributes = material_tool.get_material_attributes()
ifc.run("material.edit_material", material=material, attributes=attributes)
material_tool.sync_blender_material_name(material)
material_tool.disable_editing_material()
material_type = material_tool.get_active_material_type()
material_tool.import_material_definitions(material_type)
+21 -11
View File
@@ -16,8 +16,18 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
def copy_class(ifc, collector, geometry, root, obj=None):
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def copy_class(
ifc: tool.Ifc, collector: tool.Collector, geometry: tool.Geometry, root: tool.Root, obj: bpy.types.Object
) -> ifcopenshell.entity_instance:
element = ifc.get_entity(obj)
if not element:
return
@@ -48,16 +58,16 @@ def copy_class(ifc, collector, geometry, root, obj=None):
def assign_class(
ifc,
collector,
root,
obj=None,
ifc_class=None,
predefined_type=None,
should_add_representation=True,
context=None,
ifc_representation_class=None,
):
ifc: tool.Ifc,
collector: tool.Collector,
root: tool.Root,
obj: bpy.types.Object,
ifc_class: str,
context: ifcopenshell.entity_instance,
predefined_type: Optional[str] = None,
should_add_representation: bool = True,
ifc_representation_class: Optional[str] = None,
) -> ifcopenshell.entity_instance:
if ifc.get_entity(obj):
return
+1 -1
View File
@@ -18,11 +18,11 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union
import blenderbim.core.tool as tool
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def reference_structure(
+3 -2
View File
@@ -487,12 +487,12 @@ class Material:
def disable_editing_materials(cls): pass
def enable_editing_material(cls, material): pass
def enable_editing_materials(cls): pass
def get_active_material_type(cls): pass
def get_active_material(cls): pass
def get_active_material_type(cls): pass
def get_active_object_material(cls, obj): pass
def get_elements_by_material(cls, material): pass
def get_material_attributes(cls): pass
def get_material(cls, element, should_inherit): pass
def get_material_attributes(cls): pass
def get_name(cls, obj): pass
def get_type(cls, element): pass
def has_material_profile(cls, element): pass
@@ -503,6 +503,7 @@ class Material:
def is_material_used_in_sets(cls, material): pass
def load_material_attributes(cls, material): pass
def replace_material_with_material_profile(cls, element): pass
def sync_blender_material_name(cls, material): pass
@interface
+7 -2
View File
@@ -468,15 +468,20 @@ class Blender(blenderbim.core.tool.Blender):
active_object.select_set(True)
@classmethod
def enum_property_has_valid_index(cls, props, prop_name: str, enum_items: tuple) -> bool:
def enum_property_has_valid_index(cls, props: bpy.types.PropertyGroup, prop_name: str, enum_items: tuple) -> bool:
"""method created for readibility and to avoid console warnings like
`pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'`
"""
items_amount = len(enum_items)
# If enum has no items it seems to always produce a warning.
# E.g. if you try to get it's value directly: `BIMModelProperties.relating_type_id`.
if items_amount == 0:
return False
current_value_index = props.get(prop_name, None)
# assuming the default value is fine
if current_value_index is None:
return True
return current_value_index < len(enum_items)
return current_value_index < items_amount
@classmethod
def append_data_block(cls, filepath: str, data_block_type: str, name: str, link=False, relative=False) -> dict:
+41 -20
View File
@@ -1,11 +1,13 @@
import os
import bpy
import blenderbim.core.tool
import blenderbim.tool as tool
import ifcopenshell.util.date
import ifcopenshell.util.cost
import ifcopenshell.util.unit
import blenderbim.bim.helper
import json
from typing import Optional, Any, Generator
class Cost(blenderbim.core.tool.Cost):
@@ -36,7 +38,7 @@ class Cost(blenderbim.core.tool.Cost):
blenderbim.bim.helper.import_attributes2(cost_schedule, props.cost_schedule_attributes, callback=special_import)
@classmethod
def enable_editing_cost_items(cls, cost_schedule):
def enable_editing_cost_items(cls, cost_schedule: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_schedule_id = cost_schedule.id()
props.is_editing = "COST_ITEMS"
@@ -163,7 +165,7 @@ class Cost(blenderbim.core.tool.Cost):
return
@classmethod
def load_cost_item_types(cls, cost_item=None):
def load_cost_item_types(cls, cost_item: Optional[ifcopenshell.entity_instance] = None) -> None:
if not cost_item:
cost_item = cls.get_highlighted_cost_item()
if not cost_item:
@@ -259,7 +261,7 @@ class Cost(blenderbim.core.tool.Cost):
blenderbim.bim.helper.import_attributes2(physical_quantity, props.quantity_attributes)
@classmethod
def enable_editing_cost_item_values(cls, cost_item=None):
def enable_editing_cost_item_values(cls, cost_item: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_item_id = cost_item.id()
props.cost_item_editing_type = "VALUES"
@@ -287,7 +289,7 @@ class Cost(blenderbim.core.tool.Cost):
return attributes
@classmethod
def load_cost_item_value_attributes(cls, cost_value=None):
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):
if name == "AppliedValue":
# TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure)
@@ -334,36 +336,38 @@ class Cost(blenderbim.core.tool.Cost):
blenderbim.bim.helper.import_attributes2(cost_value, props.cost_value_attributes, callback=callback)
@classmethod
def calculate_applied_value(cls, cost_item, cost_value):
def calculate_applied_value(
cls, cost_item: ifcopenshell.entity_instance, cost_value: ifcopenshell.entity_instance
) -> float:
return ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value)
@classmethod
def is_active_schedule_of_rates(cls):
def is_active_schedule_of_rates(cls) -> bool:
return (
tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id).PredefinedType
== "SCHEDULEOFRATES"
)
@classmethod
def enable_editing_cost_item_value(cls, cost_value=None):
def enable_editing_cost_item_value(cls, cost_value: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_value_id = cost_value.id()
props.cost_value_editing_type = "ATTRIBUTES"
@classmethod
def disable_editing_cost_item_value(cls):
def disable_editing_cost_item_value(cls) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_value_id = 0
props.cost_value_editing_type = ""
@classmethod
def load_cost_item_value_formula_attributes(cls, cost_value=None):
def load_cost_item_value_formula_attributes(cls, cost_value: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.cost_value_attributes.clear()
bpy.context.scene.BIMCostProperties.cost_value_formula = ifcopenshell.util.cost.serialise_cost_value(cost_value)
@classmethod
def enable_editing_cost_item_value_formula(cls, cost_value=None):
def enable_editing_cost_item_value_formula(cls, cost_value: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_value_id = cost_value.id()
props.cost_value_editing_type = "FORMULA"
@@ -373,7 +377,7 @@ class Cost(blenderbim.core.tool.Cost):
return bpy.context.scene.BIMCostProperties.cost_value_formula
@classmethod
def get_cost_value_attributes(cls):
def get_cost_value_attributes(cls) -> dict[str, Any]:
def export_attributes(attributes, prop):
if prop.name == "UnitBasisValue":
if prop.is_null:
@@ -392,13 +396,18 @@ class Cost(blenderbim.core.tool.Cost):
return blenderbim.bim.helper.export_attributes(props.cost_value_attributes, callback)
@classmethod
def get_cost_value_unit_component(cls):
def get_cost_value_unit_component(cls) -> ifcopenshell.entity_instance:
return tool.Ifc.get().by_id(
int(bpy.context.scene.BIMCostProperties.cost_value_attributes.get("UnitBasisUnit").enum_value)
)
@classmethod
def get_cost_item_assignments(cls, cost_item, filter_by_type=None, is_deep=False):
def get_cost_item_assignments(
cls,
cost_item: ifcopenshell.entity_instance,
filter_by_type: Optional[ifcopenshell.util.cost.FILTER_BY_TYPE] = None,
is_deep: bool = False,
) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.cost.get_cost_item_assignments(
cost_item, filter_by_type=filter_by_type, is_deep=is_deep
)
@@ -408,30 +417,40 @@ class Cost(blenderbim.core.tool.Cost):
return bpy.context.scene.BIMCostProperties.show_nested_elements
@classmethod
def get_cost_item_products(cls, cost_item, is_deep=False):
def get_cost_item_products(
cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False
) -> list[ifcopenshell.entity_instance]:
return cls.get_cost_item_assignments(cost_item, filter_by_type="PRODUCT", is_deep=is_deep)
@classmethod
def get_cost_item_resources(cls, cost_item, is_deep=False):
def get_cost_item_resources(
cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False
) -> list[ifcopenshell.entity_instance]:
return cls.get_cost_item_assignments(cost_item, filter_by_type="RESOURCE", is_deep=is_deep)
@classmethod
def get_cost_item_processes(cls, cost_item, is_deep=False):
def get_cost_item_processes(
cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False
) -> list[ifcopenshell.entity_instance]:
return cls.get_cost_item_assignments(cost_item, filter_by_type="PROCESS", is_deep=is_deep)
@classmethod
def get_schedule_cost_items(cls, cost_schedule):
def get_schedule_cost_items(
cls, cost_schedule: ifcopenshell.entity_instance
) -> Generator[ifcopenshell.entity_instance, None, None]:
return ifcopenshell.util.cost.get_schedule_cost_items(cost_schedule)
@classmethod
def get_cost_schedule_products(cls, cost_schedule):
def get_cost_schedule_products(
cls, cost_schedule: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
products = []
for cost_item in ifcopenshell.util.cost.get_schedule_cost_items(cost_schedule):
products.extend(cls.get_cost_item_products(cost_item))
return products
@classmethod
def import_cost_schedule_csv(cls, file_path=None, is_schedule_of_rates=False):
def import_cost_schedule_csv(cls, file_path: Optional[str] = None, is_schedule_of_rates: bool = False) -> None:
if not file_path:
return
from ifc5d.csv2ifc import Csv2Ifc
@@ -473,7 +492,9 @@ class Cost(blenderbim.core.tool.Cost):
cls.load_schedule_of_rates_tree(schedule_of_rates=tool.Ifc.get().by_id(int(props.schedule_of_rates)))
@classmethod
def create_new_cost_item_li(cls, props_collection, cost_item, level_index, type="cost_rate"):
def create_new_cost_item_li(
cls, props_collection, cost_item: ifcopenshell.entity_instance, level_index: int, type: str = "cost_rate"
) -> None:
new = props_collection.add()
new.ifc_definition_id = cost_item.id()
new.name = cost_item.Name or "Unnamed"
+37 -55
View File
@@ -51,11 +51,14 @@ from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT
from lxml import etree
from mathutils import Vector, Matrix
from fractions import Fraction
from typing import Optional, Union, Iterable, Any
from typing import Optional, Union, Iterable, Any, Literal
from pathlib import Path
class Drawing(blenderbim.core.tool.Drawing):
ANNOTATION_DATA_TYPE = Literal["empty", "curve", "mesh"]
DOCUMENT_TYPE = Literal["SCHEDULE", "REFERENCE"]
@classmethod
def canonicalise_class_name(cls, name):
return re.sub("[^0-9a-zA-Z]+", "", name)
@@ -68,11 +71,11 @@ class Drawing(blenderbim.core.tool.Drawing):
)
@classmethod
def get_annotation_data_type(cls, object_type):
def get_annotation_data_type(cls, object_type: str) -> ANNOTATION_DATA_TYPE:
return ANNOTATION_TYPES_DATA[object_type][3]
@classmethod
def create_annotation_object(cls, drawing, object_type):
def create_annotation_object(cls, drawing: ifcopenshell.entity_instance, object_type: str) -> bpy.types.Object:
data_type = cls.get_annotation_data_type(object_type)
obj = annotation.Annotator.get_annotation_obj(drawing, object_type, data_type)
if object_type == "FILL_AREA":
@@ -298,15 +301,15 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.context.scene.DocProperties.is_editing_sheets = False
@classmethod
def disable_editing_text(cls, obj):
def disable_editing_text(cls, obj: bpy.types.Object) -> None:
obj.BIMTextProperties.is_editing = False
@classmethod
def disable_editing_assigned_product(cls, obj):
def disable_editing_assigned_product(cls, obj: bpy.types.Object) -> None:
obj.BIMAssignedProductProperties.is_editing_product = False
@classmethod
def enable_editing(cls, obj):
def enable_editing(cls, obj: bpy.types.Object) -> None:
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
@@ -330,11 +333,11 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.context.scene.DocProperties.is_editing_sheets = True
@classmethod
def enable_editing_text(cls, obj):
def enable_editing_text(cls, obj: bpy.types.Object) -> None:
obj.BIMTextProperties.is_editing = True
@classmethod
def enable_editing_assigned_product(cls, obj):
def enable_editing_assigned_product(cls, obj: bpy.types.Object) -> None:
obj.BIMAssignedProductProperties.is_editing_product = True
@classmethod
@@ -428,7 +431,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return location
@classmethod
def get_path_filename(cls, path):
def get_path_filename(cls, path: str) -> str:
return os.path.splitext(os.path.basename(path))[0]
@classmethod
@@ -483,7 +486,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return ""
@classmethod
def get_name(cls, element):
def get_name(cls, element: ifcopenshell.entity_instance) -> Union[str, None]:
return element.Name
@classmethod
@@ -563,7 +566,7 @@ class Drawing(blenderbim.core.tool.Drawing):
ifcopenshell.util.element.remove_deep2(ifc_file, literal)
@classmethod
def synchronise_ifc_and_text_attributes(cls, obj):
def synchronise_ifc_and_text_attributes(cls, obj: bpy.types.Object) -> None:
literals = cls.get_text_literal(obj, return_list=True)
literals_attributes = cls.export_text_literal_attributes(obj)
defined_ifc_ids = [l.ifc_definition_id for l in obj.BIMTextProperties.literals]
@@ -784,7 +787,7 @@ class Drawing(blenderbim.core.tool.Drawing):
new.ifc_definition_id = drawing.id() # Last, to prevent unnecessary prop callbacks
@classmethod
def import_documents(cls, document_type):
def import_documents(cls, document_type: DOCUMENT_TYPE) -> None:
dprops = bpy.context.scene.DocProperties
if document_type == "SCHEDULE":
documents_collection = dprops.schedules
@@ -845,7 +848,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return next(s for s in props.sheets[: props.active_sheet_index + 1][::-1] if s.is_sheet)
@classmethod
def import_text_attributes(cls, obj):
def import_text_attributes(cls, obj: bpy.types.Object) -> None:
props = obj.BIMTextProperties
props.literals.clear()
@@ -863,7 +866,7 @@ class Drawing(blenderbim.core.tool.Drawing):
props.font_size = str(text_data["FontSize"])
@classmethod
def import_assigned_product(cls, obj):
def import_assigned_product(cls, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
product = cls.get_assigned_product(element)
if product:
@@ -934,38 +937,16 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.context.scene.DocProperties.should_draw_decorations = True
@classmethod
def update_text_value(cls, obj):
element = tool.Ifc.get_entity(obj)
if element.is_a("IfcTypeProduct"):
objs = [obj]
for occurrence in ifcopenshell.util.element.get_types(element):
obj = tool.Ifc.get_object(occurrence)
if obj:
objs.append(obj)
else:
objs = []
element_type = ifcopenshell.util.element.get_type(element)
if element_type and element_type.RepresentationMaps:
obj = tool.Ifc.get_object(element_type)
if obj:
objs.append(obj)
for occurrence in ifcopenshell.util.element.get_types(element_type):
obj = tool.Ifc.get_object(occurrence)
if obj:
objs.append(obj)
else:
objs = [obj]
for obj in objs:
props = obj.BIMTextProperties
literals = cls.get_text_literal(obj, return_list=True)
cls.import_text_attributes(obj)
for i, literal in enumerate(literals):
product = cls.get_assigned_product(tool.Ifc.get_entity(obj)) or tool.Ifc.get_entity(obj)
props.literals[i].value = cls.replace_text_literal_variables(literal.Literal, product)
def update_text_value(cls, obj: bpy.types.Object) -> None:
props = obj.BIMTextProperties
literals = cls.get_text_literal(obj, return_list=True)
cls.import_text_attributes(obj)
for i, literal in enumerate(literals):
product = cls.get_assigned_product(tool.Ifc.get_entity(obj)) or tool.Ifc.get_entity(obj)
props.literals[i].value = cls.replace_text_literal_variables(literal.Literal, product)
@classmethod
def update_text_size_pset(cls, obj):
def update_text_size_pset(cls, obj: bpy.types.Object) -> None:
"""updates pset `EPset_Annotation.Classes` value
based on current font size from `obj.BIMTextProperties.font_size`
"""
@@ -1544,7 +1525,7 @@ class Drawing(blenderbim.core.tool.Drawing):
tool.Geometry.record_object_position(obj)
@classmethod
def get_document_references(cls, document):
def get_document_references(cls, document: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
if tool.Ifc.get_schema() == "IFC2X3":
return document.DocumentReferences or []
return document.HasDocumentReferences or []
@@ -1575,7 +1556,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return reference.Description
@classmethod
def generate_reference_attributes(cls, reference: ifcopenshell.entity_instance, **attributes: Any) -> dict[str, Any]:
def generate_reference_attributes(
cls, reference: ifcopenshell.entity_instance, **attributes: Any
) -> dict[str, Any]:
"""will automatically convert attributes below for IFC2X3 compatibility:
- Identification -> ItemReference
@@ -1657,23 +1640,22 @@ class Drawing(blenderbim.core.tool.Drawing):
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
updated_set = set()
for i in elements:
# exclude annotations to avoid including annotations from other drawings
if not i.is_a("IfcAnnotation"):
if not i.is_a("IfcAnnotation"):
updated_set.add(i)
#add aggregate too, if element is host by one
# add aggregate too, if element is host by one
if i.Decomposes:
aggregate = i.Decomposes[0].RelatingObject
#remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
if not aggregate.is_a("IfcProject"):
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
if not aggregate.is_a("IfcProject"):
updated_set.add(aggregate)
# After the iteration is complete, update elements with updated set
# After the iteration is complete, update elements with updated set
elements.update(updated_set)
# add annotations from the current drawing
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
elements.update(annotations)
@@ -1716,7 +1698,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return reference.ReferencedDocument
@classmethod
def select_assigned_product(cls, context):
def select_assigned_product(cls, context: bpy.types.Context) -> None:
obj = context.active_object
element = tool.Ifc.get_entity(obj)
product = cls.get_assigned_product(element)
@@ -1735,7 +1717,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return True if (camera and camera.data.type == "ORTHO") else False
@classmethod
def is_active_drawing(cls, drawing):
def is_active_drawing(cls, drawing: ifcopenshell.entity_instance) -> bool:
return drawing.id() == bpy.context.scene.DocProperties.active_drawing_id
@classmethod
+21 -8
View File
@@ -24,8 +24,10 @@ import logging
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.geom
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.system
import blenderbim.core.tool
import blenderbim.core.drawing
@@ -88,7 +90,7 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.data.meshes.remove(data)
@classmethod
def delete_ifc_object(cls, obj):
def delete_ifc_object(cls, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
if not element:
return
@@ -319,7 +321,7 @@ class Geometry(blenderbim.core.tool.Geometry):
return new_mesh
@classmethod
def get_active_representation(cls, obj):
def get_active_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
"""< IfcShapeRepresentation or None"""
if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
@@ -459,7 +461,9 @@ class Geometry(blenderbim.core.tool.Geometry):
return f"{representation.ContextOfItems.id()}/{representation.id()}"
@classmethod
def get_styles(cls, obj, only_assigned_to_faces=False):
def get_styles(
cls, obj: bpy.types.Object, only_assigned_to_faces: bool = False
) -> list[Union[ifcopenshell.entity_instance, None]]:
styles = [tool.Style.get_style(s.material) for s in obj.material_slots if s.material]
if not only_assigned_to_faces:
return styles
@@ -467,8 +471,15 @@ class Geometry(blenderbim.core.tool.Geometry):
usage_count = [0] * len(obj.material_slots)
if not usage_count: # if there are no materials, polygons will still use index 0
return []
for poly in obj.data.polygons:
usage_count[poly.material_index] += 1
# remove usages for empty material slots
for i, slot in reversed(list(enumerate(obj.material_slots))):
if not slot.material:
del usage_count[i]
styles = [style for style, usage in zip(styles, usage_count, strict=True) if usage > 0]
return styles
@@ -560,11 +571,11 @@ class Geometry(blenderbim.core.tool.Geometry):
new.value = element[i]
@classmethod
def is_body_representation(cls, representation):
def is_body_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
return representation.ContextOfItems.ContextIdentifier == "Body"
@classmethod
def is_box_representation(cls, representation):
def is_box_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
return representation.ContextOfItems.ContextIdentifier == "Box"
@classmethod
@@ -572,11 +583,11 @@ class Geometry(blenderbim.core.tool.Geometry):
return not all([tool.Cad.is_x(o, 1.0) for o in obj.scale]) or obj in IfcStore.edited_objs
@classmethod
def is_mapped_representation(cls, representation):
def is_mapped_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
return representation.RepresentationType == "MappedRepresentation"
@classmethod
def is_meshlike(cls, representation):
def is_meshlike(cls, representation: ifcopenshell.entity_instance) -> bool:
if ifcopenshell.util.representation.resolve_representation(representation).RepresentationType in (
"AdvancedBrep",
"Annotation2D",
@@ -656,7 +667,9 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.data.objects.remove(obj)
@classmethod
def resolve_mapped_representation(cls, representation):
def resolve_mapped_representation(
cls, representation: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
if representation.RepresentationType == "MappedRepresentation":
return cls.resolve_mapped_representation(representation.Items[0].MappingSource.MappedRepresentation)
return representation
@@ -242,3 +242,16 @@ class Material(blenderbim.core.tool.Material):
meshes_to_objects[mesh] = obj
for obj in meshes_to_objects.values():
tool.Geometry.reload_representation(obj)
@classmethod
def sync_blender_material_name(cls, material):
name = material.Name or "Unnamed"
obj = tool.Ifc.get_object(material)
if obj:
obj.name = name
style = tool.Style.get_style(obj)
if style:
style.Name = name
obj = tool.Ifc.get_object(style)
if obj:
obj.name = name
+3 -1
View File
@@ -97,7 +97,9 @@ class Misc(blenderbim.core.tool.Misc):
IfcStore.edited_objs.add(obj)
@classmethod
def split_objects_with_cutter(cls, objs, cutter):
def split_objects_with_cutter(
cls, objs: list[bpy.types.Object], cutter: bpy.types.Object
) -> list[bpy.types.Object]:
cutter_mesh = cutter.data
bm = bmesh.new()
+3 -2
View File
@@ -23,6 +23,7 @@ import collections
import collections.abc
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
import ifcopenshell.util.placement
import ifcopenshell.util.representation
@@ -468,7 +469,7 @@ class Model(blenderbim.core.tool.Model):
has_deleted_opening = True
@classmethod
def get_material_layer_parameters(cls, element):
def get_material_layer_parameters(cls, element: ifcopenshell.entity_instance) -> dict[str, Any]:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layer_set_direction = "AXIS2"
offset = 0.0
@@ -551,7 +552,7 @@ class Model(blenderbim.core.tool.Model):
data = tool.Ifc.get().createIfcText(json.dumps(data))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
else:
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
@classmethod
def get_flow_segment_axis(cls, obj):
@@ -21,6 +21,7 @@ import bpy
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.core.tool
import blenderbim.core.root
import blenderbim.bim.schema
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
+8 -3
View File
@@ -29,6 +29,7 @@ import blenderbim.core.style
import blenderbim.tool as tool
from mathutils import Vector
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
from typing import Union, Optional
class Root(blenderbim.core.tool.Root):
@@ -129,7 +130,7 @@ class Root(blenderbim.core.tool.Root):
return ifcopenshell.util.representation.get_representation(element, context=context.ContextType)
@classmethod
def get_element_type(cls, element):
def get_element_type(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_type(element)
@classmethod
@@ -282,8 +283,12 @@ class Root(blenderbim.core.tool.Root):
@classmethod
def run_geometry_add_representation(
cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None
):
cls,
obj: bpy.types.Object,
context: ifcopenshell.entity_instance,
ifc_representation_class: Optional[str] = None,
profile_set_usage: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
return blenderbim.core.geometry.add_representation(
tool.Ifc,
tool.Geometry,
+1 -1
View File
@@ -1630,7 +1630,7 @@ class Sequence(blenderbim.core.tool.Sequence):
group.Description = json.dumps(description)
else:
description = json.dumps({"type": "BBIM_AnimationColorScheme", "colourscheme": colour_scheme})
group = tool.Ifc.run("group.add_group", Name=name, Description=description)
group = tool.Ifc.run("group.add_group", name=name, description=description)
return group[0]
@classmethod
+3 -2
View File
@@ -31,6 +31,7 @@ import json
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon, MultiPolygon
from typing import Generator
class Spatial(blenderbim.core.tool.Spatial):
@@ -187,14 +188,14 @@ class Spatial(blenderbim.core.tool.Spatial):
]
@classmethod
def get_selected_products(cls):
def get_selected_products(cls) -> Generator[ifcopenshell.entity_instance, None, None]:
for obj in bpy.context.selected_objects:
entity = tool.Ifc.get_entity(obj)
if entity and entity.is_a("IfcProduct"):
yield entity
@classmethod
def get_selected_product_types(cls):
def get_selected_product_types(cls) -> Generator[ifcopenshell.entity_instance, None, None]:
for obj in bpy.context.selected_objects:
entity = tool.Ifc.get_entity(obj)
if entity and entity.is_a("IfcTypeProduct"):
@@ -105,6 +105,7 @@ For Linux or Mac:
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/bcf
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
@@ -117,6 +118,7 @@ For Linux or Mac:
$ ln -s $PWD/src/ifccsv/ifccsv.py $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
$ ln -s $PWD/src/ifcdiff/ifcdiff.py $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
$ ln -s $PWD/src/bsdd/bsdd.py $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
$ ln -s $PWD/src/bcf/src/bcf $BLENDER_ADDON_PATH/libs/site/packages/bcf
$ ln -s $PWD/src/ifc4d/ifc4d $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
$ ln -s $PWD/src/ifc5d/ifc5d $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
$ ln -s $PWD/src/ifccityjson/ifccityjson $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
@@ -172,6 +174,7 @@ Before running it follow the instructions descibed after `rem` tags.
del "%blenderbim%\libs\site\packages\ifccsv.py"
del "%blenderbim%\libs\site\packages\ifcdiff.py"
del "%blenderbim%\libs\site\packages\bsdd.py"
rd /S /Q "%blenderbim%\libs\site\packages\bcf"
rd /S /Q "%blenderbim%\libs\site\packages\ifc4d"
rd /S /Q "%blenderbim%\libs\site\packages\ifc5d"
rd /S /Q "%blenderbim%\libs\site\packages\ifccityjson"
@@ -184,6 +187,7 @@ Before running it follow the instructions descibed after `rem` tags.
mklink "%blenderbim%\libs\site\packages\ifccsv.py" "%cd%\src\ifccsv\ifccsv.py"
mklink "%blenderbim%\libs\site\packages\ifcdiff.py" "%cd%\src\ifcdiff\ifcdiff.py"
mklink "%blenderbim%\libs\site\packages\bsdd.py" "%cd%\src\bsdd\bsdd.py"
mklink /D "%blenderbim%\libs\site\packages\bcf" "%cd%\src\bcf\src\bcf"
mklink /D "%blenderbim%\libs\site\packages\ifc4d" "%cd%\src\ifc4d\ifc4d"
mklink /D "%blenderbim%\libs\site\packages\ifc5d" "%cd%\src\ifc5d\ifc5d"
mklink /D "%blenderbim%\libs\site\packages\ifccityjson" "%cd%\src\ifccityjson\ifccityjson"
+1 -1
View File
@@ -42,7 +42,7 @@ Installation
You do not need to unzip the add-on file. You should install it as a zipped file.
You should now see **Import-Export: BlenderBIM** available in your add-ons list. Enable the add-on by pressing the checkbox.
You should now see **System: BlenderBIM** available in your add-ons list. Enable the add-on by pressing the checkbox.
.. image:: images/install-blenderbim-3.png
+1 -1
View File
@@ -960,7 +960,7 @@ def run_test_code():
def saving_sample_test_files(and_open_in_blender=None):
filepath = f"{variables['cwd']}/test/files/temp/sample_test_file"
blend_filepath = f"{filepath}.blend"
bpy.ops.export_ifc.bim(filepath=f"{filepath}.ifc", should_save_as=True)
bpy.ops.bim.export_ifc(filepath=f"{filepath}.ifc", should_save_as=True)
bpy.ops.wm.save_as_mainfile(filepath=f"{filepath}.blend")
@@ -19,6 +19,7 @@
import bpy
import math
import ifcopenshell
import ifcopenshell.api
import blenderbim.core.tool
import blenderbim.tool as tool
from mathutils import Vector
+33 -15
View File
@@ -23,22 +23,25 @@ import ifcopenshell.util.unit
import ifcopenshell.util.selector
import ifcopenshell.util.element
import locale
from typing import Any, Optional
CostItem = dict[str, Any]
class Csv2Ifc:
def __init__(self):
self.csv = None
self.file = None
self.cost_items = []
self.cost_schedule = None
self.is_schedule_of_rates = False
self.units = {}
self.csv: str = None
self.file: ifcopenshell.file = None
self.cost_items: list[CostItem] = []
self.cost_schedule: ifcopenshell.entity_instance = None
self.is_schedule_of_rates: bool = False
self.units: dict[str, ifcopenshell.entity_instance] = {}
def execute(self):
def execute(self) -> None:
self.parse_csv()
self.create_ifc()
def parse_csv(self):
def parse_csv(self) -> None:
self.parents = {}
self.headers = {}
locale.setlocale(locale.LC_ALL, "") # set the system locale
@@ -47,6 +50,7 @@ class Csv2Ifc:
for row in reader:
if not row[0]:
continue
# parse header
if row[0] == "Hierarchy":
self.has_categories = True
for i, col in enumerate(row):
@@ -55,6 +59,17 @@ class Csv2Ifc:
if col == "Value":
self.has_categories = False
self.headers[col] = i
# validate header
mandatory_fields = {"Name", "Quantity", "Unit"}
if not self.is_schedule_of_rates:
mandatory_fields.update({"Property", "Query"})
available_fields = set(self.headers.keys())
if not mandatory_fields.issubset(available_fields):
raise Exception(
f"Missing mandatory fields in CSV header: {', '.join(mandatory_fields-available_fields)}"
)
continue
cost_data = self.get_row_cost_data(row)
hierarchy_key = int(row[0])
@@ -64,7 +79,7 @@ class Csv2Ifc:
self.parents[hierarchy_key - 1]["children"].append(cost_data)
self.parents[hierarchy_key] = cost_data
def get_row_cost_data(self, row):
def get_row_cost_data(self, row: list[str]) -> CostItem:
name = row[self.headers["Name"]]
identification = row[self.headers["Identification"]] if "Identification" in self.headers else None
quantity = row[self.headers["Quantity"]]
@@ -99,7 +114,7 @@ class Csv2Ifc:
"children": [],
}
def create_ifc(self):
def create_ifc(self) -> None:
if not self.file:
self.create_boilerplate_ifc()
if not self.cost_schedule:
@@ -108,11 +123,13 @@ class Csv2Ifc:
self.cost_schedule.PredefinedType = "SCHEDULEOFRATES"
self.create_cost_items(self.cost_items)
def create_cost_items(self, cost_items, parent=None):
def create_cost_items(
self, cost_items: list[CostItem], parent: Optional[ifcopenshell.entity_instance] = None
) -> None:
for cost_item in cost_items:
self.create_cost_item(cost_item, parent)
def create_cost_item(self, cost_item, parent):
def create_cost_item(self, cost_item: CostItem, parent: Optional[ifcopenshell.entity_instance] = None) -> None:
if parent is None:
cost_item["ifc"] = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=self.cost_schedule)
else:
@@ -161,6 +178,7 @@ class Csv2Ifc:
quantity = ifcopenshell.api.run(
"cost.add_cost_item_quantity", self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class
)
# 3 IfcPhysicalSimpleQuantity Value
quantity[3] = cost_item["Quantity"]
if cost_item["assignments"]["Query"]:
@@ -184,7 +202,7 @@ class Csv2Ifc:
self.create_cost_items(cost_item["children"], cost_item["ifc"])
def create_unit(self, symbol):
def create_unit(self, symbol) -> ifcopenshell.entity_instance:
unit = self.units.get(symbol, None)
if unit:
return unit
@@ -194,12 +212,12 @@ class Csv2Ifc:
self.units[symbol] = unit
return unit
def create_boilerplate_ifc(self):
def create_boilerplate_ifc(self) -> None:
self.file = ifcopenshell.file(schema="IFC4")
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
def has_property(self, product, property_name):
def has_property(self, product, property_name) -> bool:
if not property_name:
return True
qtos = ifcopenshell.util.element.get_psets(product, qtos_only=True)
@@ -59,7 +59,6 @@
#include <GeomAPI_ProjectPointOnSurf.hxx>
#include <Geom_Plane.hxx>
#include <IntTools_FaceFace.hxx>
#include <STEPConstruct_PointHasher.hxx>
#include "clash_utils.h"
#ifdef WITH_HDF5
@@ -67,6 +67,20 @@ def batching_argument_deprecation(
return (replace_usecase or usecase_path, settings)
def renamed_arguments_deprecation(
usecase_path: str, settings: dict, arguments_remapped: dict[str, str]
) -> tuple[str, dict]:
for prev_argument, new_argument in arguments_remapped.items():
if prev_argument in settings:
print(
f"WARNING. `{prev_argument}` argument is deprecated for API method "
f'"{usecase_path}" and should be replaced with `{new_argument}`.'
)
settings = settings | {new_argument: settings[prev_argument]}
settings.pop(prev_argument)
return (usecase_path, settings)
ARGUMENTS_DEPRECATION = {
"spatial.assign_container": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products"
@@ -143,6 +157,10 @@ ARGUMENTS_DEPRECATION = {
"project.unassign_declaration": partial(
batching_argument_deprecation, prev_argument="definition", new_argument="definitions"
),
"group.add_group": partial(
renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
),
"layer.add_layer": partial(renamed_arguments_deprecation, arguments_remapped={"Name": "name"}),
}
@@ -324,8 +342,18 @@ def wrap_usecase(usecase_path, usecase):
try:
result = usecase(*args, **settings)
except TypeError as e:
msg = f"Incorrect function arguments provided for {usecase_path}\n{str(e)}. You specified args {args} and settings {settings}\n\nCorrect signature is {inspect.signature(usecase)}\nSee help(ifcopenshell.api.{usecase_path}) for documentation."
except NotImplementedError as e:
if not e.args[0].startswith(f"{usecase.__name__}()"):
# signature errors typically start with function name
# e.g. "TypeError: edit_library() got an unexpected keyword argument 'test'"
# otherwise it's an error inside api call and we shouldn't get in the way
raise e
msg = (
f"Incorrect function arguments provided for {usecase_path}\n{str(e)}. "
f"You specified args {args} and settings {settings}\n\n"
f"Correct signature is {inspect.signature(usecase)}\n"
f"See help(ifcopenshell.api.{usecase_path}) for documentation."
)
raise TypeError(msg) from e
if should_run_listeners:
@@ -89,7 +89,6 @@ def assign_connection_geometry(
usecase.axis = axis
usecase.ref_direction = ref_direction
usecase.unit_scale = unit_scale
usecase.ifc_vertices = []
return usecase.execute()
@@ -110,7 +110,9 @@ class Usecase:
"IfcCalendarDate", **ifcopenshell.util.date.datetime2ifc(edition_date, "IfcCalendarDate")
)
else:
result.EditionDate = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
if edition_date:
edition_date = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
result.EditionDate = edition_date
self.relate_to_project(result)
@@ -16,8 +16,17 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Optional, Literal
def add_context(file, context_type=None, context_identifier=None, target_view=None, parent=None) -> None:
def add_context(
file: ifcopenshell.file,
context_type: Optional[Literal["Model", "Plan"]] = None,
context_identifier: Optional[str] = None,
target_view: Optional[str] = None,
parent: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
"""Adds a new geometric representation context
In IFC, physical objects may have zero, one, or multiple geometric
@@ -87,7 +96,7 @@ def add_context(file, context_type=None, context_identifier=None, target_view=No
:param context_type: The type of the context, must be one of "Model" or
"Plan" only.
:type context_type: str
:type context_type: str, optional
:param context_identifier: The identifier of the context, chosen from
one of the common identifiers above or consult the IFC documentation
(under the IfcShapeRepresentation page) for more details. Optional
@@ -104,7 +113,7 @@ def add_context(file, context_type=None, context_identifier=None, target_view=No
:type parent: ifcopenshell.entity_instance, optional
:return: the newly created IfcGeometricRepresentationContext or
IfcGeometricRepresentationSubContext entity
:rtype: ifcopenshell.entity_instance, optional
:rtype: ifcopenshell.entity_instance
Example:
@@ -16,8 +16,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_context(file, context, attributes) -> None:
def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcGeometricRepresentationContext
For more information about the attributes and data types of an
@@ -26,7 +29,7 @@ def edit_context(file, context, attributes) -> None:
:param context: The IfcGeometricRepresentationContext entity you want to edit
:type context: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:type attributes: dict
:return: None
:rtype: None
@@ -44,7 +47,7 @@ def edit_context(file, context, attributes) -> None:
ifcopenshell.api.run("context.edit_context", model,
context=body, attributes={"ContextIdentifier": "Body"})
"""
settings = {"context": context, "attributes": attributes or {}}
settings = {"context": context, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["context"], name, value)
@@ -22,7 +22,9 @@ from datetime import datetime
from typing import Optional
def add_cost_schedule(file: ifcopenshell.file, name: Optional[str] = None, predefined_type="NOTDEFINED") -> None:
def add_cost_schedule(
file: ifcopenshell.file, name: Optional[str] = None, predefined_type: str = "NOTDEFINED"
) -> ifcopenshell.entity_instance:
"""Add a new cost schedule
A cost schedule is a group of cost items which typically represent a
@@ -16,13 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any, Optional
from typing import Any
def edit_information(
file: ifcopenshell.file,
information: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
attributes: dict[str, Any],
) -> None:
"""Edits the attributes of an IfcDocumentInformation
@@ -32,7 +32,7 @@ def edit_information(
:param reference: The IfcDocumentInformation entity you want to edit
:type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:type attributes: dict
:return: None
:rtype: None
@@ -46,7 +46,7 @@ def edit_information(
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
"""
settings = {"information": information, "attributes": attributes or {}}
settings = {"information": information, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["information"], name, value)
@@ -16,13 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any, Optional
from typing import Any
def edit_reference(
file: ifcopenshell.file,
reference: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
attributes: dict[str, Any],
) -> None:
"""Edits the attributes of an IfcDocumentReference
@@ -32,7 +32,7 @@ def edit_reference(
:param reference: The IfcDocumentReference entity you want to edit
:type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:type attributes: dict
:return: None
:rtype: None
@@ -49,7 +49,7 @@ def edit_reference(
ifcopenshell.api.run("document.edit_reference", model,
reference=reference, attributes={"Identification": "2.1.15"})
"""
settings = {"reference": reference, "attributes": attributes or {}}
settings = {"reference": reference, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["reference"], name, value)
@@ -17,9 +17,14 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
from typing import Union
COORD = Union[tuple[float, float], tuple[float, float, float]]
def add_axis_representation(file, context=None, axis=None) -> None:
def add_axis_representation(
file: ifcopenshell.file, context: ifcopenshell.entity_instance, axis: tuple[COORD, COORD]
) -> ifcopenshell.entity_instance:
"""Adds a new axis representation
Certain objects are typically "axis-based", such as walls, beams,
@@ -16,27 +16,51 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import ifcopenshell.util.unit
import numpy as np
import numpy.typing as npt
from typing import Optional, TYPE_CHECKING, Literal
if TYPE_CHECKING:
import bpy.types
def add_boolean(file, **usecase_settings) -> None:
NPArrayOfFloats = npt.NDArray[np.float64]
def add_boolean(
file: ifcopenshell.file,
representation: ifcopenshell.entity_instance,
# A matrix to define a clipping Ifchalfspacesolid.
# The XY plane is the clipping boundary and +Z is removed.
operator: str = "DIFFERENCE",
# IfcHalfSpaceSolid, Mesh
type: Literal["IfcHalfSpaceSolid", "Mesh"] = "IfcHalfSpaceSolid",
matrix: Optional[NPArrayOfFloats] = None,
# A Blender OBJ to define the voided OBJ for a "Mesh" type
blender_obj: Optional[bpy.types.Object] = None,
# A Blender OBJ to define the void OBJ for a "Mesh" type
blender_void: Optional[bpy.types.Object] = None,
should_force_faceted_brep: bool = False,
should_force_triangulation: bool = False,
) -> list[ifcopenshell.entity_instance]:
"""For `type` values:
- "IfcHalfSpaceSolid" - `matrix` is not optional.
- "Mesh" - `blender_obj` and `blender_void` are not optional
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {
"representation": None,
"operator": "DIFFERENCE",
# IfcHalfSpaceSolid, Mesh
"type": "IfcHalfSpaceSolid",
# The XY plane is the clipping boundary and +Z is removed.
"matrix": None, # A matrix to define a clipping Ifchalfspacesolid.
"blender_obj": None, # A Blender OBJ to define the voided OBJ for a "Mesh" type
"blender_void": None, # A Blender OBJ to define the void OBJ for a "Mesh" type
"should_force_faceted_brep": False,
"should_force_triangulation": False,
"representation": representation,
"operator": operator,
"type": type,
"matrix": matrix,
"blender_obj": blender_obj,
"blender_void": blender_void,
"should_force_faceted_brep": should_force_faceted_brep,
"should_force_triangulation": should_force_triangulation,
}
for key, value in usecase_settings.items():
usecase.settings[key] = value
return usecase.execute()
@@ -16,13 +16,15 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import collections.abc
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from ifcopenshell.api.geometry.add_window_representation import create_ifc_window
from mathutils import Vector
from math import cos, radians
import collections
from typing import Any, Optional, Literal, Union
import dataclasses
SUPPORTED_DOOR_TYPES = (
@@ -38,9 +40,14 @@ SUPPORTED_DOOR_TYPES = (
)
def mm(x: float) -> float:
"""mm to meters shortcut for readability"""
return x / 1000
def create_ifc_door_lining(
builder: ShapeBuilder, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze()
):
) -> ifcopenshell.entity_instance:
"""`thickness` of the profile is defined as list in the following order: `(SIDE, TOP)`
`thickness` can be also defined just as 1 float value.
@@ -69,80 +76,212 @@ def create_ifc_door_lining(
return door_lining
def create_ifc_box(builder: ShapeBuilder, size: Vector, position: Vector = V(0, 0, 0).freeze()):
def create_ifc_box(
builder: ShapeBuilder, size: Vector, position: Vector = V(0, 0, 0).freeze()
) -> ifcopenshell.entity_instance:
rect = builder.rectangle(size.xy)
box = builder.extrude(rect, size.z, position=position, extrusion_vector=V(0, 0, 1))
return box
def add_door_representation(file, **usecase_settings) -> None:
"""units in usecase_settings expected to be in ifc project units"""
# we use dataclass as we need default values for arguments
# it's okay to use slots since we don't need dynamic attributes
@dataclasses.dataclass(slots=True)
class DoorLiningProperties:
LiningDepth: Optional[float] = None
"""Optional, defaults to 50mm."""
LiningThickness: Optional[float] = None
"""Optional, defaults to 50mm."""
LiningOffset: Optional[float] = None
"""Offset from the outer side of the wall (by Y-axis). Optional, defaults to 0.0."""
LiningToPanelOffsetX: Optional[float] = None
"""Offset from the wall. Optional, defaults to 25mm."""
LiningToPanelOffsetY: Optional[float] = None
"""Offset from the X-axis (unlike windows). Optional, defaults to 25mm."""
TransomThickness: Optional[float] = None
"""Vertical distance between door and window panels. Optional, defaults to 0.0."""
TransomOffset: Optional[float] = None
"""Distance from the bottom door opening
to the beginning of the transom
unlike windows TransomOffset which goes to the center of the transom.
Optional, defaults 1.525m."""
ShapeAspectStyle: None = None
"""Optional. Deprecated argument."""
CasingDepth: Optional[float] = None
"""Casing cover wall faces around the opening
on the left, right and upper sides
Casing should be either on both sides of the wall or no casing
If `LiningOffset` is present then therefore casing is not possible on outer wall
therefore there will be no casing on inner wall either. Optional, defaults to 5mm."""
CasingThickness: Optional[float] = None
"""Casing thickness by Z-axis. Optional, defaults to 75mm."""
ThresholdDepth: Optional[float] = None
"""Threshold covers the bottom side of the opening. Optional, defaults to 100mm."""
ThresholdThickness: Optional[float] = None
"""Theshold thickness by Z-axis. Optional, defaults to 25mm."""
ThresholdOffset: Optional[float] = None
"""Threshold offset by Y-axis. Optional, defaults to 0.0."""
def initialize_properties(self, unit_scale: float) -> None:
# in meters
# fmt: off
default_values: dict[str, float] = dict(
LiningDepth = mm(50),
LiningThickness = mm(50),
LiningOffset = 0.0,
LiningToPanelOffsetX = mm(25),
LiningToPanelOffsetY = mm(25),
TransomThickness = 0.0,
TransomOffset = mm(1525),
CasingDepth = mm(5),
CasingThickness = mm(75),
ThresholdDepth = mm(100),
ThresholdThickness = mm(25),
ThresholdOffset = 0.0,
)
# fmt: on
si_conversion = 1 / unit_scale
for attr, default_value in default_values.items():
if getattr(self, attr) is not None:
continue
setattr(self, attr, default_value * si_conversion)
@dataclasses.dataclass(slots=True)
class DoorPanelProperties:
PanelDepth: Optional[float] = None
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
PanelWidth: float = 1.0
"""Ratio to the clear door opening. Optional, defaults to 1.0."""
FrameDepth: Optional[float] = None
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
FrameThickness: Optional[float] = None
"""Frame thickness by X axis. Optional, defaults to 35 mm."""
PanelPosition: None = None
"""Optional, value is never used"""
PanelOperation: None = None
"""Optional, value is never used.
Defines the basic ways to describe how door panels operate."""
ShapeAspectStyle: None = None
"""Optional. Deprecated argument."""
def initialize_properties(self, unit_scale: float) -> None:
# in meters
# fmt: off
default_values: dict[str, float] = dict(
PanelDepth = mm(35),
FrameDepth = mm(35),
FrameThickness = mm(35),
)
# fmt: on
si_conversion = 1 / unit_scale
for attr, default_value in default_values.items():
if getattr(self, attr) is not None:
continue
setattr(self, attr, default_value * si_conversion)
def add_door_representation(
file: ifcopenshell.file,
*, # keywords only as this API implementation is probably not final
context: ifcopenshell.entity_instance,
overall_height: Optional[float] = None,
overall_width: Optional[float] = None,
# door type
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorTypeOperationEnum.htm
operation_type: Literal[
"SINGLE_SWING_LEFT",
"SINGLE_SWING_RIGHT",
"DOUBLE_SWING_RIGHT",
"DOUBLE_SWING_LEFT",
"DOUBLE_DOOR_SINGLE_SWING",
"DOUBLE_DOOR_DOUBLE_SWING",
"SLIDING_TO_LEFT",
"SLIDING_TO_RIGHT",
"DOUBLE_DOOR_SLIDING",
] = "SINGLE_SWING_LEFT",
lining_properties: Optional[Union[DoorLiningProperties, dict[str, Any]]] = None,
panel_properties: Optional[Union[DoorPanelProperties, dict[str, Any]]] = None,
unit_scale: Optional[float] = None,
) -> ifcopenshell.entity_instance:
"""units in usecase_settings expected to be in ifc project units
:param context: IfcGeometricRepresentationContext for the representation.
:type context: ifcopenshell.entity_instance
:param overall_height: Overall door height. Defaults to 2m.
:type overall_height: float, optional
:param overall_width: Overall door width. Defaults to 0.9m.
:type overall_width: float, optional
:param operation_type: Type of the door. Defaults to SINGLE_SWING_LEFT.
:type operation_type: str, optional
:param lining_properties: DoorLiningProperties or a dictionary to create one.
See DoorLiningProperties description for details.
:type lining_properties: Union[DoorLiningProperties, dict[str, Any]]]
:param panel_properties: DoorPanelProperties or a dictionary to create one.
See DoorPanelProperties description for details.
:type panel_properties: Union[DoorPanelProperties, dict[str, Any]]]
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: IfcShapeRepresentation for a door.
:rtype: ifcopenshell.entity_instance
"""
usecase = Usecase()
usecase.file = file
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoor.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorTypeOperationEnum.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorLiningProperties.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorPanelProperties.htm
usecase.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(usecase.file)}
usecase.settings.update(
# define unit_scale first as it's going to be used setting default arguments
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale
settings: dict[str, Any] = {"unit_scale": unit_scale}
if lining_properties is None:
lining_properties = DoorLiningProperties()
elif not isinstance(lining_properties, DoorLiningProperties):
lining_properties = DoorLiningProperties(**lining_properties)
lining_properties.initialize_properties(unit_scale)
lining_properties = dataclasses.asdict(lining_properties)
if panel_properties is None:
panel_properties = DoorPanelProperties()
elif not isinstance(panel_properties, DoorPanelProperties):
panel_properties = DoorPanelProperties(**panel_properties)
panel_properties.initialize_properties(unit_scale)
panel_properties = dataclasses.asdict(panel_properties)
settings.update(
{
"context": None, # IfcGeometricRepresentationContext
"overall_height": usecase.convert_si_to_unit(2.0),
"overall_width": usecase.convert_si_to_unit(0.9),
# DOUBLE_DOOR_DOUBLE_SWING, DOUBLE_DOOR_FOLDING, DOUBLE_DOOR_LIFTING_VERTICAL,
# DOUBLE_DOOR_SINGLE_SWING, DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_LEFT,
# DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_RIGHT, DOUBLE_DOOR_SLIDING,
# DOUBLE_SWING_LEFT, DOUBLE_SWING_RIGHT, FOLDING_TO_LEFT,
# FOLDING_TO_RIGHT, LIFTING_HORIZONTAL, LIFTING_VERTICAL_LEFT,
# LIFTING_VERTICAL_RIGHT, REVOLVING, REVOLVING_VERTICAL,
# ROLLINGUP, SINGLE_SWING_LEFT, SINGLE_SWING_RIGHT, SLIDING_TO_LEFT,
# SLIDING_TO_RIGHT, SWING_FIXED_LEFT, SWING_FIXED_RIGHT
"operation_type": "SINGLE_SWING_LEFT", # door type
"lining_properties": {
"LiningDepth": usecase.convert_si_to_unit(0.050),
"LiningThickness": usecase.convert_si_to_unit(0.050),
# offset from the outer side of the wall (by Y-axis)
"LiningOffset": usecase.convert_si_to_unit(0.0),
# offset from the wall
"LiningToPanelOffsetX": usecase.convert_si_to_unit(0.025),
# offset from the X-axis (unlike windows)
"LiningToPanelOffsetY": usecase.convert_si_to_unit(0.025),
# transom - vertical distance between door and window panels
"TransomThickness": usecase.convert_si_to_unit(0.000),
# TransomOffset - distance from the bottom door opening
# to the beginning of the transom
# unlike windows TransomOffset which goes to the center of the transom
"TransomOffset": usecase.convert_si_to_unit(1.525),
"ShapeAspectStyle": None, # DEPRECATED
# Casing cover wall faces around the opening
# on the left, right and upper sides
# Casing should be either on both sides of the wall or no casing
# If `LiningOffset` is present then therefore casing is not possible on outer wall
# therefore there will be no casing on inner wall either
"CasingDepth": usecase.convert_si_to_unit(0.005),
"CasingThickness": usecase.convert_si_to_unit(0.075), # by Z-axis
# Threshold covers the bottom side of the opening
"ThresholdDepth": usecase.convert_si_to_unit(0.1),
"ThresholdThickness": usecase.convert_si_to_unit(0.025), # by Z-axis
# offset by Y-axis
"ThresholdOffset": usecase.convert_si_to_unit(0.000),
},
"panel_properties": {
"PanelDepth": usecase.convert_si_to_unit(0.035), # by Y
"PanelWidth": 1.0, # as ratio to the clear door opening
"FrameDepth": usecase.convert_si_to_unit(0.035), # by Y
"FrameThickness": usecase.convert_si_to_unit(0.035), # by X
# LEFT, MIDDLE, RIGHT, NOTDEFINED
"PanelPosition": ..., # NEVER USED
# defines the basic ways to describe how door panels operate
# basically how it opens
"PanelOperation": None, # NEVER USED
"ShapeAspectStyle": None, # DEPRECATED
},
"context": context,
"overall_height": overall_height if overall_height is not None else usecase.convert_si_to_unit(2.0),
"overall_width": overall_width if overall_width is not None else usecase.convert_si_to_unit(0.9),
"operation_type": operation_type,
"lining_properties": lining_properties,
"panel_properties": panel_properties,
}
)
for key, value in usecase_settings.items():
usecase.settings[key] = value
usecase.settings = settings
return usecase.execute()
@@ -19,13 +19,17 @@
import ifcopenshell.util.unit
def add_footprint_representation(file, **usecase_settings) -> None:
def add_footprint_representation(
file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
# A list of IFC curves to include in the curve set
curves: list[ifcopenshell.entity_instance],
) -> ifcopenshell.entity_instance:
settings = {
"context": None, # IfcGeometricRepresentationContext
"curves": [], # A list of IFC curves to include in the curve set
"context": context,
"curves": curves,
}
for key, value in usecase_settings.items():
settings[key] = value
return file.createIfcShapeRepresentation(
settings["context"],
@@ -17,26 +17,43 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
from typing import Optional
COORD_3D = tuple[float, float, float]
def add_mesh_representation(file: ifcopenshell.file, **usecase_settings) -> None:
def add_mesh_representation(
file: ifcopenshell.file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
# Vertices, edges, and faces are given in the form of: [item1, item2, item3, ...]
# A list of coordinates
# ... where itemN = [(0., 0., 0.), (1., 1., 1.), (x, y, z), ...]
vertices: list[COORD_3D],
# A list of edges, represented by vertex index pairs
# ... where itemN = [(0, 1), (1, 2), (v1, v2), ...]
edges: list[tuple[int, int]],
# A list of polygons, represented by vertex indices
# ... where itemN = [(0, 1, 2), (5, 4, 2, 3), (v1, v2, v3, ... vN), ...]
faces: list[list[int]],
# Optionally apply a vector offset to all coordinates
cooridnate_offset: Optional[COORD_3D] = None,
# A scale factor to apply for all vectors in case the unit is different
unit_scale: Optional[float] = None,
# Force using IfcFacetedBreps instead of IfcPolygonalFaceSets
force_faceted_brep: bool = False,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": None, # IfcGeometricRepresentationContext
# Vertices, edges, and faces are given in the form of: [item1, item2, item3, ...]
# ... where itemN = [(0., 0., 0.), (1., 1., 1.), (x, y, z), ...]
"vertices": None, # A list of coordinates
# ... where itemN = [(0, 1), (1, 2), (v1, v2), ...]
"edges": None, # A list of edges, represented by vertex index pairs
# ... where itemN = [(0, 1, 2), (5, 4, 2, 3), (v1, v2, v3, ... vN), ...]
"faces": None, # A list of polygons, represented by vertex indices
"coordinate_offset": None, # Optionally apply a vector offset to all coordinates
"unit_scale": None, # A scale factor to apply for all vectors in case the unit is different
"force_faceted_brep": False, # Force using IfcFacetedBreps instead of IfcPolygonalFaceSets
"context": context,
"vertices": vertices,
"edges": edges,
"faces": faces,
"coordinate_offset": cooridnate_offset,
"unit_scale": unit_scale,
"force_faceted_brep": force_faceted_brep,
}
for key, value in usecase_settings.items():
usecase.settings[key] = value
return usecase.execute()
@@ -19,23 +19,35 @@
import ifcopenshell.geom
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
from typing import Any, Union, Optional, Literal
VECTOR_3D = tuple[float, float, float]
def add_profile_representation(file, **usecase_settings) -> None:
def add_profile_representation(
file: ifcopenshell.file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
profile: ifcopenshell.entity_instance,
# in meters
depth: float = 1.0,
cardinal_point: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 5,
# A list of planes that define clipping half space solids
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None),
) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": None, # IfcGeometricRepresentationContext
"profile": None,
"depth": 1.0,
"cardinal_point": 5,
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
"placement_zx_axes": (None, None),
"context": context,
"profile": profile,
"depth": depth,
"cardinal_point": cardinal_point,
"clippings": clippings if clippings is not None else [],
"placement_zx_axes": placement_zx_axes,
}
for key, value in usecase_settings.items():
usecase.settings[key] = value
return usecase.execute()
@@ -22,46 +22,100 @@ from itertools import chain
from mathutils import Vector, Matrix
import collections
import mathutils
from pprint import pprint
from math import pi, cos, sin, tan, radians
from typing import Literal, Optional, Any
def mm(x):
def mm(x: float) -> float:
"""mm to meters shortcut for readability"""
return x / 1000
def add_railing_representation(file, **usecase_settings) -> None:
def add_railing_representation(
file: ifcopenshell.file,
*, # keywords only as this API implementation is probably not final
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
railing_type: Literal["WALL_MOUNTED_HANDRAIL"] = "WALL_MOUNTED_HANDRAIL",
railing_path: list[Vector],
use_manual_supports: bool = False,
support_spacing: Optional[float] = None,
railing_diameter: Optional[float] = None,
clear_width: Optional[float] = None,
terminal_type: Literal[
"180",
"TO_END_POST",
"TO_WALL",
"TO_FLOOR",
"TO_END_POST_AND_FLOOR",
] = "180",
height: Optional[float] = None,
looped_path: bool = False,
unit_scale: Optional[float] = None,
) -> ifcopenshell.entity_instance:
"""
units in usecase_settings expected to be in ifc project units
Units are expected to be in IFC project units.
`railing_path` is a list of point coordinates for the railing path,
coordinates are expected to be at the top of the railing, not at the center
:param context: IfcGeometricRepresentationContext for the representation.
:type context: ifcopenshell.entity_instance
:param railing_type: Type of the railing. Defaults to "WALL_MOUNTED_HANDRAIL".
:type railing_type: Literal["WALL_MOUNTED_HANDRAIL"], optional
:param railing_path: A list of points coordinates for the railing path,
coordinates are expected to be at the top of the railing, not at the center.
If not provided, default path [(0, 0, 1), (1, 0, 1), (2, 0, 1)] (in meters) will be used
:type railing_path: list[Vector], optional.
:param use_manual_supports: If enabled, supports are added on every vertex on the edges of the railing path.
If disabled, supports are added automatically based on the support spacing. Default to False.
:type use_manual_supports: bool, optional
:param support_spacing: Distance between supports if automatic supports are used. Defaults to 1m.
:type support_spacing: float, optional
:param railing_diameter: Railing diameter. Defaults to 50mm.
:type railing_diameter: float, optional
:param clear_width: Clear width between the railing and the wall. Defaults to 40mm.
:type clear_width: float, optional
:param terminal_type: type of the cap. Defaults to "180".
:type terminal_type: Literal["180","TO_END_POST","TO_WALL","TO_FLOOR","TO_END_POST_AND_FLOOR"], optional
:param height: defaults to 1m
:type height: float, optional
:param looped_path: Whether to end the railing on the first point of `railing_path`. Defaults to False.
:type looped_path: bool, optional
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: IfcShapeRepresentation for a railing.
:rtype: ifcopenshell.entity_instance
`railing_path` is expected to be a list of Vector objects
"""
usecase = Usecase()
usecase.file = file
usecase.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(usecase.file)}
usecase.settings.update(
# define unit_scale first as it's going to be used setting default arguments
settings: dict[str, Any] = {
"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale,
}
settings.update(
{
"context": None, # IfcGeometricRepresentationContext
"railing_type": "WALL_MOUNTED_HANDRAIL",
"railing_path": usecase.path_si_to_units([V(0, 0, 1), V(1, 0, 1), V(2, 0, 1)]),
"use_manual_supports": False,
"support_spacing": usecase.convert_si_to_unit(mm(1000)),
"railing_diameter": usecase.convert_si_to_unit(mm(50)),
"clear_width": usecase.convert_si_to_unit(mm(40)),
"terminal_type": "180",
"height": usecase.convert_si_to_unit(mm(1000)),
"looped_path": False,
"context": context,
"railing_type": railing_path,
"railing_path": (
railing_path
if railing_path is not None
else usecase.path_si_to_units([V(0, 0, 1), V(1, 0, 1), V(2, 0, 1)])
),
"use_manual_supports": use_manual_supports,
"support_spacing": support_spacing if support_spacing is not None else usecase.convert_si_to_unit(mm(1000)),
"railing_diameter": (
railing_diameter if railing_diameter is not None else usecase.convert_si_to_unit(mm(50))
),
"clear_width": clear_width if clear_width is not None else usecase.convert_si_to_unit(mm(40)),
"terminal_type": terminal_type,
"height": height if height is not None else usecase.convert_si_to_unit(mm(1000)),
"looped_path": looped_path,
}
)
usecase.settings = settings
for key, value in usecase_settings.items():
usecase.settings[key] = value
if usecase.settings["railing_type"] != "WALL_MOUNTED_HANDRAIL":
if railing_type != "WALL_MOUNTED_HANDRAIL":
raise Exception('Only "WALL_MOUNTED_HANDRAIL" railing_type is supported at the moment.')
return usecase.execute()
@@ -16,11 +16,12 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bpy.types
import math
import bmesh
import ifcopenshell.util.unit
from mathutils import Vector, Matrix
from typing import Union, Optional, Literal, Any
Z_AXIS = Vector((0, 0, 1))
@@ -28,7 +29,44 @@ X_AXIS = Vector((1, 0, 0))
EPSILON = 1e-6
def add_representation(file: ifcopenshell.file, **usecase_settings) -> ifcopenshell.entity_instance:
def add_representation(
file: ifcopenshell.file,
*, # keywords only as this API implementation is probably not final
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
# This is (currently) a Blender object, hence this depends on Blender now
blender_object: bpy.types.Object,
# This is (currently) a Blender data object, hence this depends on Blender now
geometry: Union[bpy.types.Mesh, bpy.types.Curve],
# Optionally apply a vector offset to all coordinates
coordinate_offset: Optional[Vector] = None,
# How many representation items to create
total_items: int = 1,
# A scale factor to apply for all vectors in case the unit is different
unit_scale: Optional[float] = None,
# If we should force faceted breps for meshes
should_force_faceted_brep: bool = False,
# If we should force triangulation for meshes
should_force_triangulation: bool = False,
# If UV coordinates should also be generated
should_generate_uvs: bool = False,
# Whether to cast a mesh into a particular class
ifc_representation_class: Optional[
Literal[
"IfcExtrudedAreaSolid/IfcRectangleProfileDef",
"IfcExtrudedAreaSolid/IfcCircleProfileDef",
"IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef",
"IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids",
"IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage",
"IfcGeometricCurveSet/IfcTextLiteral",
"IfcTextLiteral",
]
] = None,
# The material profile set if the extrusion requires it
profile_set_usage: Optional[ifcopenshell.entity_instance] = None,
# The text literal if the representation requires it
text_literal: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
# lazy import Helper to avoid circular import
if "Helper" not in globals():
from blenderbim.bim.module.geometry.helper import Helper
@@ -37,34 +75,27 @@ def add_representation(file: ifcopenshell.file, **usecase_settings) -> ifcopensh
# TODO: This usecase currently depends on Blender's data model
usecase.file = file
usecase.settings = {
"context": None, # IfcGeometricRepresentationContext
"blender_object": None, # This is (currently) a Blender object, hence this depends on Blender now
"geometry": None, # This is (currently) a Blender data object, hence this depends on Blender now
"coordinate_offset": None, # Optionally apply a vector offset to all coordinates
"total_items": 1, # How many representation items to create
"unit_scale": None, # A scale factor to apply for all vectors in case the unit is different
"should_force_faceted_brep": False, # If we should force faceted breps for meshes
"should_force_triangulation": False, # If we should force triangulation for meshes
"should_generate_uvs": False, # If UV coordinates should also be generated
# Possible IFC representation classes:
# IfcExtrudedAreaSolid/IfcRectangleProfileDef
# IfcExtrudedAreaSolid/IfcCircleProfileDef
# IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef
# IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids
# IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage
# IfcGeometricCurveSet/IfcTextLiteral
# IfcTextLiteral
"ifc_representation_class": None, # Whether to cast a mesh into a particular class
"profile_set_usage": None, # The material profile set if the extrusion requires it
"text_literal": None, # The text literal if the representation requires it
"context": context,
"blender_object": blender_object,
"geometry": geometry,
"coordinate_offset": coordinate_offset,
"total_items": total_items,
"unit_scale": unit_scale,
"should_force_faceted_brep": should_force_faceted_brep,
"should_force_triangulation": should_force_triangulation,
"should_generate_uvs": should_generate_uvs,
"ifc_representation_class": ifc_representation_class,
"profile_set_usage": profile_set_usage,
"text_literal": text_literal,
}
usecase.ifc_vertices = []
for key, value in usecase_settings.items():
usecase.settings[key] = value
return usecase.execute()
class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
self.is_manifold = None
if (
@@ -473,11 +504,10 @@ class Usecase:
geom_data = self.settings["geometry"]
if isinstance(geom_data, bpy.types.Mesh):
if not self.is_mesh_curve_consecutive(geom_data):
return
if self.file.schema == "IFC2X3":
return self.create_curves_from_mesh_ifc2x3(should_exclude_faces=should_exclude_faces, is_2d=is_2d)
return self.create_curves_from_mesh(should_exclude_faces=should_exclude_faces, is_2d=is_2d)
if self.is_mesh_curve_consecutive(geom_data):
if self.file.schema == "IFC2X3":
return self.create_curves_from_mesh_ifc2x3(should_exclude_faces=should_exclude_faces, is_2d=is_2d)
return self.create_curves_from_mesh(should_exclude_faces=should_exclude_faces, is_2d=is_2d)
import blenderbim.tool as tool
@@ -17,22 +17,32 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
from math import sin, cos
from typing import Any, Optional, Union
def add_slab_representation(file, **usecase_settings) -> None:
def add_slab_representation(
file,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
# in meters
depth: float = 0.2,
# in radians
x_angle: float = 0.0,
# A list of planes that define clipping half space solids
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": None, # IfcGeometricRepresentationContext
"depth": 0.2,
"x_angle": 0, # Radians
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
"context": context,
"depth": depth,
"x_angle": x_angle,
"clippings": clippings if clippings is not None else [],
}
for key, value in usecase_settings.items():
usecase.settings[key] = value
return usecase.execute()
@@ -18,27 +18,39 @@
import ifcopenshell.util.unit
from math import sin, cos
from typing import Optional, Union, Any
from ifcopenshell.util.data import Clipping
def add_wall_representation(file, **usecase_settings) -> None:
def add_wall_representation(
file: ifcopenshell.file,
context: ifcopenshell.entity_instance, # IfcGeometricRepresentationContext
# all lengths are in meters
length: float = 1.0,
height: float = 3.0,
offset: float = 0.0,
thickness: float = 0.2,
# Sloped walls along the wall's X axis, provided in radians
x_angle: float = 0.0,
# A list of planes that define clipping half space solids
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
# Any existing IfcBooleanResults
booleans: Optional[list[ifcopenshell.entity_instance]] = None,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
"context": None, # IfcGeometricRepresentationContext
"length": 1.0,
"height": 3.0,
"offset": 0.0,
"thickness": 0.2,
# Sloped walls along the wall's X axis, provided in radians
"x_angle": 0,
# Planes are defined either by Clipping objects
# or by dictionaries of arguments for `Clipping.parse`
"clippings": [], # A list of planes that define clipping half space solids
"booleans": [], # Any existing IfcBooleanResults
"context": context,
"length": length,
"height": height,
"offset": offset,
"thickness": thickness,
"x_angle": x_angle,
"clippings": clippings if clippings is not None else [],
"booleans": booleans if booleans is not None else [],
}
for key, value in usecase_settings.items():
usecase.settings[key] = value
return usecase.execute()
@@ -16,11 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import collections.abc
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from itertools import chain
from mathutils import Vector
import collections
import dataclasses
from typing import Any, Optional, Literal, Union
# SCHEMAS describe panels setup
@@ -42,6 +45,11 @@ DEFAULT_PANEL_SCHEMAS = {
}
def mm(x: float) -> float:
"""mm to meters shortcut for readability"""
return x / 1000
def create_ifc_window_frame_simple(
builder: ShapeBuilder, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze()
):
@@ -210,71 +218,209 @@ def create_ifc_window(
return output_items
def add_window_representation(file, **usecase_settings) -> None:
"""units in usecase_settings expected to be in ifc project units"""
# we use dataclass as we need default values for arguments
# it's okay to use slots since we don't need dynamic attributes
@dataclasses.dataclass(slots=True)
class WindowLiningProperties:
LiningDepth: Optional[float] = None
"""Optional, defaults to 50mm."""
LiningThickness: Optional[float] = None
"""Optional, defaults to 50mm."""
LiningOffset: Optional[float] = None
"""Offset to the wall. Optional, defaults to 50mm."""
LiningToPanelOffsetX: Optional[float] = None
"""Offset from the wall. Optional, defaults to 25mm."""
# that way it allows you to define overall_depth constant between all panels
# and still have panels with different size:
# overall_depth = lining_depth + offset_y
# full offset from X axis = overall_depth - frame_depth.
LiningToPanelOffsetY: Optional[float] = None
"""Offset from the lining. Optional, defaults to 25mm."""
MullionThickness: Optional[float] = None
"""Mullion thickness (horizontal distance between panels).
Applies to windows of types: DoublePanelVertical, TriplePanelBottom, TriplePanelTop,
TriplePanelLeft, TriplePanelRight.
Optional, defaults to 50mm."""
FirstMullionOffset: Optional[float] = None
"""Distance from the first lining to the mullion center. Optional, defaults to 300mm."""
SecondMullionOffset: Optional[float] = None
"""Distance from the first lining to the second mullion center.
Applies to windows of type: TriplePanelVertical.
Optional, defaults to 450mm."""
TransomThickness: Optional[float] = None
"""Transom thickness (vertical distance between panels), works similar way to mullions.
Applies to windows of types:DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop,
TriplePanelLeft, TriplePanelRight.
Optional, defaults to 50mm."""
FirstTransomOffset: Optional[float] = None
"""Optional, defaults to 300mm."""
SecondTransomOffset: Optional[float] = None
"""
Applies to windows of type: TriplePanelHorizontal.
Optional, defaults to 600mm."""
ShapeAspectStyle: None = None
"""Optional. Deprecated argument."""
def initialize_properties(self, unit_scale: float) -> None:
# in meters
# fmt: off
default_values: dict[str, float] = dict(
LiningDepth = mm(50),
LiningThickness = mm(50),
LiningOffset = mm(50),
LiningToPanelOffsetX = mm(25),
LiningToPanelOffsetY = mm(25),
MullionThickness = mm(50),
FirstMullionOffset = mm(300),
SecondMullionOffset = mm(450),
TransomThickness = mm(50),
FirstTransomOffset = mm(300),
SecondTransomOffset = mm(600),
)
# fmt: on
si_conversion = 1 / unit_scale
for attr, default_value in default_values.items():
if getattr(self, attr) is not None:
continue
setattr(self, attr, default_value * si_conversion)
@dataclasses.dataclass(slots=True)
class WindowPanelProperties:
FrameDepth: Optional[float] = None
"""Frame thickness by Y axis. Optional, defaults to 35 mm."""
FrameThickness: Optional[float] = None
"""Frame thickness by X axis. Optional, defaults to 35 mm."""
PanelPosition: None = None
"""Optional, value is never used"""
PanelOperation: None = None
"""Optional, value is never used.
Defines the basic ways to describe how window panels operate."""
ShapeAspectStyle: None = None
"""Optional. Deprecated argument."""
def initialize_properties(self, unit_scale: float) -> None:
# in meters
# fmt: off
default_values: dict[str, float] = dict(
FrameDepth = mm(35),
FrameThickness = mm(35),
)
# fmt: on
si_conversion = 1 / unit_scale
for attr, default_value in default_values.items():
if getattr(self, attr) is not None:
continue
setattr(self, attr, default_value * si_conversion)
def add_window_representation(
file: ifcopenshell.file,
*, # keywords only as this API implementation is probably not final
context: ifcopenshell.entity_instance,
overall_height: Optional[float] = None,
overall_width: Optional[float] = None,
partition_type: Literal[
"SINGLE_PANEL",
"DOUBLE_PANEL_HORIZONTAL",
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_VERTICAL",
] = "SINGLE_PANEL",
lining_properties: Optional[Union[WindowLiningProperties, dict[str, Any]]] = None,
panel_properties: Optional[list[Union[WindowPanelProperties, dict[str, Any]]]] = None,
unit_scale: Optional[float] = None,
) -> ifcopenshell.entity_instance:
"""units in usecase_settings expected to be in ifc project units
:param context: IfcGeometricRepresentationContext for the representation.
:type context: ifcopenshell.entity_instance
:param overall_height: Overall window height. Defaults to 0.9m.
:type overall_height: float, optional
:param overall_width: Overall window width. Defaults to 0.6m.
:type overall_width: float, optional
:param partition_type: Type of the window. Defaults to SINGLE_PANEL.
:type partition_type: str, optional
:param lining_properties: WindowLiningProperties or a dictionary to create one.
See WindowLiningProperties description for details.
:type lining_properties: Union[WindowLiningProperties, dict[str, Any]]]
:param panel_properties: A list of WindowPanelProperties or dictionaries to create one.
See WindowPanelProperties description for details.
:type panel_properties: list[Union[WindowPanelProperties, dict[str, Any]]]]
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: IfcShapeRepresentation for a window.
:rtype: ifcopenshell.entity_instance
"""
usecase = Usecase()
usecase.file = file
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindow.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowTypePartitioningEnum.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowLiningProperties.htm
# http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowPanelProperties.htm
usecase.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(usecase.file)}
usecase.settings.update(
# define unit_scale first as it's going to be used setting default arguments
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale
settings: dict[str, Any] = {"unit_scale": unit_scale}
if lining_properties is None:
lining_properties = WindowLiningProperties()
elif not isinstance(lining_properties, WindowLiningProperties):
lining_properties = WindowLiningProperties(**lining_properties)
lining_properties.initialize_properties(unit_scale)
lining_properties = dataclasses.asdict(lining_properties)
if panel_properties is None:
panel_properties = [WindowPanelProperties()]
for i in range(len(panel_properties)):
properties = panel_properties[i]
if not isinstance(properties, WindowPanelProperties):
properties = WindowPanelProperties(**properties)
properties.initialize_properties(unit_scale)
panel_properties[i] = dataclasses.asdict(properties)
settings.update(
{
"context": None, # IfcGeometricRepresentationContext
# SINGLE_PANEL, DOUBLE_PANEL_HORIZONTAL, DOUBLE_PANEL_VERTICAL,
# TRIPLE_PANEL_BOTTOM, TRIPLE_PANEL_HORIZONTAL, TRIPLE_PANEL_LEFT,
# TRIPLE_PANEL_RIGHT, TRIPLE_PANEL_TOP, TRIPLE_PANEL_VERTICAL
"partition_type": "SINGLE_PANEL",
"overall_height": usecase.convert_si_to_unit(0.9),
"overall_width": usecase.convert_si_to_unit(0.6),
"lining_properties": {
"LiningDepth": usecase.convert_si_to_unit(0.050),
"LiningThickness": usecase.convert_si_to_unit(0.050),
"LiningOffset": usecase.convert_si_to_unit(0.050), # offset to the wall
# offset from the wall
"LiningToPanelOffsetX": usecase.convert_si_to_unit(0.025),
# offset from the lining
# that way it allows you to define overall_depth constant between all panels
# and still have panels with different size:
# overall_depth = lining_depth + offset_y
# full offset from X axis = overall_depth - frame_depth
"LiningToPanelOffsetY": usecase.convert_si_to_unit(0.025),
# applies to DoublePanelVertical, TriplePanelBottom, TriplePanelTop,
# TriplePanelLeft, TriplePanelRight
# mullion - horizontal distance between panels
"MullionThickness": usecase.convert_si_to_unit(0.050),
# distance from the first lining to the mullion center
"FirstMullionOffset": usecase.convert_si_to_unit(0.3),
# applies to TriplePanelVertical
# distance from the first lining to the second mullion center
"SecondMullionOffset": usecase.convert_si_to_unit(0.45),
# applies to DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop,
# TriplePanelLeft, TriplePanelRight
# works similar way to mullion
"TransomThickness": usecase.convert_si_to_unit(0.050),
"FirstTransomOffset": usecase.convert_si_to_unit(0.3),
# applies to TriplePanelHorizontal
"SecondTransomOffset": usecase.convert_si_to_unit(0.6),
"ShapeAspectStyle": None, # DEPRECATED
},
"panel_properties": [
{
"FrameDepth": usecase.convert_si_to_unit(0.035), # by Y
"FrameThickness": usecase.convert_si_to_unit(0.035), # by X
# BOTTOM, LEFT, MIDDLE, RIGHT, TOP
"PanelPosition": ..., # NEVER USED
# defines the basic ways to describe how window panels operate
# how it's hanged, how it opens
"OperationType": None, # NEVER USED
"ShapeAspectStyle": None, # DEPRECATED
},
],
"context": context,
"overall_height": overall_height if overall_height is not None else usecase.convert_si_to_unit(0.9),
"overall_width": overall_width if overall_width is not None else usecase.convert_si_to_unit(0.6),
"partition_type": partition_type,
"lining_properties": lining_properties,
"panel_properties": panel_properties,
}
)
for key, value in usecase_settings.items():
usecase.settings[key] = value
usecase.settings = settings
usecase.settings["panel_schema"] = DEFAULT_PANEL_SCHEMAS[usecase.settings["partition_type"]]
return usecase.execute()
@@ -20,12 +20,12 @@ import ifcopenshell.api
import ifcopenshell.util.element
def assign_representation(file, **usecase_settings) -> None:
def assign_representation(
file: ifcopenshell.file, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {"product": None, "representation": None}
for key, value in usecase_settings.items():
usecase.settings[key] = value
usecase.settings = {"product": product, "representation": representation}
return usecase.execute()
@@ -20,16 +20,20 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
from typing import Optional
def connect_element(file, **usecase_settings) -> None:
def connect_element(
file: ifcopenshell.file,
relating_element: ifcopenshell.entity_instance,
related_element: ifcopenshell.entity_instance,
description: Optional[str] = None,
) -> ifcopenshell.entity_instance:
settings = {
"relating_element": None,
"related_element": None,
"description": None,
"relating_element": relating_element,
"related_element": related_element,
"description": description,
}
for key, value in usecase_settings.items():
settings[key] = value
incompatible_connections = []
@@ -20,18 +20,24 @@ import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
from typing import Optional
def connect_path(file, **usecase_settings) -> None:
def connect_path(
file: ifcopenshell.file,
relating_element: ifcopenshell.entity_instance,
related_element: ifcopenshell.entity_instance,
relating_connection: str = "NOTDEFINED",
related_connection: str = "NOTDEFINED",
description: Optional[str] = None,
) -> ifcopenshell.entity_instance:
settings = {
"relating_element": None,
"related_element": None,
"relating_connection": "NOTDEFINED",
"related_connection": "NOTDEFINED",
"description": None,
"relating_element": relating_element,
"related_element": related_element,
"relating_connection": relating_connection,
"related_connection": related_connection,
"description": description,
}
for key, value in usecase_settings.items():
settings[key] = value
incompatible_connections = []
for rel in settings["relating_element"].ConnectedTo:
@@ -22,8 +22,16 @@ import ifcopenshell.util.unit
def create_2pt_wall(
file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None, is_si=True
) -> None:
file: ifcopenshell.file,
element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
p1: tuple[float, float],
p2: tuple[float, float],
elevation: float,
height: float,
thickness: float,
is_si: bool = True,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {
@@ -20,30 +20,31 @@ import ifcopenshell
import ifcopenshell.util.element
def disconnect_element(file, **usecase_settings) -> None:
settings = {
"relating_element": None,
"related_element": None,
}
for key, value in usecase_settings.items():
settings[key] = value
def disconnect_element(
file: ifcopenshell.file,
relating_element: ifcopenshell.entity_instance,
related_element: ifcopenshell.entity_instance,
) -> None:
# TODO: arguments relating_element, related_element probably
# should be renamed to element1, element2
# as api call doesn't really treat them as "relating" and "related"
# and just purging all connections between them
incompatible_connections = []
for rel in settings["relating_element"].ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == settings["related_element"]:
for rel in relating_element.ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == related_element:
incompatible_connections.append(rel)
for rel in settings["relating_element"].ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == settings["related_element"]:
for rel in relating_element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == related_element:
incompatible_connections.append(rel)
for rel in settings["related_element"].ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == settings["relating_element"]:
for rel in related_element.ConnectedTo:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == relating_element:
incompatible_connections.append(rel)
for rel in settings["related_element"].ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == settings["relating_element"]:
for rel in related_element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == relating_element:
incompatible_connections.append(rel)
if incompatible_connections:
@@ -19,33 +19,36 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
from typing import Optional
def disconnect_path(file, **usecase_settings) -> None:
settings = {
"relating_element": None,
"related_element": None,
"element": None,
"connection_type": None,
}
for key, value in usecase_settings.items():
settings[key] = value
if settings["connection_type"] and settings["element"]:
def disconnect_path(
file: ifcopenshell.file,
element: Optional[ifcopenshell.entity_instance] = None,
connection_type: Optional[str] = None,
relating_element: Optional[ifcopenshell.entity_instance] = None,
related_element: Optional[ifcopenshell.entity_instance] = None,
) -> None:
"""There are two options to use this API method:
- provide `element` (connected from) and `connection_type` that should be disconnected.
- provide connected elements to disconnect explicitly:
`relating_element` (connected from) and `related_element` (connected to)
"""
if connection_type and element:
connections = [
r
for r in settings["element"].ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatingConnectionType == settings["connection_type"]
for r in element.ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatingConnectionType == connection_type
] + [
r
for r in settings["element"].ConnectedFrom
if r.is_a("IfcRelConnectsPathElements") and r.RelatedConnectionType == settings["connection_type"]
for r in element.ConnectedFrom
if r.is_a("IfcRelConnectsPathElements") and r.RelatedConnectionType == connection_type
]
else:
elif related_element:
connections = [
r
for r in settings["relating_element"].ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == settings["related_element"]
for r in relating_element.ConnectedTo
if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == related_element
]
for connection in set(connections):
@@ -31,8 +31,8 @@ def edit_object_placement(
file: ifcopenshell.file,
product: ifcopenshell.entity_instance,
matrix: Optional[NPArrayOfFloats] = None,
is_si=True,
should_transform_children=False,
is_si: bool = True,
should_transform_children: bool = False,
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
@@ -16,14 +16,15 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def map_representation(file, **usecase_settings) -> None:
def map_representation(
file: ifcopenshell.file, representation: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
usecase = Usecase()
usecase.file = file
usecase.settings = {"representation": None}
usecase.ifc_vertices = []
for key, value in usecase_settings.items():
usecase.settings[key] = value
usecase.settings = {"representation": representation}
return usecase.execute()
@@ -19,12 +19,10 @@
import ifcopenshell.util.element
def remove_boolean(file, **usecase_settings) -> None:
def remove_boolean(file: ifcopenshell.file, item: ifcopenshell.entity_instance) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {"item": None}
for key, value in usecase_settings.items():
usecase.settings[key] = value
usecase.settings = {"item": item}
return usecase.execute()

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