Compare commits

...

240 Commits

Author SHA1 Message Date
Dion Moult 5a82d7a9d4 Refactor enabling pset editing operator into core/tool 2024-05-22 20:52:43 +10:00
Dion Moult 69c197c1a4 get_psets utility now defaults to merging duplicate psets instead of overriding
This behaviour is more comprehensive for broken data and now consistent for types and materials.
2024-05-22 08:12:38 +10:00
Ryan Schultz 4027f28c85 Can bim.select_similar from the pset or attribute panels (#4611) 2024-05-21 10:30:38 -05:00
Bruno Perdigão 3a1154c4ba Merge pull request #4597 from IfcOpenShell/linked_aggregate_operator_from_panel
when bpy.ops.bim.object_duplicate_move_linked_aggregate is called fro…
2024-05-21 10:45:49 -03:00
Dion Moult aeb1e4bf49 See #4696. Allow editing of properties that do not comply with the pset template definition.
Strictly speaking, the only properties that should exist in a pset should be those in a pset template. However, there are situations where this is not the case, such as when the pset template has since been modified, or if there is invalid data coming from other software, or migrating between schemas. In this case, we should still load the property, and give the user the option to edit it (or null it).
2024-05-21 23:06:15 +10:00
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
Dion Moult b7737cb6d3 Fix #4655. profiles is now available as a keyword in the selector syntax. 2024-05-11 22:57:16 +10:00
Dion Moult 55026ca996 Fix #4616. You can now create a new representation based off any other object. This makes it easy to custom model whatever you want as a starting point. 2024-05-11 22:29:39 +10:00
Dion Moult 3fa573e3c1 Fix #4615. Report to user if they are attempting to create an incompatible context-representation combination. 2024-05-11 20:48:38 +10:00
Dion Moult 00ae6809a9 See #4657. Fallback to copying cache if moving cache fails due to permission error. 2024-05-11 19:59:39 +10:00
Dion Moult c3fbb11093 See #4657. Restrict annotation psets to their particular predefined type. 2024-05-11 19:38:22 +10:00
Kurt Battisti a0d27c3f1c Make Whole status UI message clearer (#4658)
Replace UI text "No Aggregate" with "No Whole relation defined" to avoid misinterpretation when instance is a whole with parts.
2024-05-11 19:04:35 +10:00
Dion Moult 811a908cc3 More segfault fixes in test suite (maybe order matters only on Linux?) 2024-05-11 19:01:57 +10:00
Dion Moult 94e5baf48a Fix #4647. Better error reporting if the user tries to activate a drawing that isn't available. 2024-05-11 19:01:21 +10:00
Dion Moult cd2570cdbb Fix #4649. Bug where editing property set template enumerations didn't clean up properly. 2024-05-11 15:18:18 +10:00
Dion Moult 6df6553935 Notify user if duration parsing not available in util.date 2024-05-11 15:14:39 +10:00
Dion Moult 8e6c372a4f Fix ability to run tests and segfaulting test 2024-05-11 15:00:30 +10:00
Andrej730 371e11de9b root.remove_product to consider IfcRelConnectsPortToElement 2024-05-10 19:18:09 +05:00
Andrej730 d2a708fcc4 fix bug in ifc2x3 tests bootstrap
it was creating new applications and users every time `ifcopenshell.api.owner.settings.get_user` or `ifcopenshell.api.owner.settings.get_application` was called
2024-05-10 19:18:09 +05:00
Andrej730 845d772a08 move sequence.get_related_products to util module 2024-05-10 19:18:09 +05:00
Andrej730 1e630ede08 ifc2x3 tests 2024-05-10 19:18:09 +05:00
Andrej730 c50d1ea2fe typing 2024-05-10 19:18:08 +05:00
Dion Moult e7eb00eaa3 See #4652. Fix bug where auto-detection of a false origin could be incorrect
We were incorrectly multiplying a shape matrix in SI units with a vertex in project units. We also didn't do a final check whether or not that final resultant coordinate was far away or not (for example, origin and vertex can cancel each other out)
2024-05-10 23:28:37 +10:00
Dion Moult cd6eac3c8f See #4652. Fix bug where using create_mesh won't work if you pass it a triangulation. 2024-05-10 22:39:56 +10:00
Dion Moult 69e21da60c Minor naming changes in IfcFM to be more true to IFC jargon 2024-05-10 22:13:39 +10:00
Dion Moult 57b905862a Fix #4650. When the bbim build hotfixes the bot build, it should replace, not copy over. 2024-05-10 18:20:41 +10:00
Dion Moult 51d3a2b77b Whoops 2024-05-10 16:16:43 +10:00
Andrej730 b2920996be remove deprecated api method type.get_related_objects 2024-05-09 18:15:49 +05:00
Andrej730 bdc5a7a01b more descriptive errors creating entities - setting incorrect attributes
example:

```
ifc_file = ifcopenshell.file(schema="IFC4")

entity = ifc_file.create_entity("IfcRoot", "xxx", None, None, None, None)
entity = ifc_file.create_entity("IfcRoot", Location=None)
```
2024-05-09 18:15:49 +05:00
Andrej730 1c6a9e2c49 more descriptive errors settings incorrect data type to attributes
Example:
```
ifc_file = ifcopenshell.file(schema="IFC2X3")
entity = ifc_file.create_entity("IfcCostSchedule")

entity.UpdateDate = ifcopenshell.util.date.datetime2ifc(datetime.now(), "IfcDateTime")
```
2024-05-09 18:15:49 +05:00
Andrej730 8ffb77551f more tests for ifc2x3 2024-05-09 18:15:49 +05:00
Andrej730 2d35869c41 typing 2024-05-09 18:00:17 +05:00
Andrej730 359dab573b document.remove_information to support ifc2x3 #4636 2024-05-09 14:02:10 +05:00
Andrej730 6e2edbf101 explicitly specify available imported symbols
Fix LSP warnings like ""entity_instance" is not exported from module "ifcopenshell"PylancereportPrivateImportUsage" for `ifcopenshell.file`, `ifcopenshell.entity_instance` etc.

After adding py.typed in 722201a1a it's now a typed library and all imported symbols in modules are considered private by default and should be added to `__all__` if they are supposed to be generally available from imported module.

See https://github.com/microsoft/pyright/blob/main/docs/typed-libraries.md#library-interface
2024-05-09 13:35:29 +05:00
Dion Moult 4c8f93d737 Add error catching to model loading and let users hide error dialog 2024-05-09 16:33:05 +10:00
Dion Moult 2789bc565a Write troubleshooting documentation 2024-05-09 16:21:24 +10:00
Dion Moult a1e0b8858e Track last actions and friendlier error reporting when something breaks 2024-05-09 13:37:57 +10:00
Dion Moult f075a6178b Experimental code for serialising API settings 2024-05-09 13:29:26 +10:00
Dion Moult 5fa385e1e0 Allow for having wildcard listeners in the api 2024-05-09 13:27:08 +10:00
Dion Moult b57141a80c New type of IfcTester report which doesn't require audit to be run to see a spreadsheet view of requirements 2024-05-09 10:46:57 +10:00
Dion Moult d453adb7cf Forgot to commit IfcTester fix for missing failed entities 2024-05-09 10:36:39 +10:00
Andrej730 c5a7a8680e fix removing references in ifc2x3 #4636 2024-05-08 18:18:03 +05:00
Andrej730 d4ccadb832 fix circular import after 10d30b0de 2024-05-08 18:18:03 +05:00
Andrej730 cbf04546b8 fix many implicit ifcopenshell.guid imports
a bit related to 10d30b0de
2024-05-08 18:18:02 +05:00
Dion Moult 37c0084874 Friendlier error reporting if bbim fails to install 2024-05-08 22:26:54 +10:00
Andrej730 875afd69d5 skip objects without placements importing containers
It seems that IFC doesn't specify that IfcRelAggregates.RelatedObjects should necessary have a placement. E.g. FreeCAD is using IfcGroups in RelatedObjects to store hierarchy in IFC.

Example - https://community.osarch.org/discussion/2144/object-information-spatial-container
2024-05-08 13:47:11 +05:00
Andrej730 67f4ed00d0 fix couple silent bugs importing containers
1) we were setting OwnerHistory to the "elevation" attribute of SpatialElement (in BIMSpatialProperties.containers)
2) we were setting "elevation" attribute at all when SpatialElement doesn't have "elevation" attribute

Though those bugs were silent since PropertyGroup doesn't alarm when we add a completely new attribute to it.
2024-05-08 13:47:11 +05:00
Dion Moult 10d30b0de7 Document api modules and require explicit imports of API submodules 2024-05-08 17:00:35 +10:00
Bruno Perdigão 1110f0cffb Merge branch 'v0.7.0' into linked_aggregate_operator_from_panel 2024-05-07 21:00:03 -03:00
Bruno Perdigão 66e829a8aa - Fix the basepoint to be the parent aggregate.
- Change the way that "location_to_cursor_3d" is called.
- Create a new operator to use in the ui.
2024-05-07 20:56:12 -03:00
Dion Moult 984b1212a6 Whoops 2024-05-08 09:45:12 +10:00
Andrej730 bc72c927c1 replace deprecated api call
noticed fixing #4632
2024-05-07 21:41:01 +05:00
Andrej730 b8275f2802 fix errors using deprecated api after ab696b9 #4632 2024-05-07 21:39:09 +05:00
Andrej730 267527f3fc fix issue after removing ifcopenshell.main in d76462ca4 2024-05-07 21:26:48 +05:00
Ryan Schultz c5b4513f65 fix #4622 - can now reassign IfcWindowStyle and IfcDoorStyle 2024-05-07 09:46:55 -05:00
Dion Moult 5da4fbb39c Fix #4631. Fix packaging problem on PyPI for IfcPatch. 2024-05-07 23:36:56 +10:00
Dion Moult 93639e9e50 Even more cleaning of documentation references 2024-05-07 19:08:13 +10:00
Dion Moult d76462ca42 Write more documentation
Sphinx autoapi also now only shows subpackages 1 level deep. This prevents us having a huge long list. Also don't show private or special members. Also show imported members so ifcopenshell.file and ifcopenshell.entity_instance works in docs too.
2024-05-07 17:32:47 +10:00
Dion Moult 722201a1af Add py.typed for static analysis with mypy 2024-05-07 17:15:58 +10:00
Dion Moult 0a3dddef2f More Python 2 to Python 3 upgrades 2024-05-07 16:03:07 +10:00
Dion Moult 26434f0331 Fix #4589. Symlink entire ifcopenshell dir for dev setups. 2024-05-07 15:51:48 +10:00
Dion Moult f6c2e2c20d Fix #4530. Fix various styling issues on docs. 2024-05-07 15:44:06 +10:00
Dion Moult 424a06f6c8 More cleaning up of forward type hints to fix import errors 2024-05-07 14:58:12 +10:00
Dion Moult 89c4cbeb05 Drop support for Python 2. 2024-05-07 12:17:46 +10:00
Dion Moult 7e13ed746e Don't rely on util for basic ifcopenshell module capabilities. Keep util as an optional module for users to load. 2024-05-07 10:32:02 +10:00
Andrej730 28d205b4a3 show info message on saving/loading csv from bbim 2024-05-06 18:56:20 +05:00
Andrej730 dcc52fc4e4 pset.edit_pset not to fail silently on invalid enum values #4608 2024-05-06 18:22:52 +05:00
Andrej730 a9d785c28f typing 2024-05-06 18:22:52 +05:00
Bruno Postle 54a3730c20 fix d11ec67 mathutils dependency regression
mathutils is a blender module, wrap imports in try/except
2024-05-06 13:55:20 +01:00
Andrej730 31d322a28c descriptions for operators removing classifications/contexts #4614 2024-05-06 17:29:30 +05:00
Andrej730 3b90086109 Fix loading search queries with exclusion #4609
It was failing to load queries like "!IfcWall" or "!2Mg7PHubX0xxOfF0DdA9Wd"
2024-05-06 17:06:10 +05:00
Gorgious56 30b870b98d Fix #4617 : Switching representation in edit mode is no longer possible 2024-05-06 13:20:33 +02:00
Thomas Krijnen b3e7975b83 Consider material profile set in single material determination 2024-05-06 13:18:05 +02:00
Andrej730 8f7f6223de unlink pasted blender objects if there is no active ifc file #4619 2024-05-06 15:03:27 +05:00
Dion Moult 6c313dd25c Fix for static analysis of ifcopenshell.api submodules. See #2693. 2024-05-06 18:39:13 +10:00
Bruno Postle b718bd19bd fix TypeError
File "/usr/lib64/python3.12/site-packages/ifcopenshell/api/project/assign_declaration.py", line 126, in execute
    related_definitions = set(has_context.RelatedDefinitions) - objects_with_contexts
                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for -: 'set' and 'list'
2024-05-06 09:05:56 +01:00
Dion Moult ab5ea4c853 Implement listener wrapper for new API functions. See #2693. 2024-05-06 14:43:43 +10:00
Dion Moult d11ec67129 Generate functions for all API usecases for better static code features. See #2693. 2024-05-06 14:43:43 +10:00
Ryan Schultz 10f894e2ea Find the "Types" Collection regardless of the IfcProject.Name 2024-05-05 20:46:07 -05:00
Ryan Schultz 34bbc8f1ea for 'Add Fitting' changed the hot key from Shift+F to Shift+Y. per https://community.osarch.org/discussion/2124/ui-discussion-around-add-fitting-and-add-bend
Also added Shift+F (bim.flip_object) to all profile-based objects
2024-05-05 19:20:13 -05:00
Dion Moult a213ab6760 Fix docstrings to reference datatype as it would be used for users 2024-05-06 08:39:48 +10:00
Ryan Schultz d694d8fcc2 fix #4610: bim.select_type() from type launcher throws error 2024-05-05 13:16:30 -05:00
ppaawweeuu 5f2d451c5e Update selector_syntax.rst (#4618)
info about 'Must contain..' and 'Must not contain..' comparisons added
2024-05-05 23:40:32 +10:00
Dion Moult ab696b980a See #2693. Expose static functions of IfcOpenShell API. 2024-05-05 16:26:41 +10:00
Andrej730 9f2df7fb9d preview external shading style as material is selected from other .blend file
previously it required to save external style attributes to preview the changes, now it's couple clicks saved if you want to preview different blender materials for your styles
2024-05-03 17:55:30 +05:00
Andrej730 9e5c3ff675 fix bug appending blender material when style.Location isn't .blend
it wasn't considering that .Location could be either None or not a .blend file and was failing in those cases
2024-05-03 17:38:04 +05:00
Andrej730 a67108e6c1 Reload current shading style after editing it #4567
Examples:
1) active shading type = EXTERNAL, now after changing it's SHADING attributes and accepting the changes, it will reload EXTERNAL shading back since it's the one that's active.
2) active shading type = SHADING, after adding EXTERNAL shader will reload SHADING style to the material.
2024-05-03 17:36:14 +05:00
Andrej730 b1706f2eac fix error launching type manager in empty project 2024-05-03 16:28:10 +05:00
Andrej730 ac29e152bb project.unassign_declaration - support batching #4474 2024-05-03 16:28:10 +05:00
Andrej730 de690a385c project.assign_declaration - support batching #4474 2024-05-03 16:28:10 +05:00
Andrej730 a973b62918 small comment fix 2024-05-03 16:28:10 +05:00
Andrej730 92426a640a bim.active_model not to unhide all types and drawings elements 2024-05-03 16:23:55 +05:00
Andrej730 93f8638a94 Use active camera drawing id for bim.create_drawing #4581
There was inconsistency between .poll and .execute on what use as active drawing.
2024-05-03 16:23:55 +05:00
Andrej730 16b0b1c3a5 Fix UI error #4575 2024-05-03 15:43:42 +05:00
Dion Moult 25fff3fdd0 Consolidate duplicate operators "Convert to Blender file" and "Purge IFC Links" 2024-05-03 17:02:05 +10:00
dependabot[bot] 0384de46a4 Bump pydantic from 1.10.7 to 1.10.13 in /src/opencdeserver/api/app (#4585)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.7 to 1.10.13.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.7...v1.10.13)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-03 16:21:08 +10:00
Ryan Schultz 080f325557 extends bim.select_type by selecting multiple types from a selection of objects.
Also when the type is selected and turned on, all other types are automatically turned off.
https://imgur.com/a/DQUF0ai
2024-05-03 00:59:33 -05:00
Andrej730 86cc2bf39a fix bug using "trace outlines" for reprsentation in ifc2x3
Mentioned in #4593

The error message was (it was trying to access curves from the original mesh instead of dummy curve object):
```
Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\geometry\operator.py", line 46, in execute
    IfcStore.execute_ifc_operator(self, context)
  File "\blenderbim\bim\ifc.py", line 349, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\bim\module\geometry\operator.py", line 169, in _execute
    core.add_representation(
  File "\blenderbim\core\geometry.py", line 52, in add_representation
    representation = ifc.run(
                     ^^^^^^^^
  File "\blenderbim\tool\ifc.py", line 34, in run
    return ifcopenshell.api.run(command, IfcStore.get_file(), **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\api\__init__.py", line 172, in run
    result = usecase_class(ifc_file, **settings).execute()
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\api\geometry\add_representation.py", line 73, in execute
    return self.create_plan_representation()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\api\geometry\add_representation.py", line 170, in create_plan_representation
    return self.create_annotation2d_representation()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\api\geometry\add_representation.py", line 833, in create_annotation2d_representation
    items = [self.file.createIfcGeometricCurveSet(self.create_curves(is_2d=True))]
                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\api\geometry\add_representation.py", line 492, in create_curves
    curves = self.create_curves_from_curve_ifc2x3(is_2d=is_2d, curve_object_data=dummy.data)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\api\geometry\add_representation.py", line 575, in create_curves_from_curve_ifc2x3
    for spline in self.settings["geometry"].splines:
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Mesh' object has no attribute 'splines'
```
2024-05-02 16:48:52 +05:00
Andrej730 c7effa3f25 More descriptive errors linking ifc files 2024-05-02 12:13:44 +05:00
Andrej730 be262eaef2 fix errors linking ifc2x3 projects
it was failing with something very verbose:

```
  File "\blenderbim\bim\module\project\operator.py", line 1239, in execute
    db = ifcpatch.execute(
         ^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcpatch\__init__.py", line 80, in execute
    patcher.patch()
  File "\blenderbim\libs\site\packages\ifcpatch\recipes\ExtractPropertiesToSQLite.py", line 137, in patch
    properties.append([i, "IFC Material", f"Layer {idx + 1} Name", item.Name])
                                                                   ^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\entity_instance.py", line 197, in __getattr__
    raise AttributeError(
AttributeError: entity instance of type 'IFC2X3.IfcMaterialLayer' has no attribute 'Name'
Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\project\operator.py", line 1239, in execute
    db = ifcpatch.execute(
         ^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcpatch\__init__.py", line 80, in execute
    patcher.patch()
  File "\blenderbim\libs\site\packages\ifcpatch\recipes\ExtractPropertiesToSQLite.py", line 137, in patch
    properties.append([i, "IFC Material", f"Layer {idx + 1} Name", item.Name])
                                                                   ^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\entity_instance.py", line 197, in __getattr__
    raise AttributeError(
AttributeError: entity inTraceback (most recent call last):
  File "\Temp\tmpdggt3syt.py", line 9, in <module>
    run()
  File "\Temp\tmpdggt3syt.py", line 5, in run
    bpy.ops.bim.load_linked_project(filepath="/Dormitory-ARC.ifc", false_origin="0,0,0")
  File "\Blender\4.1\scripts\modules\bpy\ops.py", line 109, in __call__
    ret = _op_call(self.idname_py(), kw)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\project\operator.py", line 1239, in execute
    db = ifcpatch.execute(
         ^^^^^^^^^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcpatch\__init__.py", line 80, in execute
    patcher.patch()
  File "\blenderbim\libs\site\packages\ifcpatch\recipes\ExtractPropertiesToSQLite.py", line 137, in patch
    properties.append([i, "IFC Material", f"Layer {idx + 1} Name", item.Name])
                                                                   ^^^^^^^^^
  File "\blenderbim\libs\site\packages\ifcopenshell\entity_instance.py", line 197, in __getattr__
    raise AttributeError(
AttributeError: entity instance of type 'IFC2X3.IfcMaterialLayer' has no attribute 'Name'
Location: \Blender\4.1\scripts\modules\bpy\ops.py:109

... truncatedUnregistered Snippets Library
BMAX Connector - UnRegistred!
An error occurred while processing your IFC.
Traceback (most recent call last):
  File "\blenderbim\bim\module\project\operator.py", line 949, in execute
    self.link_ifc()
  File "\blenderbim\bim\module\project\operator.py", line 991, in link_ifc
    self.link_blend(blend_filepath)
  File "\blenderbim\bim\module\project\operator.py", line 953, in link_blend
    with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
OSError: load: \Dormitory-ARC.ifc.cache.blend failed to open blend file
Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\project\operator.py", line 949, in execute
    self.link_ifc()
  File "\blenderbim\bim\module\project\operator.py", line 991, in link_ifc
    self.link_blend(blend_filepath)
  File "\blenderbim\bim\module\project\operator.py", line 953, in link_blend
    with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
OSError: load: \Dormitory-ARC.ifc.cache.blend failed to open blend file
Location: \Blender\4.1\scripts\modules\bpy\ops.py:109
Traceback (most recent call last):
  File "\blenderbim\bim\module\project\operator.py", line 870, in execute
    bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
  File "\Blender\4.1\scripts\modules\bpy\ops.py", line 109, in __call__
    ret = _op_call(self.idname_py(), kw)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\module\project\operator.py", line 949, in execute
    self.link_ifc()
  File "\blenderbim\bim\module\project\operator.py", line 991, in link_ifc
    self.link_blend(blend_filepath)
  File "\blenderbim\bim\module\project\operator.py", line 953, in link_blend
    with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
OSError: load: \Dormitory-ARC.ifc.cache.blend failed to open blend file
Location: \Blender\4.1\scripts\modules\bpy\ops.py:109
```
2024-05-01 18:26:58 +05:00
Andrej730 523e51c775 use json serialization for #4599
changed my mind about 5410f14aa, probably better to use `json.dumps` to keep it consistent with how we serialize other attributes
2024-05-01 18:26:58 +05:00
Andrej730 5410f14aa1 ifc2sql - stringify psets list values #4599 2024-05-01 18:03:25 +05:00
Andrej730 45b2411c2b Fix #4598 after 229c7285c 2024-05-01 17:43:31 +05:00
Andrej730 9f12091c7f fix bug assigning materials
it was clearing all other blender materials - e.g. blender materials associated with representation items styles. Also had the same issue with affecting extra elements as with unassigning materials
2024-05-01 16:42:20 +05:00
Andrej730 bef5d3cca5 blenderbim - fix bug with unassigning material
When you would unassign material (e.g. implicitly by removing the object), it might have also removed materials slots from other occurrences of the same type.

Which may had some unexpected sideffects later on - as unassigned styles saving IFC project (BBIM unassigned styles based on a assumption that it was the user decision to remove the materials and related styles).
2024-05-01 16:42:20 +05:00
Andrej730 0f548eb93e Indicate in UI if type material was overridden by occurrence material
Example - https://i.imgur.com/T1hidJg.png

+small optimization in material_name
2024-05-01 16:42:20 +05:00
Andrej730 374348bb81 IfcImporter small optimization 2024-05-01 16:42:20 +05:00
Andrej730 b42f6b1827 ifc delete to show time it took if it was more than 10 secs 2024-05-01 16:42:20 +05:00
Andrej730 addcf531f0 owner.update_owner_history - optimization
as it may be used very often
2024-05-01 16:42:20 +05:00
Andrej730 0ef683013a RegenerateGlobalIds - more informative fixing duplicates 2024-05-01 16:42:20 +05:00
Andrej730 137a306269 typing 2024-05-01 16:42:20 +05:00
Andrej730 fc0ffd1c02 fix a typo #4596 😬 2024-05-01 15:40:45 +05:00
Bruno Perdigão 04172a87cb when bpy.ops.bim.object_duplicate_move_linked_aggregate is called from the panel, the new objects are placed based on the 3d cursor position 2024-04-30 17:50:31 -03:00
Bruno Perdigão 97e0c43702 Merge pull request #4440 from brunoperdigao/fix-normal-duplication-of-linked-aggregate
Fix normal duplication of linked aggregate
2024-04-30 17:01:44 -03:00
Andrej730 8dde98f543 fix #4592 (after 0da3270)
Before IFC4 Shape Aspects counldn't be part of IfcRepresentationMap
2024-04-29 15:22:26 +05:00
Ryan Schultz 9962a0c905 small fix to previous commit. Solves https://github.com/IfcOpenShell/IfcOpenShell/commit/4b56b341243d6b0eed82e8533848b124e82e1344#commitcomment-141442796 2024-04-28 18:57:42 -05:00
Ryan Schultz 4b56b34124 Tweaked UI for Linked Aggregates https://imgur.com/a/rExISKO 2024-04-28 10:34:52 -05:00
Thomas Krijnen f857c513c5 submodule 2024-04-28 10:17:57 +02:00
Kaare Hansen 41df6410bd Comment std::cout causing console spam 2024-04-28 10:11:32 +02:00
Ryan Schultz dcbd6a925d small fix for #4361. remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615 2024-04-27 18:13:38 -05:00
Ryan Schultz 3848a21136 fix #4361: Have the aggregate empty turn on too, if at least one object in the aggregate, is turned on as well 2024-04-27 11:31:39 -05:00
Andrej730 8af37cb4ec Fix #4586 occurred after e14d96e6e 2024-04-27 12:03:56 +05:00
Ryan Schultz 44c28e9a4c fix #4327: reorder format as well 2024-04-26 20:48:59 -05:00
Ryan Schultz 677324927c fix #4431: prompt for name, when creating aggregate 2024-04-26 20:09:20 -05:00
Massimo Fabbro d212344c54 Covering tool more user friendly UI ... 2024-04-26 17:19:28 +02:00
Massimo Fabbro 74da327803 Fix create space from cursor bug where spaces were created always in zero elevation 2024-04-26 17:19:28 +02:00
Massimo Fabbro f021041b31 Remove the (maybe) unnecessary poll functions for spatial tool and cleaner error messages 2024-04-26 17:19:28 +02:00
Massimo Fabbro c05d2df116 now spaces created with spatial tool have relative zero elevation origin point 2024-04-26 17:19:28 +02:00
Andrej730 2b926f7906 more descriptive error setting non-optional attributes with None
Example:

```
import ifcopenshell
import ifcopenshell.api.owner.settings

ifc_file = ifcopenshell.file(schema="IFC2X3")
ifc_file.createIfcOwnerHistory(OwningUser=None)

# Before:
# Traceback (most recent call last):
#   File "\test.py", line 16, in <module>
#     ifcopenshell.api.run("owner.create_owner_history", ifc_file)
#   File "\ifcopenshell\api\__init__.py", line 172, in run
#     result = usecase_class(ifc_file, **settings).execute()
#              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\api\owner\create_owner_history.py", line 111, in execute
#     return self.file.create_entity(
#            ^^^^^^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\file.py", line 350, in create_entity
#     e[idx] = arg
#     ~^^^^^
#   File "\ifcopenshell\entity_instance.py", line 305, in __setitem__
#     self.wrapped_data.setArgumentAsNull(idx)
#   File "\ifcopenshell\ifcopenshell_wrapper.py", line 5285, in setArgumentAsNull
#     return _ifcopenshell_wrapper.entity_instance_setArgumentAsNull(self, i)
#            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# RuntimeError: Attribute not set

# After:
# Traceback (most recent call last):
#   File "\test.py", line 16, in <module>
#     ifcopenshell.api.run("owner.create_owner_history", ifc_file)
#   File "\ifcopenshell\api\__init__.py", line 172, in run
#     result = usecase_class(ifc_file, **settings).execute()
#              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\api\owner\create_owner_history.py", line 111, in execute
#     return self.file.create_entity(
#            ^^^^^^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\file.py", line 350, in create_entity
#     e[idx] = arg
#     ~^^^^^
#   File "\ifcopenshell\entity_instance.py", line 316, in __setitem__
#     raise ValueError(
# ValueError: attribute 'OwningUser' is not optional for entity instance of type 'IFC2X3.IfcOwnerHistory'
```
2024-04-26 18:23:19 +05:00
Andrej730 3ac69043d6 bbim to provide default user and organisation in ifc2x3 #4574
previously every operation that would try to create owner history would fail with the error below if there wasn't IfcPersonAndOrganisation in the project.
2024-04-26 18:23:19 +05:00
Andrej730 f726708815 edit_object_placement - small optimizations 2024-04-26 18:23:19 +05:00
Andrej730 21e153087a edit_object_placement - adherence support
Update edit_object_placement.py
2024-04-26 18:23:18 +05:00
Andrej730 31d85b715a typing 2024-04-26 18:23:18 +05:00
Andrej730 344cc1fb7d file.by_type docs note 2024-04-26 11:49:44 +05:00
Andrej730 1cea3d21e8 BBIM support importing IfcRelAdheresToElement #4565 2024-04-26 11:44:32 +05:00
Andrej730 640320f70d shape_builder - create non-optional position for profiles in ifc2x3 2024-04-25 18:05:47 +05:00
Andrej730 29352092aa fix issue creating 3d location for 2d placement in ifc2x3 2024-04-25 18:05:47 +05:00
Andrej730 5b877b549a fix numpy typing #4579
On older numpy versions, np.ndarray was less forgiving and wasn't allowing passing 1 argument instead of required 2.

And turned out numpy doesn't yet have typing for shapes (https://github.com/numpy/numpy/issues/16544), so all matrices and other shapes specified as `npt.NDArray[np.float64]`.

Fixed type discrepancies for `get_edges` and `get_faces` and also had to fix `import_ifc` as Blender apparently has problems with storing np.int32 in custom attributes (https://projects.blender.org/blender/blender/issues/121072), tested that Blender is okay with np.int32 in other cases we had (addressing BMesh.verts[i] where `i` is np.int32).
2024-04-25 18:05:47 +05:00
Andrej730 721466c429 typing 2024-04-25 18:05:47 +05:00
Andrej730 cce2fee1fa More descriptive error setting non-present attributes
Example:

```
import ifcopenshell

ifc_file = ifcopenshell.file(schema="IFC2X3")
# ifc_file.begin_transaction()

wall = ifc_file.createIfcWall()
wall.Identification = "25"

# Before:
# Traceback (most recent call last):
#   File "test.py", line 4, in <module>
#     wall.Identification = "25"
#     ^^^^^^^^^^^^^^^^^^^
#   File "\ifcopenshell\entity_instance.py", line 279, in __setattr__
#     self[index] = value
#     ~~~~^^^^^^^
#   File "\ifcopenshell\entity_instance.py", line 293, in __setitem__
#     method = self.method_list[idx]
#              ~~~~~~~~~~~~~~~~^^^^^
# IndexError: list index out of range

# or this (if file had a transaction going)
#   File "\ifcopenshell\entity_instance.py", line 279, in __setattr__
#     self[index] = value
#     ~~~~^^^^^^^
#   File "\ifcopenshell\entity_instance.py", line 288, in __setitem__
#     self.wrapped_data.file.transaction.store_edit(self, idx, value)
#   File "\ifcopenshell\file.py", line 101, in store_edit
#     "old": self.serialise_value(element, element[index]),
#                                          ~~~~~~~^^^^^^^
#   File "\ifcopenshell\entity_instance.py", line 283, in __getitem__
#     raise IndexError("Attribute index {} out of range for instance of type {}".format(key, self.is_a()))
# IndexError: Attribute index 4294967295 out of range for instance of type IfcWall

# After:
# Traceback (most recent call last):
#   File "test.py", line 4, in <module>
#     wall.Identification = "25"
#     ^^^^^^^^^^^^^^^^^^^
#   File "ifcopenshell\entity_instance.py", line 283, in __setattr__
#     raise AttributeError(
# AttributeError: entity instance of type 'IFC2X3.IfcWall' has no attribute 'Identification'

```
2024-04-25 18:05:47 +05:00
Andrej730 087d55f02a fix editing sheets in ifc2x3 #4576 2024-04-25 18:05:47 +05:00
Andrej730 f1037de14d small fix for 60a70f523 2024-04-25 18:05:46 +05:00
Ryan Schultz 4711672170 small tweak to 3ff31b577c and cd013079f4 - duplicate material and duplicate style have '_copy' suffix. 2024-04-24 21:48:26 -05:00
Bruno Postle c0b4e099af fix sheet regeneration missing drawing Identifier
Was passing LAYOUT document reference to sheeter instead of
DRAWING document reference
2024-04-24 23:05:44 +01:00
Andrej730 f41fbf24c3 small fix for e14d96e6e 2024-04-24 18:19:20 +05:00
Andrej730 e14d96e6ee Use .Name instead of .Description if IFC2X3 for sheets #4576
As Description was added only in IFC4
2024-04-24 18:17:37 +05:00
Andrej730 716a33f347 convert_file_length_units - fix issue converting to imperial units
ifcpatch ConvertLengthUnit - change used units from plural to singular names, to make it consistent across the api

more test coverage - test converting to more units and back from them
2024-04-24 15:15:30 +05:00
Andrej730 c5f084a4af ConvertLengthUnit test 2024-04-24 14:12:13 +05:00
Andrej730 606a0b46f2 MergeProject - support merging projects with different units 2024-04-24 14:12:13 +05:00
Andrej730 1c729a74d1 util.unit to support converting aggregate of aggregates 2024-04-24 14:12:13 +05:00
Andrej730 db31574103 typing 2024-04-24 14:12:13 +05:00
stefkeB d03b47e1e6 Fix HDF5 conditional inclusion in GeomTree
IfcGeomTree uses HDF5, but this is an optional module, so we have to wrap it with #ifdef WITH_HDF5
2024-04-23 18:46:10 +02:00
Ryan Schultz ae62ac2777 small tweak to b8a9674483 - duplicate profile has '_copy' suffix. 2024-04-23 07:42:02 -05:00
Andrej730 998f9c6a1e make styles ui more similar to profiles and materials ui
before - https://i.imgur.com/bTbyt2t.png
after - https://i.imgur.com/U53h8Yh.png
2024-04-23 16:39:49 +05:00
Andrej730 3ff31b577c bim.duplicate_style 2024-04-23 16:39:49 +05:00
Andrej730 cd013079f4 bim.duplicate_material 2024-04-23 16:39:49 +05:00
Andrej730 b8a9674483 bim.duplicate_profile
simple operator for profile duplication
example - https://imgur.com/a/dsP78iV
2024-04-23 16:39:49 +05:00
Andrej730 8beb1bef98 preserve selected profile index removing profiles 2024-04-23 16:39:38 +05:00
Andrej730 377676f881 dev environment - add libs/desktop and bsdd 2024-04-23 15:23:16 +05:00
Andrej730 4efe284338 use IFC4X3_ADD2 for ifcopenshell.schema_by_name
as it is the main IFC4X3 schema
2024-04-22 17:30:25 +05:00
Andrej730 34a4e3f37a util.doc - use release schema version for IFC4X3
As it will be more like to be available in the ifcopenshell package. Related to #4565
2024-04-22 17:30:25 +05:00
Andrej730 889c9a9e9f typing 2024-04-22 17:30:25 +05:00
Andrej730 9e79499532 small optimization 2024-04-22 17:30:24 +05:00
Kristof Semjen 589b98053e Fixes #4261
SWIG_Python_str_AsChar and SWIG_Python_str_DelForPy3 are no longer available in swig 4.2 (see : https://github.com/swig/swig/commit/f89dd59d4b82ece899087682fdb86e94d2611513 ),
this commit fixes the build for swig versions > 4.2.
2024-04-21 16:22:56 +02:00
Bruno Postle fc5bb230a9 Abandon SVG layout generation on missing drawings
Drawings need to be generated before layout can be generated, so
remove a partial failed layout caused by missing drawings.
2024-04-21 11:12:24 +01:00
Bruno Perdigão 01361cc8f2 Merge branch 'v0.7.0' into fix-normal-duplication-of-linked-aggregate 2024-04-18 21:00:39 -03:00
Bruno Perdigão e6bef16c45 small deletions 2024-03-18 23:39:06 -03:00
Bruno Perdigão 4829c4c02b fix: when subaggregate that is a linked aggregate is unassinged from the main aggregate, it remains a linked aggregate. 2024-03-16 15:27:04 -03:00
Bruno Perdigão 3bf4a8ef52 small refactor 2024-03-16 12:44:26 -03:00
Bruno Perdigão c95a47ca97 fix #4430. Shift+D whem selecting all the objects of a linked aggregate creates a normal copy that is not linked 2024-03-15 21:34:27 -03:00
684 changed files with 20561 additions and 16286 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)
+10 -31
View File
@@ -151,8 +151,10 @@ endif
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.7.0.zip
cd dist/working && unzip v0.7.0.zip
# IfcOpenBot sometimes lags behind, so we hotfix the Python utilities
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/util/
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util dist/blenderbim/libs/site/packages/ifcopenshell/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/*.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py dist/blenderbim/libs/site/packages/ifcopenshell/express/
# Provides bcf functionality
@@ -275,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
@@ -364,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
+159 -6
View File
@@ -18,7 +18,13 @@
import os
import sys
import site
import bpy
import platform
import traceback
import subprocess
import webbrowser
import addon_utils
from collections import deque
bl_info = {
"name": "BlenderBIM",
@@ -32,16 +38,163 @@ bl_info = {
"category": "System",
}
last_error = None
last_actions: deque = deque(maxlen=10)
def get_debug_info():
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
return {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"last_actions": last_actions,
"last_error": last_error,
}
def format_debug_info(info: dict):
last_actions = ""
for action in info["last_actions"]:
last_actions += f"\n# {action['type']}: {action['name']}"
if settings := action.get("settings"):
last_actions += f"\n>>> {settings}"
info["last_actions"] = last_actions
text = "\n".join(f"{k}: {v}" for k, v in info.items())
return text.strip()
if sys.modules.get("bpy", None):
# Process *.pth in /libs/site/packages to setup globally importable modules
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
import blenderbim.bim
try:
import blenderbim.bim
import ifcopenshell.api
def register():
blenderbim.bim.register()
def log_api(usecase_path, ifc_file, settings):
last_actions.append(
{
"type": "ifcopenshell.api",
"name": usecase_path,
"settings": ifcopenshell.api.serialise_settings(settings),
}
)
def unregister():
blenderbim.bim.unregister()
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def register():
blenderbim.bim.register()
def unregister():
blenderbim.bim.unregister()
except:
last_error = traceback.format_exc()
print(last_error)
print(format_debug_info(get_debug_info()))
print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on")
class BIM_PT_fatal_error(bpy.types.Panel):
bl_label = "BlenderBIM Fatal Error"
bl_idname = "SCENE_PT_error_message"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
def draw(self, context):
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")
box.label(text=f"Python {py} BBIM {info['blenderbim_version']}", icon="SCRIPTPLUGINS")
layout.operator("bim.copy_debug_information", text="Copy Error Message To Clipboard")
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"
uri: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.uri)
return {"FINISHED"}
class CopyDebugInformation(bpy.types.Operator):
bl_idname = "bim.copy_debug_information"
bl_label = "Copy Debug Information"
bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context):
info = format_debug_info(get_debug_info())
context.window_manager.clipboard = info
return {"FINISHED"}
class HiddenPanel:
@classmethod
def false_poll(cls, context):
return False
def register():
# Only show our error panel and nothing else in the scene tab
for item_name in dir(bpy.types):
item = getattr(bpy.types, item_name)
if not hasattr(item, "bl_rna") or not isinstance(item.bl_rna, bpy.types.Panel):
continue
if getattr(item, "bl_context", None) != "scene":
continue
# Reregister scene panel with a new poll to hide it
item.poll = HiddenPanel.false_poll
bpy.utils.unregister_class(item)
bpy.utils.register_class(item)
bpy.utils.register_class(BIM_PT_fatal_error)
bpy.utils.register_class(CopyDebugInformation)
bpy.utils.register_class(OpenUri)
def unregister():
bpy.utils.unregister_class(OpenUri)
bpy.utils.unregister_class(CopyDebugInformation)
bpy.utils.unregister_class(BIM_PT_fatal_error)
+2 -1
View File
@@ -17,11 +17,11 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
from pathlib import Path
import bpy
import bpy.utils.previews
import blenderbim
import importlib
from pathlib import Path
from . import handler, ui, prop, operator, helper
from typing import Callable, Union
@@ -103,6 +103,7 @@ classes = [
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.ClippingPlaneCutWithCappings,
operator.CloseError,
operator.EditBlenderCollection,
operator.FileAssociate,
operator.FileUnassociate,
@@ -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'; }
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -5,20 +5,20 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#6,#7));
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/BATTING,IfcTypeProduct',(#6,#7));
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#9,#10,#11,#12,#13));
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/SECTION,IfcTypeProduct',(#9,#10,#11,#12,#13));
#9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('2$6U0mLI9AiPRWeBabdY3u',$,'EndArrowSymbol','Custom symbol for the end of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('1naFqntIL7igCY7hCaE7kq',$,'HasConnectedSectionLine','Connect or disconnect section markers with line (by default = True).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#15=IFCSIMPLEPROPERTYTEMPLATE('0ulAhgk3v9qfGlDILauR6J',$,'SheetsDir','Default sheets directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#16=IFCSIMPLEPROPERTYTEMPLATE('2yvlVKiQXASucfH40deCvu',$,'LayoutsDir','Default layouts directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#17=IFCSIMPLEPROPERTYTEMPLATE('2kXZqXicL3jRwsOnLM_0ho',$,'TitleblocksDir','Default titleblocks directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -28,7 +28,7 @@ DATA;
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#25,#26,#27,#28));
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
+9 -5
View File
@@ -16,6 +16,7 @@
# 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
import os
import bpy
import json
@@ -36,6 +37,7 @@ import blenderbim.core.style
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector
from typing import Union
from logging import Logger
class IfcExporter:
@@ -163,10 +165,10 @@ class IfcExporter:
bpy.ops.bim.update_representation(obj=obj.name)
tool.Geometry.record_object_position(obj)
def get_application_name(self):
def get_application_name(self) -> str:
return "BlenderBIM"
def get_application_version(self):
def get_application_version(self) -> str:
version = ".".join(
[
str(x)
@@ -184,11 +186,13 @@ class IfcExporter:
class IfcExportSettings:
def __init__(self):
self.logger = None
self.output_file = None
self.logger: Logger = None
self.output_file: str = None
self.json_version: str = None
self.json_compact: bool = None
@staticmethod
def factory(context, output_file, logger):
def factory(context: bpy.types.Context, output_file: str, logger: Logger) -> IfcExportSettings:
settings = IfcExportSettings()
settings.output_file = output_file
settings.logger = logger
+16 -3
View File
@@ -223,7 +223,7 @@ def redo_post(scene):
tool.Ifc.rebuild_element_maps()
def get_application(ifc):
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
version = get_application_version()
for element in ifc.by_type("IfcApplication"):
@@ -238,7 +238,20 @@ def get_application(ifc):
)
def get_application_version():
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
if pao := next(iter(ifc.by_type("IfcPersonAndOrganization")), None):
return pao
elif ifc.schema == "IFC2X3":
if (person := next(iter(ifc.by_type("IfcPerson")), None)) is None:
person = tool.Ifc.run("owner.add_person")
if (organization := next(iter(ifc.by_type("IfcOrganization")), None)) is None:
organization = tool.Ifc.run("owner.add_organisation")
pao = tool.Ifc.run("owner.add_person_and_organisation", person=person, organisation=organization)
return pao
def get_application_version() -> str:
return ".".join(
[
str(x)
@@ -279,7 +292,7 @@ def load_post(scene):
key=key, owner=global_subscription_owner, args=(area,), notify=viewport_shading_changed_callback
)
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
ifcopenshell.api.owner.settings.get_user = get_user
ifcopenshell.api.owner.settings.get_application = get_application
AuthoringData.type_thumbnails = {}
+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}
+26 -12
View File
@@ -19,12 +19,15 @@
import os
import bpy
import uuid
import shutil
import hashlib
import zipfile
import tempfile
import traceback
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper
import blenderbim
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
@@ -37,19 +40,19 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
class IfcStore:
path: str = ""
file: ifcopenshell.file = None
schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None
cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None
cache_path: str = None
file: Optional[ifcopenshell.file] = None
schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None
cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None
cache_path: Optional[str] = None
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
edited_objs: Set[bpy.types.Object] = set()
pset_template_path: str = ""
pset_template_file: ifcopenshell.file = None
pset_template_file: Optional[ifcopenshell.file] = None
classification_path: str = ""
classification_file: ifcopenshell.file = None
classification_file: Optional[ifcopenshell.file] = None
library_path: str = ""
library_file: ifcopenshell.file = None
library_file: Optional[ifcopenshell.file] = None
current_transaction = ""
last_transaction = ""
history = []
@@ -119,7 +122,13 @@ class IfcStore:
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
IfcStore.cache = None
os.replace(IfcStore.cache_path, new_cache_path)
try:
shutil.move(IfcStore.cache_path, new_cache_path)
except PermissionError:
try:
shutil.copy2(IfcStore.cache_path, new_cache_path)
except PermissionError:
pass # Well we tried. No cache for you!
IfcStore.get_cache()
@staticmethod
@@ -329,6 +338,7 @@ class IfcStore:
@staticmethod
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False):
blenderbim.last_actions.append({"type": "operator", "name": operator.bl_idname})
bpy.context.scene.BIMProperties.is_dirty = True
is_top_level_operator = not bool(IfcStore.current_transaction)
@@ -343,10 +353,14 @@ class IfcStore:
else:
operator.transaction_key = IfcStore.current_transaction
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
try:
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
except:
blenderbim.last_error = traceback.format_exc()
raise
if is_top_level_operator:
if tool.Ifc.get():
+84 -51
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
@@ -34,6 +35,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import blenderbim.tool as tool
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from itertools import chain, accumulate
@@ -199,7 +201,7 @@ class MaterialCreator:
class IfcImporter:
def __init__(self, ifc_import_settings):
def __init__(self, ifc_import_settings: IfcImportSettings):
self.ifc_import_settings = ifc_import_settings
self.diff = None
self.file: ifcopenshell.file = None
@@ -298,25 +300,26 @@ class IfcImporter:
if self.ifc_import_settings.should_setup_viewport_camera:
self.setup_viewport_camera()
self.setup_arrays()
self.profile_code("Setup arrays")
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):
@@ -430,7 +433,7 @@ class IfcImporter:
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement")]
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
if self.ifc_import_settings.is_coordinating:
self.elements = [e for e in self.elements if e.Representation]
@@ -601,9 +604,14 @@ class IfcImporter:
return products
def predict_dense_mesh(self):
if self.ifc_import_settings.should_use_native_meshes:
return
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
@@ -611,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
@@ -674,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
@@ -697,17 +707,19 @@ class IfcImporter:
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
point = np.array(
point = mat @ np.array(
(
shape.geometry.verts[0] / self.unit_scale,
shape.geometry.verts[1] / self.unit_scale,
shape.geometry.verts[2] / self.unit_scale,
shape.geometry.verts[0],
shape.geometry.verts[1],
shape.geometry.verts[2],
0.0,
)
)
return mat @ point
point = point / self.unit_scale
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:
@@ -724,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, matrix):
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,
@@ -743,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
@@ -862,7 +877,6 @@ class IfcImporter:
else:
self.create_generic_elements(self.spatial_elements, unselectable=False)
def create_elements(self) -> None:
self.create_generic_elements(self.elements)
tmp = self.context_settings
@@ -953,7 +967,9 @@ class IfcImporter:
self.create_product(element, mesh=mesh)
def create_products(
self, products, settings: Optional[ifcopenshell.geom.main.settings] = None
self,
products: set[ifcopenshell.entity_instance],
settings: Optional[ifcopenshell.geom.main.settings] = None,
) -> set[ifcopenshell.entity_instance]:
results = set()
if not products:
@@ -1492,7 +1508,10 @@ class IfcImporter:
# Occurs when reloading a project
pass
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
project_collection.children[self.type_collection.name].hide_viewport = True
types_collection = project_collection.children[self.type_collection.name]
types_collection.hide_viewport = False
for obj in types_collection.collection.objects: # turn off all objects inside Types collection.
obj.hide_set(True)
def clean_mesh(self):
obj = None
@@ -1627,30 +1646,34 @@ class IfcImporter:
if self.ifc_import_settings.has_filter:
rel_aggregates = set()
for element in self.elements:
if element.IsDecomposedBy:
rel_aggregates.add(element.IsDecomposedBy[0])
elif element.Decomposes:
rel_aggregates.add(element.Decomposes[0])
elif getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if [e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")]:
rel_aggregates.add(element.IsNestedBy[0])
elif getattr(element, "Nests", []):
rel_aggregates.add(element.Nests[0])
if decomposed_by := element.IsDecomposedBy:
rel_aggregates.add(decomposed_by[0])
elif decomposes := element.Decomposes:
rel_aggregates.add(decomposes[0])
elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None):
rel_aggregates.add(nested_by[0])
elif nests := getattr(element, "Nests", []):
rel_aggregates.add(nests[0])
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
rel_aggregates.add(element.AdheresToElement[0])
else:
rel_aggregates = [
r
for r in self.file.by_type("IfcRelAggregates")
if r.RelatingObject.is_a("IfcElement") or r.RelatingObject.is_a("IfcElementType")
if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType")
] + [
r
for r in self.file.by_type("IfcRelNests")
if (
r.RelatingObject.is_a("IfcElement")
or r.RelatingObject.is_a("IfcElementType")
or (r.RelatingObject.is_a("IfcPositioningElement") and not r.RelatingObject.is_a("IfcGrid"))
(relating_obj := r.RelatingObject).is_a("IfcElement")
or relating_obj.is_a("IfcElementType")
or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid"))
)
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
]
if self.file.schema == "IFC4X3":
rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")]
if len(rel_aggregates) > 10000:
# More than 10,000 collections makes Blender unhappy
@@ -1660,7 +1683,9 @@ class IfcImporter:
aggregates: dict[str, dict] = {}
for rel_aggregate in rel_aggregates:
element: ifcopenshell.entity_instance = rel_aggregate.RelatingObject
element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr(
rel_aggregate, "RelatingElement"
)
collection = bpy.data.collections.new(tool.Loader.get_name(element))
aggregates[element.GlobalId] = {"element": element, "collection": collection}
self.collections[element.GlobalId] = collection
@@ -1761,6 +1786,9 @@ class IfcImporter:
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
nest = ifcopenshell.util.element.get_nest(element)
return self.collections[nest.GlobalId].objects.link(obj)
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
adherend = element.AdheresToElement[0].RelatingElement
return self.collections[adherend.GlobalId].objects.link(obj)
return self.place_object_in_spatial_decomposition_collection(element, obj)
@@ -1810,7 +1838,7 @@ class IfcImporter:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
return rel.RelatingGroup
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.array:
def get_element_matrix(self, element: ifcopenshell.entity_instance) -> np.ndarray:
if isinstance(element, ifcopenshell.sqlite_entity):
result = self.geometry_cache["shapes"][element.id()]["matrix"]
else:
@@ -1893,11 +1921,7 @@ class IfcImporter:
and geometry.verts
and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2]))
):
m = shape.transformation.matrix.data
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
offset_point = np.linalg.inv(mat) @ np.array(
offset_point = np.array(
(
float(props.blender_eastings),
float(props.blender_northings),
@@ -1905,8 +1929,15 @@ class IfcImporter:
0.0,
)
)
if geometry != shape:
m = shape.transformation.matrix.data
mat = np.array(
([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1])
)
offset_point = np.linalg.inv(mat) @ offset_point
verts = [None] * len(geometry.verts)
for i in range(0, len(geometry.verts), 3):
# Note: this enh2xyz call is crazy slow.
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
geometry.verts[i],
geometry.verts[i + 1],
@@ -1932,7 +1963,9 @@ class IfcImporter:
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
#
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
@@ -1959,14 +1992,14 @@ class IfcImporter:
print(traceback.format_exc())
def a2p(self, o, z, x):
def a2p(self, o: mathutils.Vector, z: mathutils.Vector, x: mathutils.Vector) -> mathutils.Matrix:
y = z.cross(x)
r = mathutils.Matrix((x, y, z, o))
r.resize_4x4()
r.transpose()
return r
def get_axis2placement(self, plc):
def get_axis2placement(self, plc: ifcopenshell.entity_instance) -> mathutils.Matrix:
if plc.is_a("IfcAxis2Placement3D"):
z = mathutils.Vector(plc.Axis.DirectionRatios if plc.Axis else (0, 0, 1))
x = mathutils.Vector(plc.RefDirection.DirectionRatios if plc.RefDirection else (1, 0, 0))
@@ -1986,7 +2019,7 @@ class IfcImporter:
o = plc.LocalOrigin.Coordinates
return self.a2p(o, z, x)
def get_local_placement(self, plc):
def get_local_placement(self, plc: Optional[ifcopenshell.entity_instance] = None) -> mathutils.Matrix:
if plc is None:
return mathutils.Matrix()
if plc.PlacementRelTo is None:
@@ -2001,11 +2034,11 @@ class IfcImporter:
bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
break
def link_element(self, element, obj):
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
self.added_data[element.id()] = obj
tool.Ifc.link(element, obj)
def set_matrix_world(self, obj, matrix_world):
def set_matrix_world(self, obj: bpy.types.Object, matrix_world: mathutils.Matrix) -> None:
obj.matrix_world = matrix_world
tool.Geometry.record_object_position(obj)
@@ -2028,7 +2061,7 @@ class IfcImporter:
class IfcImportSettings:
def __init__(self):
self.logger = None
self.logger: logging.Logger = None
self.input_file = None
self.diff_file = None
self.should_use_cpu_multiprocessing = True
@@ -96,12 +96,13 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
)
# Removes Pset related to Linked Aggregates
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)
if not element.is_a('IfcElementAssembly'):
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(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
"""Enable editing aggregation relationship"""
@@ -132,6 +133,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -139,13 +141,15 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
row = self.layout
row.prop(self, "ifc_class")
row = self.layout
row.prop(self, "aggregate_name")
def _execute(self, context):
try:
ifc_class = tool.Ifc.schema().declaration_by_name(self.ifc_class).name()
except:
return
aggregate = self.create_aggregate(context, ifc_class)
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -173,8 +177,8 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context, ifc_class):
aggregate = bpy.data.objects.new("Assembly", None)
def create_aggregate(self, context, ifc_class, aggregate_name):
aggregate = bpy.data.objects.new(aggregate_name, None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
return aggregate
@@ -20,6 +20,7 @@ from bpy.types import Panel
from blenderbim.bim.module.aggregate.data import AggregateData
from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
class BIM_PT_aggregate(Panel):
@@ -72,7 +73,7 @@ class BIM_PT_aggregate(Panel):
row.operator("bim.add_aggregate", icon="ADD", text="")
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
else:
row.label(text="No Aggregate", icon="TRIA_UP")
row.label(text="No Whole relation defined", icon="TRIA_UP")
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -131,22 +132,29 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
props = context.active_object.BIMObjectAggregateProperties
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
row = layout.row(align=True)
row.label(text="Advanced Users Only", icon="ERROR")
row = layout.row(align=True)
if type(AggregateData.data['total_linked_aggregate']) is int:
if AggregateData.data['total_linked_aggregate'] > 0:
row.label(text=f"{AggregateData.data['total_linked_aggregate']} Linked Aggregates")
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
op.select_parts = False
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
op.select_parts = True
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
if element.Decomposes:
Number_Linked_Aggregates = AggregateData.data['total_linked_aggregate']
if not Number_Linked_Aggregates:
row.label(text="Not a Linked Aggregate")
else:
row.label(text=f"{Number_Linked_Aggregates} Linked Aggregates")
op = row.operator("bim.duplicate_linked_aggregate_to_3d_cursor", text="", icon="DUPLICATE")
if type(Number_Linked_Aggregates) is int:
if Number_Linked_Aggregates > 0:
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
op.select_parts = False
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
op.select_parts = True
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
else:
row.label(text="No Linked Aggregates")
row.label(text="Not an Aggregate")
@@ -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
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
@@ -39,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()
@@ -84,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"}
@@ -101,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)
@@ -125,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,34 +19,34 @@
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:
row = layout.row(align=True)
row.label(text=attribute["name"])
row.label(text=attribute["value"])
# row.label(text=attribute["value"])
op = row.operator("bim.select_similar", text=attribute["value"], icon="NONE", emboss=False)
op.key = attribute['name']
# TODO: reimplement, see #1222
# if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name:
@@ -72,32 +72,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"])
@@ -20,6 +20,7 @@ import bpy
import bmesh
import logging
import shapely
import shapely.ops
import mathutils
import numpy as np
import multiprocessing
@@ -209,6 +209,10 @@ class DisableEditingClassification(bpy.types.Operator):
class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_classification"
bl_label = "Remove Classification"
bl_description = (
"The classification and all of its relationships, children references, "
"and relationships between objects and child references will be completely removed from a project"
)
bl_options = {"REGISTER", "UNDO"}
classification: bpy.props.IntProperty()
@@ -53,6 +53,10 @@ class AddContext(bpy.types.Operator, Operator):
class RemoveContext(bpy.types.Operator, Operator):
bl_idname = "bim.remove_context"
bl_label = "Remove Context"
bl_description = (
"Remove representation context. Any representation geometry that is assigned to the context is also removed. "
"If a context is removed, then any subcontexts are also removed"
)
bl_options = {"REGISTER", "UNDO"}
context: bpy.props.IntProperty()
@@ -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:
@@ -107,59 +107,88 @@ class CoveringToolUI:
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
if (type_material_usage == "IfcMaterialLayerSet" and
not bpy.context.selected_objects):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
if tool.Ifc.get_entity(collection_obj):
if AuthoringData.data["predefined_type"] == "FLOORING":
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
elif AuthoringData.data["predefined_type"] == "CEILING":
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
elif (AuthoringData.data["predefined_type"] == "FLOORING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
elif (AuthoringData.data["predefined_type"] == "CEILING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
elif (element and
bpy.context.selected_objects and
element.is_a("IfcCovering") and
# AuthoringData.data["predefined_type"] == "FLOORING" and
AuthoringData.data["active_material_usage"] == "LAYER3"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
# if (type_material_usage == "IfcMaterialLayerSet" and
# not bpy.context.selected_objects):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# if tool.Ifc.get_entity(collection_obj):
# if AuthoringData.data["predefined_type"] == "FLOORING":
# op = row.operator("bim.add_instance_flooring_covering_from_cursor")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
# else:
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
#
# else:
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
#
# elif (AuthoringData.data["predefined_type"] == "FLOORING" and
# type_material_usage == "IfcMaterialLayerSet" and
# element and
# bpy.context.selected_objects and
# element.is_a("IfcWall")):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_instance_flooring_coverings_from_walls")
#
# elif (AuthoringData.data["predefined_type"] == "CEILING" and
# type_material_usage == "IfcMaterialLayerSet" and
# element and
# bpy.context.selected_objects and
# element.is_a("IfcWall")):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
#
# elif (element and
# bpy.context.selected_objects and
# element.is_a("IfcCovering") and
## AuthoringData.data["predefined_type"] == "FLOORING" and
# AuthoringData.data["active_material_usage"] == "LAYER3"):
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_G")
# op = row.operator("bim.regen_selected_covering_object")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# row = cls.layout.row(align=True)
@@ -174,14 +203,14 @@ class CoveringToolUI:
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
else:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
# else:
# row = cls.layout.row(align=True)
# row.label(text="", icon="EVENT_SHIFT")
# row.label(text="", icon="EVENT_A")
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
@classmethod
def draw_type_selection_interface(cls):
@@ -72,7 +72,7 @@ class ReorderCsvAttribute(bpy.types.Operator):
def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary"]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
for prop in props:
value = getattr(new, prop)
setattr(new, prop, getattr(old, prop))
@@ -246,6 +246,7 @@ class ExportIfcCsv(bpy.types.Operator):
if props.format != "csv" and props.should_generate_svg:
schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
self.report({"INFO"}, f"Data is exported to {props.format.upper()}.")
return {"FINISHED"}
@@ -285,6 +286,7 @@ class ImportIfcCsv(bpy.types.Operator):
if not props.should_load_from_memory:
ifc_file.write(props.csv_ifc_file)
refresh_ui_data()
self.report({"INFO"}, "Data is imported to IFC.")
return {"FINISHED"}
@@ -35,7 +35,6 @@ classes = (
operator.PrintUnusedElementStats,
operator.ProfileImportIFC,
operator.PurgeHdf5Cache,
operator.PurgeIfcLinks,
operator.PurgeUnusedElementsByClass,
operator.RewindInspector,
operator.SelectExpressFile,
@@ -24,7 +24,6 @@ import random
import logging
import platform
import subprocess
import addon_utils
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
@@ -34,7 +33,7 @@ import blenderbim.tool as tool
import blenderbim.core.debug as core
import blenderbim.bim.handler
import blenderbim.bim.import_ifc as import_ifc
import blenderbim.tool as tool
from blenderbim import get_debug_info, format_debug_info
from blenderbim.bim.ifc import IfcStore
@@ -44,28 +43,7 @@ class CopyDebugInformation(bpy.types.Operator):
bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context):
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
info = {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"ifc": False,
}
info = get_debug_info()
if tool.Ifc.get():
info.update(
{
@@ -76,17 +54,13 @@ class CopyDebugInformation(bpy.types.Operator):
}
)
# Format it in a readable way
text = "\n".join(f"{k}: {v}" for k, v in info.items())
print(text)
text = format_debug_info(info)
if platform.system() == "Windows":
command = "echo | set /p nul=" + text.strip()
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
print("-" * 80)
print(text)
print("-" * 80)
context.window_manager.clipboard = text
return {"FINISHED"}
@@ -104,10 +78,11 @@ class PrintIfcFile(bpy.types.Operator):
return {"FINISHED"}
class PurgeIfcLinks(bpy.types.Operator):
bl_idname = "bim.purge_ifc_links"
bl_label = "Purge IFC Links"
bl_description = "Purge all definitions and references from the file.\nWarning : Cannot be undone."
class ConvertToBlender(bpy.types.Operator):
bl_idname = "bim.convert_to_blender"
bl_label = "Convert To Blender File"
bl_description = "Removes all IFC data and revert to basic Blender objects.\nWarning : Cannot be undone."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in bpy.data.objects:
@@ -124,26 +99,6 @@ class PurgeIfcLinks(bpy.types.Operator):
return {"FINISHED"}
class ConvertToBlender(bpy.types.Operator):
bl_idname = "bim.convert_to_blender"
bl_label = "Convert To Blender File"
bl_description = "Removes all IFC data, and converts the file to a simple Blender file."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for o in bpy.data.objects:
if o.type in {"MESH", "EMPTY"}:
o.BIMObjectProperties.ifc_definition_id = 0
if o.data:
o.data.BIMMeshProperties.ifc_definition_id = 0
for m in bpy.data.materials:
m.BIMMaterialProperties.ifc_style_id = False
bpy.context.scene.BIMProperties.ifc_file = ""
IfcStore.purge()
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class ValidateIfcFile(bpy.types.Operator):
bl_idname = "bim.validate_ifc_file"
bl_label = "Validate IFC File"
@@ -60,9 +60,6 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.purge_hdf5_cache")
row = layout.row()
row.operator("bim.purge_ifc_links")
row = layout.row()
row.operator("bim.update_representation", text="Manually Save Representation")
@@ -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?
@@ -29,6 +29,7 @@ import subprocess
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.selector
import ifcopenshell.util.representation
@@ -219,11 +220,12 @@ class CreateDrawing(bpy.types.Operator):
def execute(self, context):
self.props = context.scene.DocProperties
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
if self.print_all:
original_drawing_id = self.props.active_drawing_id
original_drawing_id = active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
else:
drawings_to_print = [self.props.active_drawing_id]
drawings_to_print = [active_drawing_id]
for drawing_i, drawing_id in enumerate(drawings_to_print):
self.drawing_index = drawing_i
@@ -1241,12 +1243,14 @@ class AddDrawingToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": drawing_reference.Location,
"Description": "DRAWING",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=drawing_reference.Location,
Description="DRAWING",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
@@ -1314,9 +1318,10 @@ class CreateSheets(bpy.types.Operator, Operator):
has_sheet_reference = False
for reference in tool.Drawing.get_document_references(sheet):
if reference.Description == "SHEET":
reference_description = tool.Drawing.get_reference_description(reference)
if reference_description == "SHEET":
has_sheet_reference = True
elif reference.Description == "RASTER":
elif reference_description == "RASTER":
if reference.Location in raster_references:
raster_references.remove(reference.Location)
else:
@@ -1327,7 +1332,9 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET"
),
)
for raster_reference in raster_references:
@@ -1335,7 +1342,9 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes={"Location": tool.Ifc.get_relative_uri(raster_reference), "Description": "RASTER"},
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER"
),
)
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
@@ -1444,13 +1453,19 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall()
# save current visibility statuses for Views and Types collections
visibility_status: dict[bpy.types.Object, bool] = {}
for col in bpy.data.collections["Views"].children:
for obj in col.objects:
visibility_status[obj] = obj.hide_get()
for obj in bpy.data.collections["Types"].objects:
visibility_status[obj] = obj.hide_get()
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_clear()
bpy.ops.bim.activate_status_filters()
subcontext = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -1468,6 +1483,11 @@ class ActivateModel(bpy.types.Operator):
is_global=True,
should_sync_changes_first=True,
)
# restore visibility after hide_view_clear()
for obj, hide_status in visibility_status.items():
obj.hide_set(hide_status)
tool.Blender.update_viewport()
return {"FINISHED"}
@@ -1495,7 +1515,14 @@ class ActivateDrawing(bpy.types.Operator):
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
core.activate_drawing_view(tool.Ifc, tool.Drawing, drawing=drawing)
try:
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
except core.CameraNotAvailableError:
self.report(
{"ERROR"},
"The drawing view is not available. Ensure you have not excluded it in the active view layer.",
)
return {"CANCELLED"}
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
@@ -1988,12 +2015,14 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": schedule_location,
"Description": "SCHEDULE",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=schedule_location,
Description="SCHEDULE",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2042,12 +2071,15 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "REFERENCE")]) + 1),
"Location": extref_location,
"Description": "REFERENCE",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "REFERENCE")])
+ 1
),
Location=extref_location,
Description="REFERENCE",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2392,8 +2424,8 @@ class EditSheet(bpy.types.Operator, Operator):
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
self.name = sheet.Name
self.identification = sheet.Identification
elif sheet.is_a("IfcDocumentReference") and sheet.Description == "TITLEBLOCK":
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
self.document_type = "TITLEBLOCK"
else:
self.document_type = "EMBEDDED"
@@ -2419,7 +2451,7 @@ class EditSheet(bpy.types.Operator, Operator):
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
elif self.document_type == "EMBEDDED":
core.rename_reference(tool.Ifc, reference=sheet, identification=self.identification)
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
elif self.document_type == "TITLEBLOCK":
titleblock = self.props.titleblock
reference = sheet
@@ -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
@@ -41,7 +41,7 @@ class SheetBuilder:
self.data_dir = None
self.scale = "NTS"
def create(self, layout_path, titleblock_name):
def create(self, layout_path: str, titleblock_name: str) -> None:
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
@@ -76,7 +76,12 @@ class SheetBuilder:
with open(layout_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
def add_drawing(self, reference, drawing, sheet):
def add_drawing(
self,
reference: ifcopenshell.entity_instance,
drawing: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
filename = drawing.Name
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
@@ -131,7 +136,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def update_sheet_drawing_sizes(self, sheet):
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -171,7 +176,7 @@ class SheetBuilder:
layout_tree.write(layout_path)
def remove_drawing(self, reference, sheet):
def remove_drawing(self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -187,7 +192,12 @@ class SheetBuilder:
layout_tree.write(layout_path)
def add_document(self, reference, document, sheet):
def add_document(
self,
reference: ifcopenshell.entity_instance,
document: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(document), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_document(document)
@@ -224,7 +234,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def add_view_title(self, x, y, parent, layout_dir):
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
@@ -241,7 +251,7 @@ class SheetBuilder:
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet):
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
self.references = {"SHEET": None, "RASTER": []}
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -272,7 +282,7 @@ class SheetBuilder:
return self.references
def build_titleblock(self, root, sheet):
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
g = self.parse_embedded_svg(image, sheet.get_info())
@@ -285,7 +295,7 @@ class SheetBuilder:
titleblock.append(g)
titleblock.remove(image)
def ensure_drawing_unique_styles(self, svg, drawing_id):
def ensure_drawing_unique_styles(self, svg: ET.Element, drawing_id: int) -> ET.Element:
"""ensures all drawing's classes and ids will be unique for the whole sheet
by adding `drawing_id` based prefix
"""
@@ -313,7 +323,7 @@ class SheetBuilder:
brackets_level -= 1
text += l
def replace_urls(text):
def replace_urls(text: str) -> str:
"""replace urls `url(#marker)` with `url(#prefix-marker)`
since `url(#marker.prefix)` doesn't seem to work
"""
@@ -343,7 +353,7 @@ class SheetBuilder:
return svg
def build_drawings(self, root, sheet):
def build_drawings(self, root: ET.Element, sheet: ifcopenshell.entity_instance):
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
drawing_id = int(view.attrib["data-id"])
try:
@@ -390,7 +400,7 @@ class SheetBuilder:
for image in images:
view.remove(image)
def build_documents(self, root, sheet):
def build_documents(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
documents = schedules + references
@@ -427,10 +437,10 @@ class SheetBuilder:
for image in images:
view.remove(image)
def get_href(self, element):
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace('\\','/')
def get_href(self, element: ET.Element) -> str:
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace("\\", "/")
def parse_embedded_svg(self, image, data):
def parse_embedded_svg(self, image: ET.Element, data: dict) -> ET.Element:
group = ET.Element("g")
group.attrib["transform"] = "translate({},{})".format(
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
@@ -466,7 +476,7 @@ class SheetBuilder:
group.append(child)
return group
def change_titleblock(self, sheet, titleblock_name):
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -499,7 +509,7 @@ class SheetBuilder:
sheet_tree.write(sheet_path)
def convert_to_mm(self, value):
def convert_to_mm(self, value: str) -> float:
# CSS is what defines these possibilities
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
# https://www.w3.org/TR/css-values-3/#absolute-lengths
@@ -520,5 +530,5 @@ class SheetBuilder:
return float(value[0:-2]) * (1 / 96) * 2.54 * 10
return float(value)
def mm_to_px(self, value):
def mm_to_px(self, value: float) -> float:
return (value / 25.4) * 96
@@ -719,14 +719,15 @@ class SvgWriter:
self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="ELEVATION", **text_style)
)
def get_reference_and_sheet_id_from_annotation(self, element):
def get_reference_and_sheet_id_from_annotation(self, element: ifcopenshell.entity_instance) -> tuple[str, str]:
reference_id = "-"
sheet_id = "-"
drawing = tool.Drawing.get_annotation_element(element)
reference = tool.Drawing.get_drawing_reference(drawing)
if reference:
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
if sheet_reference.Description != "DRAWING" or sheet_reference.Location != reference.Location:
reference_description = tool.Drawing.get_reference_description(sheet_reference)
if reference_description != "DRAWING" or sheet_reference.Location != reference.Location:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
@@ -29,6 +29,7 @@ classes = (
operator.GetRepresentationIfcParameters,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
operator.DuplicateLinkedAggregateTo3dCursor,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell.util.element
import blenderbim.tool as tool
import ifcopenshell.util.placement
from mathutils import Vector
@@ -222,7 +223,7 @@ class ConnectionsData:
@classmethod
def is_connection_realization(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
connections = element.IsConnectionRealization
connections = getattr(element, "IsConnectionRealization", None)
if not connections:
return
@@ -538,7 +538,7 @@ class Helper:
)
position = None
if self.file.schema == "IFC2X3":
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0, 0.0]))
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0]))
curve = self.file.createIfcRectangleProfileDef("AREA", None, position, xdim, ydim)
return {"curve_ucs": curve_ucs, "curve": curve}
@@ -136,19 +136,23 @@ class AddRepresentation(bpy.types.Operator, Operator):
"for Profile - 2D bounding box by local XZ axes.\n"
"For other contexts - bounding box is 3d.",
),
("PROJECT", "Full Representation", ""),
("OBJECT", "From Object", "Copies geometry from another object"),
("PROJECT", "Full Representation", "Reuses the current representation"),
],
name="Representation Conversion Method",
)
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
ifc_context = int(props.contexts or "0") or None
props = context.scene.BIMGeometryProperties
oprops = obj.BIMGeometryProperties
ifc_context = int(oprops.contexts or "0") or None
if not ifc_context:
return
ifc_context = tool.Ifc.get().by_id(ifc_context)
original_data = obj.data
if self.representation_conversion_method == "OUTLINE":
if ifc_context.ContextType == "Plan":
data = tool.Geometry.generate_outline_mesh(obj, axis="+Z")
@@ -165,17 +169,31 @@ class AddRepresentation(bpy.types.Operator, Operator):
else:
data = tool.Geometry.generate_3d_box_mesh(obj)
tool.Geometry.change_object_data(obj, data, is_global=True)
elif (
self.representation_conversion_method == "OBJECT"
and props.representation_from_object
and props.representation_from_object.data
):
data = tool.Geometry.duplicate_object_data(props.representation_from_object)
tool.Geometry.change_object_data(obj, data, is_global=True)
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
try:
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
except core.IncompatibleRepresentationError:
if obj.data != original_data:
tool.Geometry.change_object_data(obj, original_data, is_global=True)
bpy.data.meshes.remove(data)
self.report({"ERROR"}, "No compatible representation for the context could be created.")
return {"CANCELLED"}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -183,6 +201,9 @@ class AddRepresentation(bpy.types.Operator, Operator):
def draw(self, context):
row = self.layout.row()
row.prop(self, "representation_conversion_method", text="")
if self.representation_conversion_method == "OBJECT":
row = self.layout.row()
row.prop(context.scene.BIMGeometryProperties, "representation_from_object", text="")
class SelectConnection(bpy.types.Operator, Operator):
@@ -215,6 +236,13 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
disable_opening_subtractions: bpy.props.BoolProperty()
should_switch_all_meshes: bpy.props.BoolProperty()
@classmethod
def poll(cls, context):
if context.active_object.mode == "OBJECT":
return True
cls.poll_message_set("Only available in OBJECT mode - Press TAB in the viewport")
return False
def _execute(self, context):
target_representation = tool.Ifc.get().by_id(self.ifc_definition_id)
target = target_representation.ContextOfItems
@@ -223,6 +251,8 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not obj.mode == "OBJECT":
continue
if obj == context.active_object:
representation = target_representation
else:
@@ -370,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):
[
@@ -538,6 +572,8 @@ class OverrideDelete(bpy.types.Operator):
row.prop(self, "is_batch", text="Enable Faster Deletion")
def _execute(self, context):
start_time = time()
if self.is_batch:
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
@@ -562,6 +598,11 @@ class OverrideDelete(bpy.types.Operator):
IfcStore.add_transaction_operation(self)
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
operator_time = time() - start_time
if operator_time > 10:
self.report({"INFO"}, "IFC Delete was finished in {:.2f} seconds".format(operator_time))
return {"FINISHED"}
def rollback(self, data):
@@ -843,7 +884,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Recreate decompositions
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
OverrideDuplicateMove.handle_linked_aggregates(old_to_new)
OverrideDuplicateMove.remove_linked_aggregate_data(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@@ -896,25 +937,21 @@ class OverrideDuplicateMove(bpy.types.Operator):
if entity in old_to_new.keys():
core.remove_connection(tool.Geometry, connection=connection)
@staticmethod
def handle_linked_aggregates(old_to_new):
def remove_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
old_aggregate = ifcopenshell.util.element.get_aggregate(old)
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if old_aggregate == new_aggregate:
parts = ifcopenshell.util.element.get_parts(new_aggregate)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index += 1
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0],pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[new[0]])
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
@@ -940,14 +977,15 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_description = "Create and move a new linked aggregate"
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
bl_label = "IFC Duplicate Linked Aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.object_duplicate_move_linked_aggregate"
bl_label = "IFC Duplicate Linked Aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@@ -962,7 +1000,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
@staticmethod
def execute_ifc_duplicate_linked_aggregate_operator(self, context):
def execute_ifc_duplicate_linked_aggregate_operator(self, context, location_from_3d_cursor=False):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
@@ -974,15 +1012,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
obj.select_set(True)
parts = ifcopenshell.util.element.get_parts(element)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index = get_max_index(parts)
add_linked_aggregate_pset(element, index)
index += 1
for part in parts:
if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part)
else:
add_linked_aggregate_pset(part, index)
index += 1
index = add_linked_aggregate_pset(part, index)
# index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
@@ -999,6 +1037,8 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
pset=pset,
properties={"Index": index},
)
index += 1
else:
pass
@@ -1014,10 +1054,8 @@ 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)
ifcopenshell.api.run(
"group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group
)
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):
for new in old_to_new.values():
@@ -1042,6 +1080,49 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
split_name = new_obj.name.split(".")
new_obj.name = split_name[0] + "_" + number
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
def copy_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=new[0], name=self.pset_name
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=new_pset,
properties={"Index": pset["Index"]},
)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(old, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new)
def get_location_from_3d_cursor(old_to_new, aggregate):
base_obj = tool.Ifc.get_object(aggregate)
base_obj_location = base_obj.location.copy()
for new in old_to_new.values():
new_obj = tool.Ifc.get_object(new[0])
location_diff = new_obj.location - base_obj_location
new_obj.location = context.scene.cursor.location + location_diff
if len(context.selected_objects) != 1:
return {"FINISHED"}
@@ -1062,27 +1143,34 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
# Recreate aggregate relationship
for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"):
tool.Root.recreate_aggregate(old_to_new)
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, selected_element)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@staticmethod
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
bl_idname = "bim.duplicate_linked_aggregate_to_3d_cursor"
bl_label = "IFC Duplicate Linked Aggregate to 3d Cursor"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context, location_from_3d_cursor=True)
class RefreshLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.refresh_linked_aggregate"
@@ -1216,6 +1304,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
return list(set(linked_aggregate_groups)), selected_parents
def get_original_matrix(element, base_instance):
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
return selected_matrix, duplicate_matrix
def set_new_matrix(selected_matrix, duplicate_matrix, old_to_new):
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
active_element = tool.Ifc.get_entity(context.active_object)
if not active_element:
self.report({"INFO"}, "Object has no Ifc metadata.")
@@ -1255,10 +1359,7 @@ class RefreshLinkedAggregate(bpy.types.Operator):
element_aggregate = ifcopenshell.util.element.get_aggregate(element)
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance)
original_names = get_original_names(element)
@@ -1269,15 +1370,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
tool.Ifc.get_object(base_instance).select_set(True)
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(
self, context
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
set_new_matrix(selected_matrix, duplicate_matrix, old_to_new)
for old, new in old_to_new.items():
if element_aggregate and new[0].is_a("IfcElementAssembly"):
@@ -1409,14 +1504,13 @@ class OverridePasteBuffer(bpy.types.Operator):
def execute(self, context):
bpy.ops.view3d.pastebuffer()
if IfcStore.get_file():
for obj in context.selected_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
for obj in context.selected_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
return {"FINISHED"}
@@ -138,3 +138,4 @@ class BIMGeometryProperties(PropertyGroup):
name="IFC Interaction Mode",
update=update_mode,
)
representation_from_object: PointerProperty(type=bpy.types.Object)
@@ -24,6 +24,7 @@ classes = (
operator.AddLayer,
operator.AddListItem,
operator.AddMaterial,
operator.DuplicateMaterial,
operator.AddMaterialSet,
operator.AddProfile,
operator.AssignMaterial,
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.doc
import ifcopenshell.util.schema
import blenderbim.tool as tool
@@ -166,6 +167,9 @@ class ObjectMaterialData:
cls.data["type_material"] = cls.type_material()
cls.data["material_type"] = cls.material_type()
cls.data["active_material_constituents"] = cls.active_material_constituents()
# after material_name and type_material
cls.data["is_type_material_overridden"] = cls.is_type_material_overridden()
cls.is_loaded = True
@classmethod
@@ -294,8 +298,7 @@ class ObjectMaterialData:
@classmethod
def material_name(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
material = ifcopenshell.util.element.get_material(element)
material = cls.material
if material:
return getattr(material, "Name", None) or "Unnamed"
@@ -339,3 +342,18 @@ class ObjectMaterialData:
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
return []
return [m.Name for m in material.MaterialConstituents if m.Name]
@classmethod
def is_type_material_overridden(cls) -> bool:
if not cls.data["type_material"]:
return False
# try to avoid accessing ifc
if cls.data["material_name"] != cls.data["type_material"]:
return True
# in theory material can be overridden by the same material
# so we check occurrence material explicitly
element = tool.Ifc.get_entity(bpy.context.active_object)
occurrence_material = ifcopenshell.util.element.get_material(element, should_inherit=False)
return bool(occurrence_material)
@@ -134,6 +134,19 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
material_prop_purge()
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_material"
bl_label = "Diplicate Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material ID")
def _execute(self, context):
ifc_file = tool.Ifc.get()
tool.Material.duplicate_material(ifc_file.by_id(self.material))
material_prop_purge()
bpy.ops.bim.load_materials()
class AddMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material_set"
bl_label = "Add Material Set"
@@ -61,6 +61,8 @@ class BIM_PT_materials(Panel):
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
if material.ifc_definition_id:
op = row.operator("bim.duplicate_material", text="", icon="DUPLICATE")
op.material = material.ifc_definition_id
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL")
@@ -175,7 +177,13 @@ class BIM_PT_object_material(Panel):
if ObjectMaterialData.data["type_material"]:
row = self.layout.row(align=True)
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF")
if ObjectMaterialData.data["is_type_material_overridden"]:
row.label(
text=f"Inherited Material Is Occurrence Overridden",
icon="CON_CHILDOF",
)
else:
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF")
if ObjectMaterialData.data["material_class"]:
return self.draw_material_ui()
@@ -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"
@@ -193,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))
@@ -32,7 +32,9 @@ class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operato
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return tool.Ifc.get_entity(collection_obj) and relating_type == "FLOORING"
def _execute(self, context):
@@ -61,7 +63,9 @@ class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return tool.Ifc.get_entity(collection_obj) and relating_type == "CEILING"
def _execute(self, context):
@@ -116,9 +120,11 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects
return context.selected_objects and relating_type == "FLOORING"
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -147,7 +153,7 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_ceiling_coverings_from_walls"
bl_label = "Add Ceilings From Walls"
bl_label = "Add Ceiling From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add instance ceiling coverings from selected walls. The active object must be a wall and layered vertically"
@@ -155,9 +161,11 @@ class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects
return context.selected_objects and relating_type == "CEILING"
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -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)
@@ -39,6 +39,7 @@ from mathutils import Vector, Matrix
from bpy_extras.object_utils import AddObjectHelper
from . import prop
import json
from typing import Any, Union
class EnableAddType(bpy.types.Operator, tool.Ifc.Operator):
@@ -137,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))
@@ -511,7 +515,7 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
)
def ensure_material_assigned(usecase_path, ifc_file, settings):
def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
if usecase_path == "material.assign_material":
if not settings.get("material", None):
return
@@ -524,53 +528,76 @@ def ensure_material_assigned(usecase_path, ifc_file, settings):
]:
elements.extend(rel.RelatedObjects)
for element in elements:
obj = IfcStore.get_element(element.GlobalId)
if not obj or not obj.data:
continue
element_material = ifcopenshell.util.element.get_material(element)
material = [m for m in ifc_file.traverse(element_material) if m.is_a("IfcMaterial")]
object_material_ids = [
om.BIMObjectProperties.ifc_definition_id
for om in obj.data.materials
if om is not None and om.BIMObjectProperties.ifc_definition_id
]
if material and material[0].id() in object_material_ids:
continue
if len(obj.data.materials) == 1:
obj.data.materials.clear()
if not material:
continue
obj.data.materials.append(IfcStore.get_element(material[0].id()))
update_blender_ifc_materials(elements)
def ensure_material_unassigned(usecase_path, ifc_file, settings):
def ensure_material_unassigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
elements = settings["products"]
if elements[0].is_a("IfcElementType"):
elements.extend(ifcopenshell.util.element.get_types(elements[0]))
update_blender_ifc_materials(elements)
def update_blender_ifc_materials(elements: list[ifcopenshell.entity_instance]) -> None:
"""update mesh blender materials that have ifc material connected to them
by replacing them with `blender_material`"""
# since different elements can share meshes (e.g. occurrecnes without openings)
# we need to make sure not to affect them accidentally
meshes_users: dict[bpy.types.Mesh, set[bpy.types.Object]] = dict()
for obj in bpy.data.objects:
if not obj.data:
continue
meshes_users.setdefault(obj.data, set()).add(obj)
objects: set[bpy.types.Object] = set()
for element in elements:
obj = tool.Ifc.get_object(element)
obj: bpy.types.Object = tool.Ifc.get_object(element)
if not obj or not obj.data:
continue
element_material = ifcopenshell.util.element.get_material(element)
if element_material:
objects.add(obj)
meshes: set[bpy.types.Mesh] = {obj.data for obj in objects}
for mesh in meshes:
mesh_users = meshes_users[mesh]
if not mesh_users.issubset(objects):
continue
to_remove = []
for i, slot in enumerate(obj.material_slots):
if not slot.material:
# NOTE: we need `obj` as removing materials and appending them to `mesh.materials`
# will mess up mesh faces material indices
# NOTE: we make an assumption here that all mesh users
# have the same material - they either inherit it from the type
# or type doesn't have a material.
#
# If we add option to UI to add materials overriding type materials
# then this assumption won't be safe anymore
obj = next(iter(mesh_users))
element = tool.Ifc.get_entity(obj)
current_material = ifcopenshell.util.element.get_material(element)
if current_material:
current_material = tool.Ifc.get_object(current_material)
material_replaced = False
for material_slot in obj.material_slots:
material = material_slot.material
if material is None:
continue
material = tool.Ifc.get_entity(slot.material)
if material:
to_remove.append(i)
total_removed = 0
for i in to_remove:
obj.active_material_index = i - total_removed
with bpy.context.temp_override(object=obj):
bpy.ops.object.material_slot_remove()
total_removed += 1
ifc_material = tool.Ifc.get_entity(material)
# it's blender material for style, so ignore it
if not ifc_material:
continue
if ifc_material == current_material:
continue
material_slot.material = current_material
material_replaced = True
if not material_replaced and current_material:
mesh.materials.append(current_material)
# clear empty slots
for i, material in reversed(list(enumerate(mesh.materials[:]))):
if material is None:
mesh.materials.pop(index=i)
@@ -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):
@@ -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"}
@@ -27,7 +27,7 @@ import blenderbim.core.type
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_label = "Generate Space from Cursor"
bl_options = {"REGISTER"}
bl_description = (
"Create a space from the cursor position. "
@@ -35,30 +35,34 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
"select the right space collection and run the operator"
)
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
# @classmethod
# def poll(cls, context):
# print(context)
# collection = context.view_layer.active_layer_collection.collection
# collection_obj = collection.BIMCollectionProperties.obj
# active_obj = context.active_object
# element = tool.Ifc.get_entity(active_obj)
# return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
def _execute(self, context):
# This works as a 2.5 extruded polygon based on a cutting plane. Note
# that rooms exclude walls (i.e. not to wall midpoint or exterior /
# exterior edge.
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
def msg_no_collection(self, context):
self.layout.label(text="NO ACTIVE COLLECTION. PLEASE SELECT A SPATIAL COLLECTION OBJECT OR A WALL")
def msg_no_active_storey(self, context):
self.layout.label(text="NO ACTIVE STOREY. PLEASE SELECT A SPATIAL COLLECTION OBJECT")
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
context.window_manager.popup_menu(msg_no_collection, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
if not spatial_element or not spatial_element.is_a("IfcBuildingStorey"):
context.window_manager.popup_menu(msg_no_active_storey, title="Error", icon="ERROR")
return
core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type)
@@ -70,12 +74,12 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Generate spaces from selected walls. The active object must be a wall"
@classmethod
def poll(cls, context):
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
return context.selected_objects and element.is_a("IfcWall")
# @classmethod
# def poll(cls, context):
# active_obj = context.active_object
# element = tool.Ifc.get_entity(active_obj)
# if element:
# return context.selected_objects and element.is_a("IfcWall")
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -87,19 +91,30 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(active_obj)
container = tool.Spatial.get_container(element)
def msg_no_active_object(self, context):
self.layout.label(text="No active object. Please select a wall")
def msg_no_active_wall(self, context):
self.layout.label(text="The active object is not a wall. Please select a wall.")
def msg_no_container(self, context):
self.layout.label(text="The wall is not contained. Please the selected wall in a building container")
def msg_no_selected_objects(self, context):
self.layout.label(text="No selected objects found. Please select walls.")
if not active_obj:
self.report({"ERROR"}, "No active object. Please select a wall")
context.window_manager.popup_menu(msg_no_active_object, title="Error", icon="ERROR")
return
element = tool.Ifc.get_entity(active_obj)
if element and not element.is_a("IfcWall"):
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
context.window_manager.popup_menu(msg_no_active_wall, title="Error", icon="ERROR")
return
if not container:
self.report({"ERROR"}, "The wall is not contained.")
context.window_manager.popup_menu(msg_no_container, title="Error", icon="ERROR")
return
if not context.selected_objects:
self.report({"ERROR"}, "No selected objects found. Please select walls.")
context.window_manager.popup_menu(msg_no_selected_objects, title="Error", icon="ERROR")
return
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
@@ -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"}
@@ -56,8 +56,11 @@ class LaunchTypeManager(bpy.types.Operator):
ifc_class = props.ifc_class or AuthoringData.data["ifc_element_type"]
else:
ifc_class = AuthoringData.data["ifc_element_type"]
props.type_class = ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
# will be None if project has no types
if ifc_class is not None:
props.type_class = ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
return context.window_manager.invoke_popup(self, width=550)
def draw(self, context):
@@ -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"}
@@ -378,6 +378,7 @@ class BimToolUI:
op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
@@ -385,8 +386,8 @@ class BimToolUI:
"IfcDuctSegment",
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "")
if context.region.type != "TOOL_HEADER":
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
@@ -394,7 +395,6 @@ class BimToolUI:
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
@@ -719,10 +719,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.flip_wall()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.flip_fill()
elif self.active_class in ("IfcBeam", "IfcColumn"):
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.flip_object(flip_local_axes="XZ")
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
def hotkey_S_G(self):
obj = bpy.context.active_object
@@ -808,9 +807,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.join_wall(join_type="V")
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.extend_profile(join_type="V")
def hotkey_S_B(self):
bpy.ops.bim.add_boundary()
@@ -21,6 +21,7 @@ from . import ui, prop, operator, data
classes = (
operator.AddProfileDef,
operator.DuplicateProfileDef,
operator.DisableEditingArbitraryProfile,
operator.DisableEditingProfile,
operator.DisableProfileEditingUI,
@@ -66,9 +66,15 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator):
profile: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMProfileProperties
current_index = props.active_profile_index
ifcopenshell.api.run("profile.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile))
bpy.ops.bim.load_profiles()
# preserve selected index if possible
if props.profiles:
props.active_profile_index = min(current_index, len(props.profiles) - 1)
class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_profile"
@@ -126,6 +132,27 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.load_profiles()
class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_profile_def"
bl_label = "Duplicate Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = context.scene.BIMProfileProperties
if len(props.profiles) > props.active_profile_index:
return True
cls.poll_message_set("No profile selected to duplicate.")
return False
def _execute(self, context):
props = context.scene.BIMProfileProperties
ifc_file = tool.Ifc.get()
profile = ifc_file.by_id(props.profiles[props.active_profile_index].ifc_definition_id)
tool.Profile.duplicate_profile(profile)
bpy.ops.bim.load_profiles()
class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_arbitrary_profile"
bl_label = "Enable Editing Arbitrary Profile"
@@ -70,6 +70,7 @@ class BIM_PT_profiles(Panel):
row = self.layout.row(align=True)
row.prop(self.props, "profile_classes", text="")
row.operator("bim.add_profile_def", text="", icon="ADD")
row.operator("bim.duplicate_profile_def", icon="DUPLICATE", text="")
self.layout.template_list(
"BIM_UL_profiles",
@@ -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))
@@ -21,6 +21,7 @@ import bpy
import time
import logging
import tempfile
import traceback
import subprocess
import numpy as np
import ifcopenshell
@@ -47,6 +48,7 @@ from mathutils import Vector, Matrix
from bpy.app.handlers import persistent
from blenderbim.bim.module.project.data import LinksData
from blenderbim.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
from typing import Union
class NewProject(bpy.types.Operator):
@@ -301,7 +303,7 @@ class AssignLibraryDeclaration(bpy.types.Operator):
ifcopenshell.api.run(
"project.assign_declaration",
self.file,
definition=self.file.by_id(self.definition),
definitions=[self.file.by_id(self.definition)],
relating_context=self.file.by_type("IfcProjectLibrary")[0],
)
element_name = self.props.active_library_element
@@ -337,7 +339,7 @@ class UnassignLibraryDeclaration(bpy.types.Operator):
ifcopenshell.api.run(
"project.unassign_declaration",
self.file,
definition=self.file.by_id(self.definition),
definitions=[self.file.by_id(self.definition)],
relating_context=self.file.by_type("IfcProjectLibrary")[0],
)
element_name = self.props.active_library_element
@@ -673,19 +675,23 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
return self.finish_loading_project(context)
def finish_loading_project(self, context):
if not self.is_existing_ifc_file():
return {"FINISHED"}
try:
if not self.is_existing_ifc_file():
return {"FINISHED"}
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
context.scene.BIMProperties.ifc_file = self.get_filepath()
context.scene.BIMProjectProperties.is_loading = True
context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
tool.Blender.register_toolbar()
if not self.is_advanced:
bpy.ops.bim.load_project_elements()
context.scene.BIMProperties.ifc_file = self.get_filepath()
context.scene.BIMProjectProperties.is_loading = True
context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
tool.Blender.register_toolbar()
if not self.is_advanced:
bpy.ops.bim.load_project_elements()
except:
blenderbim.last_error = traceback.format_exc()
raise
return {"FINISHED"}
def invoke(self, context, event):
@@ -867,7 +873,16 @@ class LinkIfc(bpy.types.Operator):
except:
pass # Perhaps on another drive or something
new.name = filepath
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
status = bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
if status == {"CANCELLED"}:
error_msg = (
f'Error processing IFC file "{self.filepath}" '
"was critical and blend file either wasn't saved or wasn't updated. "
"See logs above in system console for details."
)
print(error_msg)
self.report({"ERROR"}, error_msg)
return {"FINISHED"}
print(f"Finished linking {len(files)} IFCs", time.time() - start)
return {"FINISHED"}
@@ -946,10 +961,12 @@ class LoadLink(bpy.types.Operator):
if self.filepath.lower().endswith(".blend"):
self.link_blend(filepath)
elif self.filepath.lower().endswith(".ifc"):
self.link_ifc()
status = self.link_ifc()
if status:
return status
return {"FINISHED"}
def link_blend(self, filepath):
def link_blend(self, filepath: str) -> None:
with bpy.data.libraries.load(filepath, link=True) as (data_from, data_to):
data_to.scenes = data_from.scenes
for scene in bpy.data.scenes:
@@ -962,7 +979,7 @@ class LoadLink(bpy.types.Operator):
link = bpy.context.scene.BIMProjectProperties.links.get(self.filepath)
link.is_loaded = True
def link_ifc(self):
def link_ifc(self) -> Union[set[str], None]:
blend_filepath = self.filepath + ".cache.blend"
h5_filepath = self.filepath + ".cache.h5"
@@ -982,11 +999,14 @@ except Exception as e:
exit(1)
"""
t = time.time()
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
temp_file.write(code)
run = subprocess.run([bpy.app.binary_path, "-b", "--python", temp_file.name, "--python-exit-code", "1"])
if run.returncode == 1:
print("An error occurred while processing your IFC.")
if not os.path.exists(blend_filepath) or os.stat(blend_filepath).st_mtime < t:
return {"CANCELLED"}
self.link_blend(blend_filepath)
@@ -1086,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"
@@ -1204,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")
@@ -31,7 +31,6 @@ import blenderbim.core.qto as QtoCore
import blenderbim.bim.module.pset.data
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
class Operator:
@@ -41,35 +40,15 @@ class Operator:
return {"FINISHED"}
def get_pset_props(context, obj, obj_type):
if obj_type == "Object":
return bpy.data.objects.get(obj).PsetProperties
elif obj_type == "Material":
return bpy.data.materials.get(obj).PsetProperties
elif obj_type == "MaterialSet":
return bpy.data.objects.get(obj).MaterialSetPsetProperties
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).MaterialSetItemPsetProperties
elif obj_type == "Task":
return context.scene.TaskPsetProperties
elif obj_type == "Resource":
return context.scene.ResourcePsetProperties
elif obj_type == "Profile":
return context.scene.ProfilePsetProperties
elif obj_type == "WorkSchedule":
return context.scene.WorkSchedulePsetProperties
elif obj_type == "Group":
return context.scene.GroupPsetProperties
class TogglePsetExpansion(bpy.types.Operator, Operator):
bl_idname = "bim.toggle_pset_expansion"
bl_label = "Toggle Pset Expansion"
pset_id: bpy.props.IntProperty()
def _execute(self, context):
blenderbim.bim.module.pset.data.is_expanded[
self.pset_id
] = not blenderbim.bim.module.pset.data.is_expanded.setdefault(self.pset_id, True)
blenderbim.bim.module.pset.data.is_expanded[self.pset_id] = (
not blenderbim.bim.module.pset.data.is_expanded.setdefault(self.pset_id, True)
)
class EnablePsetEditing(bpy.types.Operator):
@@ -83,160 +62,15 @@ class EnablePsetEditing(bpy.types.Operator):
obj_type: bpy.props.StringProperty()
def execute(self, context):
self.props = get_pset_props(context, self.obj, self.obj_type)
self.props.properties.clear()
if self.pset_id:
pset = tool.Ifc.get().by_id(self.pset_id)
self.props.active_pset_name = pset.Name
self.props.active_pset_type = ""
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(pset.Name)
self.pset_name = pset.Name
else:
pset = None
self.props.active_pset_name = self.pset_name
self.props.active_pset_type = self.pset_type
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(self.pset_name)
if pset_template:
self.load_from_pset_template(pset_template, pset)
else:
self.load_from_pset_data(pset)
self.props.active_pset_id = self.pset_id
core.enable_pset_editing(tool.Pset, pset, self.pset_name, self.pset_type, self.obj, self.obj_type)
return {"FINISHED"}
def load_from_pset_template(self, pset_template, pset):
if pset:
data = ifcopenshell.util.element.get_property_definition(pset)
del data["id"]
else:
data = {}
for prop_template in pset_template.HasPropertyTemplates:
if not prop_template.is_a("IfcSimplePropertyTemplate"):
continue # Other types not yet supported
if prop_template.TemplateType == "P_SINGLEVALUE":
self.load_single_value(pset_template, prop_template, data)
elif prop_template.TemplateType.startswith("Q_"):
self.load_single_value(pset_template, prop_template, data)
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
self.load_enumerated_value(prop_template, data)
else:
# NOTE: currently unsupported types:
# - P_BOUNDEDVALUE
# - P_LISTVALUE
# - P_REFERENCEVALUE
# - P_TABLEVALUE
pass
def load_single_value(self, pset_template, prop_template, data):
prop = self.props.properties.add()
prop.name = prop_template.Name
prop.value_type = "IfcPropertySingleValue"
metadata = prop.metadata
metadata.name = prop_template.Name
metadata.is_null = data.get(prop_template.Name, None) is None
metadata.is_optional = True
metadata.is_uri = prop_template.PrimaryMeasureType == "IfcURIReference"
metadata.has_calculator = bool(mapper.get(pset_template.Name, {}).get(prop_template.Name, None))
metadata.data_type = self.get_data_type(prop_template)
special_type = ""
if prop_template.PrimaryMeasureType in (
"IfcPositiveLengthMeasure",
"IfcLengthMeasure",
) or prop_template.TemplateType == "Q_LENGTH":
special_type = "LENGTH"
elif prop_template.PrimaryMeasureType == "IfcAreaMeasure" or prop_template.TemplateType == "Q_AREA":
special_type = "AREA"
elif prop_template.PrimaryMeasureType == "IfcVolumeMeasure" or prop_template.TemplateType == "Q_VOLUME":
special_type = "VOLUME"
metadata.special_type = special_type
if metadata.data_type == "string":
metadata.string_value = "" if metadata.is_null else str(data[prop_template.Name])
elif metadata.data_type == "integer":
metadata.int_value = 0 if metadata.is_null else int(data[prop_template.Name])
elif metadata.data_type == "float":
metadata.float_value = 0.0 if metadata.is_null else float(data[prop_template.Name])
elif metadata.data_type == "boolean":
metadata.bool_value = False if metadata.is_null else bool(data[prop_template.Name])
metadata.ifc_class = pset_template.Name
blenderbim.bim.helper.add_attribute_description(metadata, prop_template)
def get_data_type(self, prop_template):
if prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]:
return "float"
elif prop_template.TemplateType == "Q_COUNT":
return "integer"
return ifcopenshell.util.attribute.get_primitive_type(
IfcStore.get_schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
)
def load_enumerated_value(self, prop_template, data):
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
selected_enum_items = data.get(prop_template.Name, []) or []
prop = self.props.properties.add()
prop.name = prop_template.Name
prop.value_type = "IfcPropertyEnumeratedValue"
metadata = prop.metadata
metadata.name = prop_template.Name
metadata.is_null = data.get(prop_template.Name, None) is None
metadata.is_optional = True
metadata.is_uri = prop_template.PrimaryMeasureType == "IfcURIReference"
# Cute hack to abuse the metadata to find the Blender data_type
metadata.set_value(enum_items[0])
data_type = metadata.get_value_name()
for enum in enum_items:
new = prop.enumerated_value.enumerated_values.add()
setattr(new, data_type, enum)
new.is_selected = enum in selected_enum_items
def load_from_pset_data(self, pset):
props = []
if pset.is_a("IfcElementQuantity"):
props = pset.Quantities
elif pset.is_a("IfcPropertySet"):
props = pset.HasProperties
elif pset.is_a("IfcMaterialProperties") or pset.is_a("IfcProfileProperties"):
props = pset.Properties
for prop in props:
if prop.is_a("IfcPropertyEnumeratedValue"):
simple_prop = self.props.properties.add()
simple_prop.name = prop.Name
simple_prop.value_type = "IfcPropertyEnumeratedValue"
metadata = simple_prop.metadata
metadata.name = prop.Name
metadata.is_null = len(simple_prop.enumerated_value.enumerated_values) == 0
metadata.is_optional = True
metadata.set_value(prop.EnumerationReference.EnumerationValues[0].wrappedValue)
enum_items = [v.wrappedValue for v in prop.EnumerationReference.EnumerationValues]
selected_enum_items = [v.wrappedValue for v in prop.EnumerationValues]
data_type = metadata.get_value_name(display_only=True)
for enum in enum_items:
new = simple_prop.enumerated_value.enumerated_values.add()
setattr(new, data_type, enum)
new.is_selected = enum in selected_enum_items
else:
if prop.is_a("IfcPropertySingleValue"):
value = prop.NominalValue.wrappedValue if prop.NominalValue else None
elif prop.is_a("IfcPhysicalSimpleQuantity"):
value = prop[3]
new_prop = self.props.properties.add()
new_prop.name = prop.Name
metadata = new_prop.metadata
metadata.set_value(value)
metadata.name = prop.Name
metadata.is_null = value is None
metadata.is_optional = True
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
class DisablePsetEditing(bpy.types.Operator, Operator):
bl_idname = "bim.disable_pset_editing"
@@ -246,7 +80,7 @@ class DisablePsetEditing(bpy.types.Operator, Operator):
obj_type: bpy.props.StringProperty()
def _execute(self, context):
props = get_pset_props(context, self.obj, self.obj_type)
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
if props.active_pset_id:
pset = tool.Ifc.get().by_id(props.active_pset_id)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
@@ -270,7 +104,7 @@ class EditPset(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {}
@@ -341,7 +175,7 @@ class RemovePset(bpy.types.Operator, Operator):
objects = [self.obj]
pset_name = tool.Ifc.get().by_id(self.pset_id).Name
for obj in objects:
props = get_pset_props(context, obj, self.obj_type)
props = tool.Pset.get_pset_props(obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None)
@@ -372,7 +206,7 @@ class AddQto(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
bpy.ops.bim.enable_pset_editing(
@@ -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
@@ -143,7 +143,9 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
row = box.row(align=True)
row.scale_y = 0.8
row.label(text=prop["Name"])
row.label(text=str(prop["NominalValue"]))
op = row.operator("bim.select_similar", text=str(prop["NominalValue"]), icon="NONE", emboss=False)
op.key = pset['Name']
if not has_props_displayed:
row = box.row()
row.scale_y = 0.8
@@ -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()
@@ -268,7 +268,6 @@ class RemovePropTemplate(bpy.types.Operator, Operator):
prop_template: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
ifcopenshell.api.run(
"pset_template.remove_prop_template",
IfcStore.pset_template_file,
@@ -287,21 +286,21 @@ class EditPropTemplate(bpy.types.Operator, Operator):
def _execute(self, context):
props = context.scene.BIMPsetTemplateProperties
if props.active_prop_template.template_type == "P_ENUMERATEDVALUE":
enumerator = self.generate_prop_enum(props)
data_type = props.active_prop_template.get_value_name()
prop = props.active_prop_template
enumerators = [getattr(ev, data_type) for ev in prop.enum_values]
else:
enumerator = None
enumerators = None
ifcopenshell.api.run(
"pset_template.edit_prop_template",
IfcStore.pset_template_file,
**{
"prop_template": IfcStore.pset_template_file.by_id(props.active_prop_template_id),
"attributes": {
"Name": props.active_prop_template.name,
"Description": props.active_prop_template.description,
"PrimaryMeasureType": props.active_prop_template.primary_measure_type,
"TemplateType": props.active_prop_template.template_type,
"Enumerators": enumerator,
},
prop_template=IfcStore.pset_template_file.by_id(props.active_prop_template_id),
attributes={
"Name": props.active_prop_template.name,
"Description": props.active_prop_template.description,
"PrimaryMeasureType": props.active_prop_template.primary_measure_type,
"TemplateType": props.active_prop_template.template_type,
"Enumerators": enumerators,
}
)
bpy.ops.bim.disable_editing_prop_template()
@@ -309,18 +308,3 @@ class EditPropTemplate(bpy.types.Operator, Operator):
blenderbim.bim.handler.refresh_ui_data()
if tool.Ifc.get():
blenderbim.bim.schema.reload(tool.Ifc.get().schema)
# TODO -This will need to go into the
# api code at some point - vulevukusej
def generate_prop_enum(self, props):
self.file = IfcStore.pset_template_file
data_type = props.active_prop_template.get_value_name()
prop = props.active_prop_template
prop_enum = self.file.create_entity(
"IFCPROPERTYENUMERATION",
Name=prop.name,
EnumerationValues=tuple(
self.file.create_entity(prop.primary_measure_type, getattr(ev, data_type)) for ev in prop.enum_values
),
)
return prop_enum
@@ -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
@@ -185,6 +185,8 @@ class IfcClassData:
if element:
if element.is_a("IfcOpeningElement") or element.is_a("IfcOpeningStandardCase"):
return False
if element.is_a() in ("IfcWindowStyle", "IfcDoorStyle"): #see https://github.com/IfcOpenShell/IfcOpenShell/issues/4622#issuecomment-2095676368
return True
for product in cls.ifc_products():
if element.is_a(product[0]):
return True
@@ -20,6 +20,8 @@ import re
import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.selector
from ifcopenshell.util.selector import Selector
@@ -213,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)
@@ -366,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)
@@ -633,12 +635,14 @@ class SelectSimilar(Operator, tool.Ifc.Operator):
bl_label = "Select Similar"
bl_options = {"REGISTER", "UNDO"}
key: bpy.props.StringProperty()
def _execute(self, context):
props = context.scene.BIMSearchProperties
obj = context.active_object
element = tool.Ifc.get_entity(obj)
key = props.element_key
if props.element_key == "PredefinedType":
key = self.key
if key == "PredefinedType":
key = "predefined_type"
value = ifcopenshell.util.selector.get_element_value(element, key)
for obj in context.visible_objects:
@@ -113,7 +113,8 @@ class BIM_PT_select_similar(Panel):
if SelectSimilarData.data["element_key"]:
row = self.layout.row(align=True)
row.prop(props, "element_key", text="")
row.operator("bim.select_similar", text="", icon="RESTRICT_SELECT_OFF")
op = row.operator("bim.select_similar", text="", icon="RESTRICT_SELECT_OFF")
op.key = props.element_key
else:
row = self.layout.row()
row.label(text=f"No Active Element")
@@ -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
@@ -30,6 +30,7 @@ classes = (
operator.DisableAddingPresentationStyle,
operator.DisableEditingStyle,
operator.DisableEditingStyles,
operator.DuplicateStyle,
operator.EditStyle,
operator.EditSurfaceStyle,
operator.EnableAddingPresentationStyle,
@@ -23,6 +23,7 @@ import blenderbim.bim.handler
import blenderbim.tool as tool
import blenderbim.core.style as core
import ifcopenshell.util.representation
from blenderbim.bim.module.style.prop import switch_shading
from pathlib import Path
from mathutils import Vector
@@ -126,6 +127,10 @@ class DisableEditingStyle(bpy.types.Operator, tool.Ifc.Operator):
tool.Style.reload_material_from_ifc(material)
props.is_editing_style = 0
# restore selected style type
material = tool.Ifc.get_object(style)
material.BIMStyleProperties.active_style_type = material.BIMStyleProperties.active_style_type
class EditStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_style"
@@ -246,14 +251,15 @@ class BrowseExternalStyle(bpy.types.Operator):
)
def invoke(self, context, event):
external_style = None
style_elements = None
if self.active_surface_style_id:
style = tool.Ifc.get().by_id(self.active_surface_style_id)
external_style = tool.Style.get_style_elements(style).get("IfcExternallyDefinedSurfaceStyle", None)
style_elements = tool.Style.get_style_elements(style)
# automatically select previously selected external style in file browser
# if it exists in the file
if external_style and self.filepath == "":
if style_elements and self.filepath == "" and tool.Style.has_blender_external_style(style_elements):
external_style = style_elements["IfcExternallyDefinedSurfaceStyle"]
style_path = Path(tool.Ifc.resolve_uri(external_style.Location))
self.directory = str(style_path.parent)
self.filepath = str(style_path)
@@ -310,6 +316,9 @@ class BrowseExternalStyle(bpy.types.Operator):
attributes["Location"].string_value = filepath
attributes["Identification"].string_value = f"{self.data_block_type}/{self.data_block}"
attributes["Name"].string_value = self.data_block
style = tool.Ifc.get().by_id(self.active_surface_style_id)
bpy.ops.bim.activate_external_style(material_name=tool.Ifc.get_object(style).name)
return {"FINISHED"}
@@ -325,14 +334,23 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
material = context.active_object.active_material
else:
material = bpy.data.materials[self.material_name]
external_style = tool.Style.get_style_elements(material)["IfcExternallyDefinedSurfaceStyle"]
data_block_type, data_block = external_style.Identification.split("/")
style_path = Path(tool.Ifc.resolve_uri(external_style.Location))
props = context.scene.BIMStylesProperties
if props.is_editing:
location = props.external_style_attributes["Location"].string_value
identification = props.external_style_attributes["Identification"].string_value
else:
external_style = tool.Style.get_style_elements(material)["IfcExternallyDefinedSurfaceStyle"]
location = external_style.Location
identification = external_style.Identification
data_block_type, data_block = identification.split("/")
style_path = Path(tool.Ifc.resolve_uri(location))
if style_path.suffix != ".blend":
self.report(
{"ERROR"},
f"Error loading external style for \"{material.name}\" - only Blender external styles are supported",
f'Error loading external style for "{material.name}" - only Blender external styles are supported',
)
return {"CANCELLED"}
@@ -493,6 +511,22 @@ class EnableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
props.is_adding = True
class DuplicateStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_style"
bl_label = "Duplicate Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty(name="Style ID")
def _execute(self, context):
style_type = context.scene.BIMStylesProperties.style_type
ifc_file = tool.Ifc.get()
style = ifc_file.by_id(self.style)
tool.Style.duplicate_style(style)
bpy.ops.bim.disable_editing_styles()
bpy.ops.bim.load_styles(style_type=style_type)
class DisableAddingPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_adding_presentation_style"
bl_label = "Disable Add Presentation Style"
@@ -571,7 +605,8 @@ class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing_class = self.ifc_class
tool.Style.set_surface_style_props()
surface_style = tool.Style.get_style_elements(style).get(self.ifc_class, None)
style_elements = tool.Style.get_style_elements(style)
surface_style = style_elements.get(self.ifc_class, None)
attributes = tool.Style.get_style_ui_props_attributes(self.ifc_class)
# lighting style require special handling since Attribute doesn't support colors
@@ -591,6 +626,17 @@ class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
attributes.clear()
blenderbim.bim.helper.import_attributes2(surface_style or self.ifc_class, attributes, callback)
material = tool.Ifc.get_object(style)
active_style_type = material.BIMStyleProperties.active_style_type
if self.ifc_class == "IfcExternallyDefinedSurfaceStyle" and active_style_type != "External":
if tool.Style.has_blender_external_style(style_elements):
switch_shading(material, "External")
elif (
self.ifc_class in ("IfcSurfaceStyleShading", "IfcSurfaceStyleRendering", "IfcSurfaceStyleWithTextures")
and active_style_type != "Shading"
):
switch_shading(material, "Shading")
class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_surface_style"
@@ -615,6 +661,10 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
self.props.is_editing_style = 0
core.load_styles(tool.Style, style_type=self.props.style_type)
# restore selected style type
material = tool.Ifc.get_object(self.style)
material.BIMStyleProperties.active_style_type = material.BIMStyleProperties.active_style_type
def edit_existing_style(self):
material = tool.Ifc.get_object(self.style)
if self.surface_style.is_a() == "IfcSurfaceStyleShading":
@@ -33,6 +33,8 @@ from bpy.props import (
)
import gettext
from typing import Literal
_ = gettext.gettext
@@ -251,19 +253,15 @@ class BIMStylesProperties(PropertyGroup):
)
def update_shading_style(self, context):
blender_material = self.id_data
style_elements = tool.Style.get_style_elements(blender_material)
if self.active_style_type == "External":
if tool.Style.has_blender_external_style(style_elements):
try:
bpy.ops.bim.activate_external_style(material_name=blender_material.name)
except RuntimeError as error:
if str(error).startswith("Error: Error loading external style for "):
return
raise error
elif self.active_style_type == "Shading":
def switch_shading(blender_material: bpy.types.Material, style_type: Literal["External", "Shading"]) -> None:
if style_type == "External":
try:
bpy.ops.bim.activate_external_style(material_name=blender_material.name)
except RuntimeError as error:
if str(error).startswith("Error: Error loading external style for "):
return
raise error
elif style_type == "Shading":
style_elements = tool.Style.get_style_elements(blender_material)
rendering_style = None
texture_style = None
@@ -279,6 +277,16 @@ def update_shading_style(self, context):
if rendering_style and texture_style:
tool.Loader.create_surface_style_with_textures(blender_material, rendering_style, texture_style)
def update_shading_style(self, context):
blender_material = self.id_data
style_elements = tool.Style.get_style_elements(blender_material)
if self.active_style_type == "External":
if tool.Style.has_blender_external_style(style_elements):
switch_shading(blender_material, self.active_style_type)
elif self.active_style_type == "Shading":
switch_shading(blender_material, self.active_style_type)
tool.Style.record_shading(blender_material)
@@ -44,19 +44,33 @@ class BIM_PT_styles(Panel):
self.props = context.scene.BIMStylesProperties
if self.props.is_editing:
row = self.layout.row(align=True)
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
if not self.props.is_adding:
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
else:
if not self.props.is_editing:
row = self.layout.row(align=True)
row.label(text="{} Styles".format(StylesData.data["total_styles"]), icon="SHADING_RENDERED")
blenderbim.bim.helper.prop_with_search(row, self.props, "style_type", text="")
row.operator("bim.load_styles", text="", icon="IMPORT").style_type = self.props.style_type
return
active_style = self.props.styles and self.props.active_style_index < len(self.props.styles)
row = self.layout.row(align=True)
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
row.operator("bim.disable_editing_styles", text="", icon="CANCEL")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
if not self.props.is_adding:
row.operator("bim.enable_adding_presentation_style", text="", icon="ADD")
if active_style:
style = self.props.styles[self.props.active_style_index]
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
material = bpy.data.materials[material_name]
row.operator("bim.duplicate_style", text="", icon="DUPLICATE").style = style.ifc_definition_id
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
op = row.operator("bim.enable_editing_style", text="", icon="GREASEPENCIL")
op.style = style.ifc_definition_id
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index")
# adding a new IfcSurfaceStyle
@@ -77,17 +91,7 @@ class BIM_PT_styles(Panel):
row.operator("bim.disable_adding_presentation_style", text="", icon="CANCEL")
# style ui tools
if self.props.styles and self.props.active_style_index < len(self.props.styles):
row = self.layout.row(align=True)
style = self.props.styles[self.props.active_style_index]
material_name = StylesData.data["styles_to_blender_material_names"][self.props.active_style_index]
material = bpy.data.materials[material_name]
op = row.operator("bim.enable_editing_style", text="Edit Style", icon="GREASEPENCIL")
op.style = style.ifc_definition_id
row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF").style = style.ifc_definition_id
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
if active_style:
row = self.layout.row(align=True)
row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="")
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
@@ -146,23 +146,51 @@ class SelectType(bpy.types.Operator):
relating_type: bpy.props.IntProperty()
def execute(self, context):
element = tool.Ifc.get().by_id(self.relating_type)
obj = tool.Ifc.get_object(element)
if obj:
try:
tool.Blender.select_and_activate_single_object(context, obj)
except:
self.report({"INFO"}, "Type object is hidden.")
# IfcTypeProducts are only used for annotations and not part of the model interface.
if element.is_a() != "IfcTypeProduct":
try:
context.scene.BIMModelProperties.ifc_class = element.is_a()
context.scene.BIMModelProperties.relating_type_id = str(self.relating_type)
except:
# Potentially our BIM Tool is filtered to a specific element.
pass
if self.relating_type: #if operator button sends a relating_type, the iterator only selects this one type
element = tool.Ifc.get().by_id(self.relating_type)
obj = tool.Ifc.get_object(element)
selected_objs = [obj]
else: #else, the iterator selects all the types of all the selected objects
selected_objs = context.selected_objects
active_obj = context.active_object
selected_objs.append(active_obj) #update selected_objs so the active_obj is at the end of the list
last_relating_type_obj = None
types_collection_in_view_layer = self.find_collection_in_ifcproject(context, collection_name = "Types")
types_collection_in_view_layer.hide_viewport = False
types_collection = bpy.data.collections.get("Types")
for type_obj in types_collection.objects:
type_obj.hide_set(True)
for obj in selected_objs:
element = tool.Ifc.get_entity(obj)
relating_type = ifcopenshell.util.element.get_type(element)
if relating_type:
relating_type_obj = tool.Ifc.get_object(relating_type)
if relating_type_obj:
if relating_type_obj.hide_get():
relating_type_obj.hide_set(False)
relating_type_obj.select_set(True)
last_relating_type_obj = relating_type_obj
if not element.is_a("IfcTypeObject"):
obj.select_set(False)
context.view_layer.objects.active = last_relating_type_obj #makes the active_obj's type the active object
return {"FINISHED"}
def find_collection_in_ifcproject(self, context, collection_name):
ifc_project_collection = None
for child in context.view_layer.layer_collection.children:
if "IfcProject" in child.name:
ifc_project_collection = child
break
if ifc_project_collection:
collection_in_view_layer = ifc_project_collection.children.get(collection_name)
return collection_in_view_layer
class SelectSimilarType(bpy.types.Operator):
bl_idname = "bim.select_similar_type"
@@ -484,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):
@@ -88,7 +88,7 @@ class BIM_PT_type(Panel):
if TypeData.data["relating_type"]:
row.label(text=TypeData.data["relating_type"]["name"])
op = row.operator("bim.select_type", icon="OBJECT_DATA", text="")
op.relating_type = TypeData.data["relating_type"]["id"]
op.relating_type = 0 #will only select the relating types of only the selected objects
row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="")
row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="")
row.operator("bim.unassign_type", icon="X", text="")
@@ -90,6 +90,15 @@ class OpenUri(bpy.types.Operator):
return {"FINISHED"}
class CloseError(bpy.types.Operator):
bl_idname = "bim.close_error"
bl_label = "Close Error"
def execute(self, context):
blenderbim.last_error = None
return {"FINISHED"}
class SelectURIAttribute(bpy.types.Operator):
bl_idname = "bim.select_uri_attribute"
bl_label = "Select URI Attribute"
+31 -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
@@ -29,8 +30,9 @@ from ifcopenshell.util.doc import (
get_attribute_doc,
)
from . import ifc
import blenderbim.tool as tool
from blenderbim import get_debug_info
import blenderbim.bim
import blenderbim.tool as tool
from blenderbim.bim.helper import IfcHeaderExtractor
from blenderbim.bim.prop import Attribute
@@ -289,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()
@@ -319,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
@@ -402,6 +404,21 @@ class BIM_PT_tabs(Panel):
row = self.layout.row(align=True)
row.prop(aprops, "tab", text="")
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")
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"
except:
pass # Prior to load_post, we may not have any area properties setup
+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)
+5 -4
View File
@@ -46,7 +46,8 @@ def add_instance_flooring_covering_from_cursor(ifc, spatial, model, Type, geomet
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position(obj)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.traslate_obj_to_z_location(obj, z)
spatial.link_obj_to_active_collection(obj)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
@@ -74,7 +75,7 @@ def add_instance_ceiling_covering_from_cursor(ifc, spatial, model, Type, geometr
else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
z = covering.get_z_from_ceiling_height()
ceiling_height = covering.get_z_from_ceiling_height()
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
@@ -87,8 +88,8 @@ def add_instance_ceiling_covering_from_cursor(ifc, spatial, model, Type, geometr
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position(obj)
spatial.traslate_obj_to_z_location(obj, z)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.traslate_obj_to_z_location(obj, z+ceiling_height)
spatial.link_obj_to_active_collection(obj)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
+103 -45
View File
@@ -16,35 +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
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:
@@ -57,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=None):
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)
@@ -77,31 +86,37 @@ def add_sheet(ifc, drawing, titleblock=None):
else:
attributes = {"Identification": identification, "Name": "UNTITLED", "Scope": "SHEET"}
ifc.run("document.edit_information", information=sheet, attributes=attributes)
ifc.run(
"document.edit_reference",
reference=layout,
attributes={"Location": drawing.get_default_layout_path(identification, "UNTITLED"), "Description": "LAYOUT"},
attributes = drawing.generate_reference_attributes(
layout, Location=drawing.get_default_layout_path(identification, "UNTITLED"), Description="LAYOUT"
)
ifc.run(
"document.edit_reference",
reference=titleblock_reference,
attributes={"Location": drawing.get_default_titleblock_path(titleblock), "Description": "TITLEBLOCK"},
ifc.run("document.edit_reference", reference=layout, attributes=attributes)
attributes = drawing.generate_reference_attributes(
layout, Location=drawing.get_default_titleblock_path(titleblock), Description="TITLEBLOCK"
)
ifc.run("document.edit_reference", reference=titleblock_reference, attributes=attributes)
drawing.create_svg_sheet(sheet, titleblock)
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))
drawing.add_drawings(sheet)
try:
drawing.add_drawings(sheet)
except FileNotFoundError:
path_layout = drawing.get_document_uri(sheet, "LAYOUT")
if drawing.does_file_exist(path_layout):
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))
@@ -111,8 +126,12 @@ def remove_sheet(ifc, drawing, sheet=None):
drawing.import_sheets()
def rename_sheet(ifc, drawing, sheet=None, identification=None, name=None):
ifc.run("document.edit_information", information=sheet, attributes={"Identification": identification, "Name": name})
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:
attributes = {"Identification": identification, "Name": name}
ifc.run("document.edit_information", information=sheet, attributes=attributes)
for reference in drawing.get_document_references(sheet):
description = drawing.get_reference_description(reference)
if description == "SHEET":
@@ -133,29 +152,32 @@ def rename_sheet(ifc, drawing, sheet=None, identification=None, name=None):
drawing.move_file(old_location, ifc.resolve_uri(new_location))
def rename_reference(ifc, reference=None, identification=None):
ifc.run("document.edit_reference", reference=reference, attributes={"Identification": identification})
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)
@@ -168,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)
@@ -253,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)
@@ -290,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()
@@ -318,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)
@@ -352,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:
@@ -375,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
@@ -425,13 +471,25 @@ 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, 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)
drawing_tool.import_annotations_in_group(drawing_tool.get_drawing_group(drawing))
blender.activate_camera(camera)
drawing_tool.isolate_camera_collection(camera)
try:
blender.set_active_object(camera)
except:
raise CameraNotAvailableError()
drawing_tool.activate_drawing(camera)
class CameraNotAvailableError(Exception):
pass
+27 -12
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
@@ -63,6 +71,9 @@ def add_representation(
profile_set_usage=profile_set_usage,
)
if not representation:
raise IncompatibleRepresentationError()
if geometry.is_body_representation(representation):
[geometry.run_style_add_style(obj=mat) for mat in geometry.get_object_materials_without_styles(obj)]
ifc.run(
@@ -86,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`;
@@ -221,3 +232,7 @@ def edit_similar_opening_placement(geometry, opening=None, similar_openings=None
old_placement = similar_opening.ObjectPlacement
similar_opening.ObjectPlacement = opening.ObjectPlacement
geometry.delete_opening_object_placement(old_placement)
class IncompatibleRepresentationError(Exception):
pass
@@ -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)
+16
View File
@@ -42,3 +42,19 @@ def add_pset(ifc, pset, blender, obj_name, obj_type):
if not pset.is_pset_applicable(element, pset_name):
continue
pset.enable_pset_editing(pset_id=0, pset_name=pset_name, pset_type="PSET", obj=obj_name, obj_type=obj_type)
def enable_pset_editing(pset_tool, pset, pset_name, pset_type, obj_name, obj_type):
props = pset_tool.get_pset_props(obj_name, obj_type)
pset_tool.clear_blender_pset_properties(props)
pset_template = pset_tool.get_pset_template(pset_name)
if pset_template:
pset_tool.import_pset_from_template(pset_template, pset, props)
if pset:
pset_tool.import_pset_from_existing(pset, props)
pset_tool.set_active_pset(props, pset)
else:
pset_tool.enable_proposed_pset(props, pset_name, pset_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
+4 -3
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(
@@ -189,7 +189,8 @@ def generate_space(ifc, spatial, model, Type):
name = "Space"
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position(obj)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.traslate_obj_to_z_location(obj, z)
spatial.link_obj_to_active_collection(obj)
spatial.assign_ifcspace_class_to_obj(obj)
@@ -214,7 +215,7 @@ def generate_spaces_from_walls(ifc, spatial, collector):
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter(obj)
spatial.set_obj_origin_to_bboxcenter_and_zero_elevation(obj)
spatial.traslate_obj_to_z_location(obj, z)
spatial.link_obj_to_active_collection(obj)
+29 -15
View File
@@ -18,6 +18,7 @@
import abc
import inspect
from typing import Optional
# fmt: off
# pylint: skip-file
@@ -72,20 +73,21 @@ class Aggregate:
@interface
class Blender:
def set_active_object(cls, obj): pass
def get_name(cls, ifc_class, name): pass
def get_selected_objects(cls): pass
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): pass
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None, context=None): pass
def is_ifc_object(cls, obj): pass
def is_ifc_class_active(cls, ifc_class): pass
def get_viewport_context(cls): pass
def update_viewport(cls): pass
def get_default_selection_keypmap(cls): pass
def get_object_bounding_box(cls, obj): pass
def activate_camera(cls, obj): pass
def apply_bmesh(cls, mesh, bm, obj=None): pass
def get_bmesh_for_mesh(cls, mesh, clean=False): pass
def bmesh_join(cls, bm_a, bm_b, callback=None): pass
def create_ifc_object(cls, ifc_class: str, name: Optional[str] = None, data=None): pass
def get_bmesh_for_mesh(cls, mesh, clean=False): pass
def get_default_selection_keypmap(cls): pass
def get_name(cls, ifc_class, name): pass
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None, context=None): pass
def get_object_bounding_box(cls, obj): pass
def get_selected_objects(cls): pass
def get_viewport_context(cls): pass
def is_ifc_class_active(cls, ifc_class): pass
def is_ifc_object(cls, obj): pass
def set_active_object(cls, obj): pass
def update_viewport(cls): pass
@interface
@@ -329,6 +331,7 @@ class Drawing:
def get_name(cls, element): pass
def get_path_filename(cls, uri): pass
def get_reference_description(cls, reference): pass
def generate_reference_attributes(cls, reference, **attributes): pass
def get_reference_document(cls, reference): pass
def get_reference_location(cls, reference): pass
def get_references_with_location(cls, location): pass
@@ -484,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
@@ -500,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
@@ -614,9 +618,18 @@ class Profile:
@interface
class Pset:
def clear_blender_pset_properties(cls, props): pass
def enable_proposed_pset(cls, props, pset_name, pset_type): pass
def get_element_pset(cls, element, pset_name): pass
def get_prop_template_primitive_type(cls, prop_template): pass
def get_pset_name(cls, obj, obj_type): pass
def get_pset_template(cls, name): pass
def import_enumerated_value_from_template(cls, prop_template, data, props): pass
def import_pset_from_existing(cls, pset, props): pass
def import_pset_from_template(cls, pset_template, pset, props): pass
def import_single_value_from_template(cls, pset_template, prop_template, data, props): pass
def is_pset_applicable(cls,element, pset_name): pass
def set_active_pset(cls, props, pset): pass
@interface
@@ -871,7 +884,8 @@ class Spatial:
def get_transformed_mesh_from_local_to_global(cls, mesh): pass
def edit_active_space_obj_from_mesh(cls, mesh): pass
def set_obj_origin_to_bboxcenter(cls, obj): pass
def set_obj_origin_to_cursor_position(cls, obj): pass
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj): pass
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj): pass
def get_selected_objects(cls): pass
def get_active_obj(cls): pass
def get_active_obj_z(cls): pass
+25 -5
View File
@@ -49,6 +49,23 @@ class Blender(blenderbim.core.tool.Blender):
OBJECT_TYPES_THAT_SUPPORT_EDIT_GPENCIL_MODE = ("GPENCIL",)
TYPE_MANAGER_ICON = "LIGHTPROBE_VOLUME" if bpy.app.version >= (4, 1, 0) else "LIGHTPROBE_GRID"
@classmethod
def activate_camera(cls, obj: bpy.types.Object) -> None:
area = tool.Blender.get_view3d_area()
is_local_view = area.spaces[0].local_view is not None
if is_local_view:
# Turn off local view before activating drawing, and then turn it on again.
for a in bpy.context.screen.areas:
if a.type == "VIEW_3D":
override = bpy.context.copy()
override["area"] = a
bpy.ops.view3d.localview(override)
bpy.context.scene.camera = obj
bpy.ops.view3d.localview(override)
else:
bpy.context.scene.camera = obj
area.spaces[0].region_3d.view_perspective = "CAMERA"
@classmethod
def get_area_props(cls, context: bpy.types.Context) -> Any:
try:
@@ -451,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:
@@ -845,9 +867,7 @@ class Blender(blenderbim.core.tool.Blender):
bpy.utils.register_tool(ws_model.PipeTool, after={"bim.duct_tool"}, separator=False, group=False)
bpy.utils.register_tool(ws_model.BimTool, after={"bim.pipe_tool"}, separator=False, group=False)
bpy.utils.register_tool(ws_drawing.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False)
bpy.utils.register_tool(
ws_spatial.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False
)
bpy.utils.register_tool(ws_spatial.SpatialTool, after={"bim.annotation_tool"}, separator=False, group=False)
bpy.utils.register_tool(
ws_structural.StructuralTool, after={"bim.spatial_tool"}, separator=False, group=False
)
+2 -1
View File
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell
import ifcopenshell.guid
import ifcopenshell.util.brick
import blenderbim.core.tool
import blenderbim.tool as tool
@@ -604,4 +605,4 @@ class BrickStore:
def set_last_saved(cls):
save = os.path.getmtime(BrickStore.path)
save = datetime.datetime.fromtimestamp(save)
BrickStore.last_saved = f"{save.year}-{save.month}-{save.day} {save.hour}:{save.minute}"
BrickStore.last_saved = f"{save.year}-{save.month}-{save.day} {save.hour}:{save.minute}"

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