Compare commits

...

106 Commits

Author SHA1 Message Date
Andrej730 fa87092fe0 Keep 1.2m minimum opening thickness instead of using wall thickness
That way there will be a workaround if the wall is 0.6m+ meters thick but won't have an issue when you first added a window to a thin wall and it's opening later won't work for the thicker walls.
Mentioned in #4710
2024-05-24 12:10:40 +05:00
Andrej730 1a4e4678d0 assign/unassign resource operators tooltips
also removed couple unused properties
2024-05-23 18:45:31 +05:00
Andrej730 7f2c7eb467 python 3.9 compatibility 2024-05-23 18:45:31 +05:00
Andrej730 c70745826a rename bim.export_ifc to bim.save_project (similar to bim.load_project)
note that old name "export_ifc.bim" is still available (cacb38a25) though deprecated
2024-05-23 18:45:30 +05:00
Andrej730 291bffca95 add tooltip for cost schedule columns
example - https://i.imgur.com/Uh8D7Vg.png
2024-05-23 18:45:30 +05:00
Andrej730 e5a316e509 fix api calls error handling
by accident replaced in f2696d5 TypeError with NotImplementedError
2024-05-23 18:45:30 +05:00
Andrej730 e4aa97091e fix bug removing cost schedule columns
it was using _execute though it's not an ifc operator
2024-05-23 18:45:30 +05:00
Andrej730 b4f0a346c1 fix couple broken properties definitions
There was no errors but Blender seems to ignore the provided name if it was provided as a positional argument instead of a keyword argument.
2024-05-23 18:45:30 +05:00
Andrej730 073e471305 typing 2024-05-23 18:45:30 +05:00
Andrej730 4d9f47b3a7 make it more clear that bim.export_cost_schedules is expecting a folder
1) add folder filter
2) remove filepath property - that way only directories will be selectable in the file dialog.
2024-05-23 18:31:59 +05:00
Andrej730 96088c1d08 fix ui error in 40e4949
bim.add_manual_classification_reference operator doesn't have a "type" property, only "obj_type"
2024-05-23 18:31:59 +05:00
Dion Moult c405ff1adf Fix unassign_representation to not incorrectly use remove_deep without first removing known inverses.
In theory, it would be possible for the representation map to be used elsewhere, then file.remove(representation_map) would cause problems.
2024-05-23 23:08:53 +10:00
Dion Moult 02cae9c7c2 Continue to revise optimisation of remove_deep to fix failing tests after a8729933 2024-05-23 22:54:19 +10:00
Dion Moult 3c42962635 Fix #4716. Fix bug where IfcTester fails to verify negative numbers. 2024-05-23 21:40:19 +10:00
Dion Moult 0336dbabf7 Fix #4722. Things like type elements are not allowed to have openings applied to them. 2024-05-23 20:41:15 +10:00
Dion Moult a8729933af Optimise remove_deep, which can make removing lots of elements 100x faster in scripts. 2024-05-23 20:15:09 +10:00
Andrej730 993ce46fcc improve ux for assigning cost item to product types
1) also consider active object as selected when assigning / unassigning cost item to product types. Typically types are hidden and if you select some type in outliner it will become active but still not selected. Now it will be possible to add this active object without unhiding the entire Types collection.

2) info messages to make UI more responsive
2024-05-23 12:13:19 +05:00
Dion Moult edfd446833 Allow custom qtos without a template 2024-05-23 15:17:08 +10:00
Dion Moult 87c184ef8a Easily add custom psets and props without needing to create a template first
This is not best practice but is useful for quick and dirty properties.
2024-05-23 13:28:59 +10:00
Andrej730 e9b9113df8 small fix for 1aaccba80 2024-05-22 21:41:07 +05:00
Thomas Krijnen f772a53dd5 Update ci.yml 2024-05-22 17:29:15 +02:00
Thomas Krijnen 85c6b6c4da Update ci.yml 2024-05-22 15:58:34 +02:00
Andrej730 1aaccba803 simpler way to check if enum property is valid
Well, it's simpler to use but not really simpler in implementation 😅
Thanks to @Gorgious56 for helping out
Hopefully, it's a farewell to those annoying warnings

see https://blenderartists.org/t/best-way-to-handle-dynamic-enum-items-without-pyrna-enum-to-py-current-value-matches-no-enum
2024-05-22 18:44:12 +05:00
Andrej730 f2eb08a066 typing 2024-05-22 17:43:05 +05:00
Andrej730 f460676e54 fix calculating cost summary #4704 (was confusing 0 qty and no qty)
similar to 295d0a677
2024-05-22 17:37:32 +05:00
Andrej730 382e028b90 csv to cost schedule - support more cases of 0 quantity #4704
Before - https://i.imgur.com/CYwSGxY.png
After - https://i.imgur.com/Wbe6Ij6.png
I'll attach example .csv and .ifc in #4704

What changed:

1) If query was provided but it didn't found any elements, then it will still autoassign quantity = 0 instead of cost item end up without quantities at all (which has a different meaning in ifc). Works both with Property provided and without it.

2) If provided quantity = 0, it will now load as quantity = 0 instead of not creating any quantities at all.

3) You can provide both Query and Quantity and they all will be added to the cost item. E.g. if Quantity = 15, Query = "IfcWall", Property="Prop" and there are 3 walls in the model each having Prop = 25 then final quantity will be 15+25*3=90. Previously Quantity would take the priority and the result would be just = 15.

Though this is still doesn't work with counting quantities due behaviour in cost.assign_cost_item_quantity.
E.g. if Quantity = 7, Query = "IfcWall", Property="" (to make sure it will just count them) and there are 3 walls in the model then final quantity will be not 7+3=10 but just = 3, as query will take the priority here.
2024-05-22 17:15:42 +05:00
Thomas Krijnen 53d02446bf Update ci.yml 2024-05-22 13:13:18 +02:00
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
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
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
273 changed files with 3727 additions and 1925 deletions
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install src/bcf --no-deps
pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip
@@ -90,7 +90,7 @@ jobs:
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
-DCOLLADA_SUPPORT=Off \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add1" \
"-DSCHEMA_VERSIONS=2x3;4;4x3;4x3_add1;4x3_add2" \
-DGLTF_SUPPORT=On \
-DJSON_INCLUDE_DIR=/usr/include \
-DCGAL_INCLUDE_DIR=/usr/include \
+6 -3
View File
@@ -36,7 +36,9 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
# use extra version to make pre-release using eg semver
set(EXTRA_VERSION "-alpha.3")
if(NOT DEFINED EXTRA_VERSION)
set(EXTRA_VERSION "-alpha.3")
endif()
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
option(WASM_BUILD "Build a WebAssembly binary." OFF)
@@ -1147,9 +1149,10 @@ endif()
# Packaging
list(APPEND CPACK_SOURCE_IGNORE_FILES
.git
.gitignore
"/\\\\.git"
"/build/"
)
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}${EXTRA_VERSION}")
SET(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}")
+1 -1
View File
@@ -13,7 +13,7 @@ readme = "README.md"
requires-python = ">=3.8"
keywords = ["IFC", "BCF", "BIM"]
dependencies = [
"xsdata",
"xsdata>=24.4",
"numpy",
"ifcopenshell",
]
+7 -5
View File
@@ -10,14 +10,14 @@ from xsdata.formats.dataclass.serializers.config import SerializerConfig
def build_xml_parser(context: Optional[XmlContext] = None) -> XmlParser:
"""Return a parser for an XML file."""
parser = XmlParser(context=context or XmlContext())
parser.register_namespace("xs", "http://www.w3.org/2001/XMLSchema")
parser.register_namespace(ns_map=parser.ns_map, prefix="xs", uri="http://www.w3.org/2001/XMLSchema")
return parser
def build_serializer(context: Optional[XmlContext] = None) -> XmlSerializer:
"""Return a serializer for an XML file."""
return XmlSerializer(
config=SerializerConfig(pretty_print=True),
config=SerializerConfig(indent=" "),
context=context or XmlContext(),
)
@@ -36,8 +36,9 @@ class AbstractXmlParserSerializer(Protocol):
xml: The XML file as bytes.
clazz: The class to parse to.
"""
...
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
def serialize(self, obj: object, ns_map: Optional[dict[str, str]] = None) -> str:
"""
Serialize an object to XML.
@@ -48,6 +49,7 @@ class AbstractXmlParserSerializer(Protocol):
Returns:
The XML as string.
"""
...
class XmlParserSerializer:
@@ -68,7 +70,7 @@ class XmlParserSerializer:
"""
return self.parser.from_bytes(xml, clazz)
def serialize(self, obj: T, ns_map: Optional[dict[str, str]] = None) -> str:
def serialize(self, obj: object, ns_map: Optional[dict[Optional[str], str]] = None) -> str:
"""
Serialize an object to XML.
@@ -79,5 +81,5 @@ class XmlParserSerializer:
Returns:
The XML as string.
"""
ns_map = ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
ns_map = ns_map or self.parser.ns_map or {"xs": "http://www.w3.org/2001/XMLSchema"}
return self.serializer.render(obj, ns_map)
+6 -29
View File
@@ -277,34 +277,6 @@ endif
cp -r dist/working/pyparsing-2.4.5/pyparsing.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/5b/ef/f97c3e1a7efa00e989a793fe15297214fc95ad7d9e3810586bd08ce9f0f3/xmlschema-2.0.2.tar.gz
cd dist/working && tar -xzvf xmlschema*
cp -r dist/working/xmlschema-2.0.2/xmlschema dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/ad/c7/17c9d16320d8e2cfdb27d2fd298b5e8f7a3f211025b0c1bc7a39a85bd690/xsdata-22.11.tar.gz
cd dist/working && tar -xzvf xsdata*
cp -r dist/working/xsdata-22.11/xsdata dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/11/bc/5afb61dd5d863e5cf77cd952445c50c17e65953405986f19e97e4389692a/elementpath-3.0.2.tar.gz
cd dist/working && tar -xzvf elementpath*
cp -r dist/working/elementpath-3.0.2/elementpath dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by bcf
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/21/9f/b251f7f8a76dec1d6651be194dfba8fb8d7781d10ab3987190de8391d08e/six-1.14.0.tar.gz
cd dist/working && tar -xzvf six*
cp -r dist/working/six-1.14.0/six.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFCCSV and ifcopenshell.util.selector
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/18/4d/8d522136c37d9e1ea74062b41b8d5e1318ebf45063ae46ce72ed60af223b/lark-parser-0.8.5.tar.gz
@@ -366,7 +338,12 @@ endif
# Provides Brickschema functionality
cd dist/working && . env/bin/activate && $(PIP) install "brickschema[persistence]==0.7.6a2" --target=./site-packages
# Required for SVG to DXF conversion
cd dist/working && . env/bin/activate && $(PIP) install "ezdxf" --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install ezdxf --target=./site-packages
# Required by bcf
cd dist/working && . env/bin/activate && $(PIP) install xsdata --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install xmlschema --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install elementpath --target=./site-packages
cd dist/working && . env/bin/activate && $(PIP) install six --target=./site-packages
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
+25 -12
View File
@@ -120,11 +120,16 @@ if sys.modules.get("bpy", None):
bl_context = "scene"
def draw(self, context):
layout = self.layout
layout.label(text="BlenderBIM could not load.", icon="ERROR")
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
info = get_debug_info()
layout = self.layout
layout.alert = True
layout.label(text="BlenderBIM could not load.", icon="ERROR")
if info["os"] == "Windows":
layout.operator("wm.console_toggle", text="View the console for full logs.", icon="CONSOLE")
else:
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
py = ".".join(info["python_version"].split(".")[0:2])
b3d = ".".join(info["blender_version"].split(".")[0:2])
box.label(text=f"Blender {b3d} {info['os']} {info['machine']}", icon="BLENDER")
@@ -133,6 +138,21 @@ if sys.modules.get("bpy", None):
op = layout.operator("bim.open_uri", text="How Can I Fix This?")
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html#installation-issues"
layout.label(text="Try Reinstalling:", icon="IMPORT")
op = layout.operator("bim.open_uri", text="Re-download Add-on")
bbim_date = info["blenderbim_version"].split(".")[-1]
py_tag = py.replace(".", "")
if "Linux" in info["os"]:
os = "linux"
elif "Darwin" in info["os"]:
if "arm64" in info["machine"]:
os = "macosm1"
else:
os = "macos"
else:
os = "win"
op.uri = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-{bbim_date}/blenderbim-{bbim_date}-py{py_tag}-{os}.zip"
class OpenUri(bpy.types.Operator):
bl_idname = "bim.open_uri"
bl_label = "Open URI"
@@ -149,14 +169,7 @@ if sys.modules.get("bpy", None):
def execute(self, context):
info = format_debug_info(get_debug_info())
if platform.system() == "Windows":
command = "echo | set /p nul=" + info
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
context.window_manager.clipboard = info
return {"FINISHED"}
class HiddenPanel:
@@ -0,0 +1,45 @@
/*
* BlenderBIM Add-on - OpenBIM Blender Add-on
* Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
*
* This file is part of BlenderBIM Add-on.
*
* BlenderBIM Add-on is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* You may copy this `schedule.css` template alongside your input schedule
* document with the same filename. For example if you have a schedule called
* `door_types.ods`, you can create a `door_types.css` in the same folder to
* style that schedule.
*/
/**
* If you specify a font size in CSS, such as text { font-size: 5; }, all fonts
* will be overriden to match that size.
*
* If your CSS, ODS, XLSX does not specify a font size, the variables below
* will specify the default font size.
*
* If your ODS, XLSX does specify a font size, they will scale linearly based
* on the variables below.
*/
:root {
--font-size-pt: 12;
--font-size-px: 4.13;
--font-width: 0.45;
}
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; }
+6 -2
View File
@@ -29,7 +29,7 @@ from mathutils import geometry
from mathutils import Vector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from typing import Optional, Callable, Any
from typing import Optional, Callable, Any, Union
def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None):
@@ -91,7 +91,11 @@ def import_attributes(ifc_class, props, data, callback=None):
# A more elegant attribute importer signature, intended to supersede import_attributes
def import_attributes2(element, props, callback=None):
def import_attributes2(
element: Union[str, ifcopenshell.entity_instance],
props: bpy.types.PropertyGroup,
callback: Optional[Callable] = None,
) -> None:
if isinstance(element, str):
attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes()
info = {a.name(): None for a in attributes}
+21 -13
View File
@@ -26,6 +26,7 @@ import bmesh
import logging
import mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
@@ -303,22 +304,22 @@ class IfcImporter:
self.update_progress(100)
bpy.context.window_manager.progress_end()
def is_element_far_away(self, element):
def is_element_far_away(self, element: ifcopenshell.entity_instance) -> bool:
try:
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
point = placement[:, 3][0:3]
return self.is_point_far_away(point, is_meters=False)
except:
pass
return False
def is_point_far_away(self, point, is_meters=True):
def is_point_far_away(
self, point: Union[ifcopenshell.entity_instance, npt.NDArray[np.float64]], is_meters: bool = True
) -> bool:
# Locations greater than 1km are not considered "small sites" according to the georeferencing guide
# Users can configure this if they have to handle larger sites but beware of surveying precision
limit = self.ifc_import_settings.distance_limit
limit = limit if is_meters else (limit / self.unit_scale)
coords = point
if hasattr(point, "Coordinates"):
coords = point.Coordinates
coords = getattr(point, "Coordinates", point)
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
def process_context_filter(self):
@@ -608,7 +609,9 @@ class IfcImporter:
threshold = 10000 # Just from experience.
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
# The check for CfsFaces/Faces/CoordIndex accommodates invalid data from Cadwork
# 0 IfcClosedShell.CfsFaces
faces = [len(faces) for e in self.file.by_type("IfcClosedShell") if (faces := e[0])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
@@ -616,12 +619,14 @@ class IfcImporter:
if self.file.schema == "IFC2X3":
return
faces = [len(e.Faces) for e in self.file.by_type("IfcPolygonalFaceSet")]
# 2 IfcPolygonalFaceSet.Faces
faces = [len(faces) for e in self.file.by_type("IfcPolygonalFaceSet") if (faces := e[2])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
faces = [len(e.CoordIndex) for e in self.file.by_type("IfcTriangulatedFaceSet")]
# 3 IfcTriangulatedFaceSet.CoordIndex
faces = [len(index) for e in self.file.by_type("IfcTriangulatedFaceSet") if (index := e[3])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
@@ -679,7 +684,7 @@ class IfcImporter:
props.blender_orthogonal_height = str(offset_point[2])
props.has_blender_offset = True
def get_offset_point(self):
def get_offset_point(self) -> Union[npt.NDArray[np.float64], None]:
elements_checked = 0
# If more than these elements aren't far away, the file probably isn't absolutely positioned
element_checking_threshold = 10
@@ -714,7 +719,7 @@ class IfcImporter:
if self.is_point_far_away(point, is_meters=False):
return point
def does_element_likely_have_geometry_far_away(self, element):
def does_element_likely_have_geometry_far_away(self, element: ifcopenshell.entity_instance) -> bool:
for representation in element.Representation.Representations:
items = []
for item in representation.Items:
@@ -731,13 +736,14 @@ class IfcImporter:
if subelement.is_a("IfcCartesianPoint"):
if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False):
return True
return False
def apply_blender_offset_to_matrix_world(self, obj: bpy.types.Object, matrix: np.ndarray) -> mathutils.Matrix:
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
if obj.data and obj.data.get("has_cartesian_point_offset", None):
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
elif self.is_point_far_away((matrix[0, 3], matrix[1, 3], matrix[2, 3])):
elif self.is_point_far_away((matrix[:3, 3])):
obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
@@ -750,7 +756,9 @@ class IfcImporter:
return mathutils.Matrix(matrix.tolist())
def find_decomposed_ifc_class(self, element, ifc_class):
def find_decomposed_ifc_class(
self, element: ifcopenshell.entity_instance, ifc_class: str
) -> Union[ifcopenshell.entity_instance, None]:
if element.is_a(ifc_class):
return element
rel_aggregates = element.IsDecomposedBy
@@ -100,7 +100,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
@@ -144,7 +144,7 @@ class BIM_PT_linked_aggregate(Panel):
row.label(text="Not a Linked Aggregate")
else:
row.label(text=f"{Number_Linked_Aggregates} Linked Aggregates")
op = row.operator("bim.object_duplicate_move_linked_aggregate", text="", icon="DUPLICATE")
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")
@@ -27,15 +27,12 @@ classes = (
operator.CopyAttributeToSelection,
prop.BIMAttributeProperties,
ui.BIM_PT_object_attributes,
ui.BIM_PT_material_attributes,
)
def register():
bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
bpy.types.Material.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
def unregister():
del bpy.types.Object.BIMAttributeProperties
del bpy.types.Material.BIMAttributeProperties
@@ -23,7 +23,6 @@ import blenderbim.tool as tool
def refresh():
AttributesData.is_loaded = False
MaterialAttributesData.is_loaded = False
class AttributesData:
@@ -51,28 +50,3 @@ class AttributesData:
key = "STEP ID"
results.append({"name": key, "value": str(value)})
return results
class MaterialAttributesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def ifc_definition_id(cls):
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
@classmethod
def attributes(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
data = element.get_info()
for key, value in data.items():
if value is None or isinstance(value, ifcopenshell.entity_instance) or key in ["id", "type"]:
continue
results.append({"name": key, "value": str(value)})
return results
@@ -40,14 +40,10 @@ class EnableEditingAttributes(bpy.types.Operator):
bl_label = "Enable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
self.file = IfcStore.get_file()
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.attributes.clear()
@@ -85,13 +81,9 @@ class DisableEditingAttributes(bpy.types.Operator):
bl_label = "Disable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
return {"FINISHED"}
@@ -102,14 +94,10 @@ class EditAttributes(bpy.types.Operator, Operator):
bl_label = "Edit Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
@@ -126,7 +114,7 @@ class EditAttributes(bpy.types.Operator, Operator):
attributes = blenderbim.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
bpy.ops.bim.disable_editing_attributes(obj=obj.name)
return {"FINISHED"}
@@ -19,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"])
@@ -155,7 +155,7 @@ class ReferenceUI:
blenderbim.bim.helper.draw_attributes(self.props.reference_attributes, self.layout)
row = self.layout.row(align=True)
op = row.operator("bim.add_manual_classification_reference", text="Save", icon="CHECKMARK")
op.type = self.data.data["object_type"]
op.obj_type = self.data.data["object_type"]
row.operator("bim.disable_adding_manual_classification_reference", text="", icon="CANCEL")
else:
row = self.layout.row()
@@ -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,36 +223,40 @@ 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_description = "Assign cost item to currently selected or active product types"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
prop_name: bpy.props.StringProperty()
def _execute(self, context):
core.assign_cost_item_type(
product_types = core.assign_cost_item_type(
tool.Ifc,
tool.Cost,
tool.Spatial,
cost_item=tool.Ifc.get().by_id(self.cost_item),
prop_name=self.prop_name, # TODO: REVIEW PROP_NAME USABILITY
)
self.report({"INFO"}, f"Cost item was assigned to {len(product_types)} product types.")
class UnassignCostItemType(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_cost_item_type"
bl_label = "Unassign Cost Item Type"
bl_description = "Unassign cost item from currently selected or active product types"
bl_options = {"REGISTER", "UNDO"}
cost_item: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
def _execute(self, context):
core.unassign_cost_item_type(
product_types = core.unassign_cost_item_type(
tool.Ifc,
tool.Cost,
tool.Spatial,
cost_item=tool.Ifc.get().by_id(self.cost_item),
product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [],
product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else None,
)
self.report({"INFO"}, f"Cost item was unassigned from {len(product_types)} product types.")
return {"FINISHED"}
@@ -287,9 +291,11 @@ class UnassignCostItemQuantity(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc,
tool.Cost,
cost_item=tool.Ifc.get().by_id(self.cost_item),
products=[tool.Ifc.get().by_id(self.related_object)]
if self.related_object
else tool.Spatial.get_selected_products(),
products=(
[tool.Ifc.get().by_id(self.related_object)]
if self.related_object
else tool.Spatial.get_selected_products()
),
)
@@ -509,7 +515,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"
@@ -538,7 +544,7 @@ class RemoveCostColumn(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
def _execute(self, context):
def execute(self, context):
core.remove_cost_column(tool.Cost, self.name)
return {"FINISHED"}
@@ -656,13 +662,17 @@ class ExportCostSchedules(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
cost_schedule: bpy.props.IntProperty()
format: bpy.props.EnumProperty("Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
format: bpy.props.EnumProperty(name="Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
directory: bpy.props.StringProperty(subtype="FILE_PATH")
filter_folder: bpy.props.BoolProperty(
name="Filter Folders",
default=True,
)
def execute(self, context):
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
r = core.export_cost_schedules(
tool.Cost, filepath=self.filepath, format=self.format, cost_schedule=cost_schedule
tool.Cost, filepath=self.directory, format=self.format, cost_schedule=cost_schedule
)
if isinstance(r, str):
self.report({"ERROR"}, r)
@@ -676,6 +686,7 @@ class ExportCostSchedules(bpy.types.Operator):
def draw(self, context):
self.layout.label(text="Choose a format")
self.layout.prop(self, "format")
self.layout.label(text="Select a directory.")
class ClearCostItemAssignments(bpy.types.Operator, tool.Ifc.Operator):
@@ -210,7 +210,11 @@ class BIMCostProperties(PropertyGroup):
cost_value_attributes: CollectionProperty(name="Cost Value Attributes", type=Attribute)
cost_value_formula: StringProperty(name="Cost Value Formula")
cost_column: StringProperty(name="Cost Column")
should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
should_show_column_ui: BoolProperty(
name="Should Show Column UI",
description="Display UI for adding cost schedule columns, column names represent a category for cost item values",
default=False,
)
should_show_currency_ui: BoolProperty(name="Should Show Currency UI", default=False)
columns: CollectionProperty(name="Columns", type=StrProperty)
active_column_index: IntProperty(name="Active Column Index")
@@ -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:
@@ -23,6 +23,7 @@ import ifccsv
import logging
import tempfile
import ifcopenshell
import ifcopenshell.util.selector
import blenderbim.tool as tool
import blenderbim.bim.module.drawing.scheduler as scheduler
from blenderbim.bim.ifc import IfcStore
@@ -60,13 +60,7 @@ class CopyDebugInformation(bpy.types.Operator):
print(text)
print("-" * 80)
if platform.system() == "Windows":
command = "echo | set /p nul=" + text
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
context.window_manager.clipboard = text
return {"FINISHED"}
@@ -159,9 +159,7 @@ class BaseDecorator:
objecttype = "NOTDEFINED"
def __init__(self):
self.font_id = blf.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
)
self.font_id = 0 # 0 is the default font
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
@@ -404,7 +402,6 @@ class BaseDecorator:
line_no += 1 if multiline_to_bottom else -1
return
# 0 is the default font, but we're fancier than that
font_id = self.font_id
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
@@ -2014,6 +2011,13 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
drawing_font_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", drawing_font)
if os.path.exists(drawing_font_path):
font_id = blf.load(drawing_font_path)
for decorator in self.decorators.values():
decorator.font_id = font_id
def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance?
@@ -315,9 +315,7 @@ class RasterStyleProperty(enum.Enum):
SPACE_SHADING = "space.shading"
RASTER_STYLE_PROPERTIES_EXCLUDE = (
"scene.render.filepath",
)
RASTER_STYLE_PROPERTIES_EXCLUDE = ("scene.render.filepath",)
class DocProperties(PropertyGroup):
@@ -371,6 +369,7 @@ class DocProperties(PropertyGroup):
default=os.path.join("drawings", "assets", "shading_styles.json"), name="Default Shading Styles"
)
shadingstyle_default: StringProperty(default="Blender Default", name="Default Shading Style")
drawing_font: StringProperty(default="OpenGost Type B TT.ttf", name="Drawing Font")
class BIMCameraProperties(PropertyGroup):
@@ -16,22 +16,21 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
import os
import re
import bpy
import string
import svgwrite
import openpyxl
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
from odf.opendocument import load as load_ods
from odf.table import Table, TableRow, TableColumn, TableCell
from odf.text import P
from odf.style import Style
from textwrap import wrap
from pathlib import Path
import string
FONT_SIZE = 4.13
FONT_WIDTH = lambda size: size * 0.45
FONT_SIZE_PT = 12
FONT_FAMILY = "OpenGost Type B TT"
DEBUG = False
@@ -63,11 +62,29 @@ class Scheduler:
)
self.padding = 1
self.margin = 1
self.parse_css(infile)
if infile.endswith("ods"):
self.schedule_ods(infile, outfile)
elif infile.endswith("xlsx"):
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile):
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "schedule.css")
with open(stylesheet_path, "r") as stylesheet:
css = stylesheet.read()
matches = re.search("--font-size-pt:\s*([0-9.]+);", css)
self.font_size_pt = float(matches.groups()[0]) if matches else 12 # Default to 12pt
matches = re.search("--font-size-px:\s*([0-9.]+);", css)
self.font_size_px = float(matches.groups()[0]) if matches else 4.13 # Magic number 4.13px ~= 12pt
matches = re.search("--font-width:\s*([0-9.]+);", css)
self.font_width = float(matches.groups()[0]) if matches else 0.45 # A magic number for OpenGost
self.svg.defs.add(self.svg.style(css))
def schedule_xlsx(self, infile, outfile):
workbook = openpyxl.open(infile, data_only=True)
sheet = workbook.active
@@ -144,8 +161,8 @@ class Scheduler:
x += unmerged_width
continue
font_size = cell.font.size or 11 # 11pt default
font_size = font_size / FONT_SIZE_PT * FONT_SIZE # Magic?
font_size = cell.font.size or self.font_size_pt # 12pt default
font_size = font_size / self.font_size_pt * self.font_size_px # Magic?
text_position = [0.0, 0.0]
if cell.alignment.horizontal == "left":
@@ -424,9 +441,9 @@ class Scheduler:
italic_text = final_cell_style.get("font-style", None) == "italic"
# NOTE: very naive since we're scaling text proportionally
font_size = (
float(final_cell_style.get("font-size", f"{FONT_SIZE_PT}pt")[:-2])
/ FONT_SIZE_PT
* FONT_SIZE
float(final_cell_style.get("font-size", f"{self.font_size_pt}pt")[:-2])
/ self.font_size_pt
* self.font_size_px
)
if p_tags:
@@ -523,10 +540,7 @@ class Scheduler:
"""
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
text_params = {
"font-size": font_size,
"font-family": FONT_FAMILY,
}
text_params = {"font-size": font_size}
if bold:
text_params["font-weight"] = "bold"
if italic:
@@ -546,13 +560,13 @@ class Scheduler:
text_tag = self.svg.text("", **(text_params | {"font-size": "0"} | box_alignment_params))
# TODO: should be done in less naive way
# without using magic number for FONT_WIDTH
# currently it might not work for all fonts and font sizes
# TODO: Should be done without using magic number for self.font_width
if wrap_text:
wrapped_lines = []
for line in text_lines:
wrapped_line = wrap(line, width=int(cell_width // FONT_WIDTH(font_size)), break_long_words=False)
wrapped_line = wrap(
line, width=int(cell_width // (font_size * self.font_width)), break_long_words=False
)
wrapped_lines.extend(wrapped_line)
else:
wrapped_lines = text_lines
@@ -29,6 +29,7 @@ classes = (
operator.GetRepresentationIfcParameters,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
operator.DuplicateLinkedAggregateTo3dCursor,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
@@ -400,7 +400,11 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
representation_data["profile_set_usage"] = tool.Geometry.get_profile_set_usage(product)
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
# TODO: replace with core.add_representation?
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
if new_representation is None:
self.report({"ERROR"}, "Error creating representation for Blender object.")
return {"CANCELLED"}
if tool.Geometry.is_body_representation(new_representation):
[
@@ -938,7 +942,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0],pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
@@ -973,15 +977,15 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_description = "Create a new linked aggregate"
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)
@@ -996,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"
@@ -1050,7 +1054,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if self.group_name in product_groups_name:
return
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name)
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.group_name)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
@@ -1109,6 +1113,16 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
]
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"}
@@ -1135,11 +1149,29 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
custom_incremental_naming_for_element_assembly(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
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"
bl_label = "IFC Refresh Linked Aggregate"
@@ -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))
@@ -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()
@@ -121,9 +121,10 @@ class AuthoringData:
def type_thumbnail(cls):
if not cls.data["relating_type_id"]:
return 0
if not tool.Blender.enum_property_has_valid_index(cls.props, "relating_type_id", cls.data["relating_type_id"]):
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
if relating_type_id is None:
return 0
element = tool.Ifc.get().by_id(int(cls.props.relating_type_id))
element = tool.Ifc.get().by_id(int(relating_type_id))
return cls.type_thumbnails.get(element.id(), None) or 0
@classmethod
@@ -251,8 +252,8 @@ class AuthoringData:
@classmethod
def predefined_type(cls):
relating_type_id = cls.props.relating_type_id
if not relating_type_id:
relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id")
if relating_type_id is None:
return
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
if not hasattr(relating_type, "PredefinedType"):
@@ -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 = 0.0
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: float = 0.0,
) -> 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 = max(1.2, opening_thickness_si)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
@@ -346,7 +361,9 @@ class FilledOpeningGenerator:
return True
return False
def get_existing_opening_occurrence_if_any(self, filling):
def get_existing_opening_occurrence_if_any(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
filling_type = ifcopenshell.util.element.get_type(filling)
if filling_type:
filling_occurrences = ifcopenshell.util.element.get_types(filling_type)
@@ -138,7 +138,10 @@ class AddConstrTypeInstance(bpy.types.Operator):
if not relating_type_id:
return {"FINISHED"}
if self.from_invoke:
# Check relating_type_id enum_items since it's possible
# that we're adding e.g. IfcRoofType being in a Slab Tool
# and roof type id won't be present in the relating_type_id enum.
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
props.relating_type_id = str(self.relating_type_id)
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
@@ -931,13 +931,11 @@ class PatchNonParametricMepSegment(bpy.types.Operator, tool.Ifc.Operator):
return context.active_object
def _execute(self, context):
styles = tool.Geometry.get_styles(context.active_object)
blenderbim.core.material.patch_non_parametric_mep_segment(
tool.Ifc, tool.Material, tool.Profile, obj=context.active_object
)
bpy.ops.bim.enable_editing_extrusion_axis()
bpy.ops.bim.edit_extrusion_axis()
styles = tool.Geometry.get_styles(context.active_object)
class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
@@ -60,7 +60,7 @@ def update_ifc_class(self, context):
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
if not tool.Blender.enum_property_has_valid_index(self, "relating_type_id", AuthoringData.data["relating_type_id"]):
if tool.Blender.get_enum_safe(self, "relating_type_id") is None:
self["relating_type_id"] = 0
@@ -535,5 +535,5 @@ class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMRailingProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -757,7 +757,7 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMRoofProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -337,6 +337,6 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMStairProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -590,6 +590,6 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMWindowProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -36,8 +36,8 @@ classes = (
operator.EnableCulling,
operator.EnableEditingHeader,
operator.ExportIFC,
operator.ExportIFCDeprecated,
operator.FlipClippingPlane,
operator.ImportIFC,
operator.LinkIfc,
operator.LoadLink,
operator.LoadLinkedProject,
@@ -97,7 +97,7 @@ def register():
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Window", space_type="EMPTY")
kmi = km.keymap_items.new("export_ifc.bim", "S", "PRESS", ctrl=True)
kmi = km.keymap_items.new("bim.save_project", "S", "PRESS", ctrl=True)
kmi.properties.should_save_as = False
addon_keymaps.append((km, kmi))
@@ -1106,8 +1106,8 @@ class ToggleLinkVisibility(bpy.types.Operator):
]
class ExportIFC(bpy.types.Operator):
bl_idname = "export_ifc.bim"
class ExportIFCBase:
bl_idname = "bim.save_project"
bl_label = "Save IFC"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
@@ -1224,14 +1224,20 @@ class ExportIFC(bpy.types.Operator):
return "Save the IFC file. Will save both .IFC/.BLEND files if synced together"
class ImportIFC(bpy.types.Operator):
bl_idname = "import_ifc.bim"
bl_label = "Import IFC"
bl_options = {"REGISTER", "UNDO"}
class ExportIFC(ExportIFCBase, bpy.types.Operator):
pass
# 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.save_project", 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.save_project", text="Save IFC Project As...")
op.should_save_as = True
self.layout.separator()
self.layout.operator("bim.revert_project")
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AddProposedProp,
operator.AddPset,
operator.AddQto,
operator.CopyPropertyToSelection,
@@ -39,14 +40,6 @@ classes = (
prop.IfcPropertyEnumeratedValue,
prop.IfcProperty,
prop.PsetProperties,
prop.MaterialPsetProperties,
prop.MaterialSetPsetProperties,
prop.MaterialSetItemPsetProperties,
prop.TaskPsetProperties,
prop.ResourcePsetProperties,
prop.GroupPsetProperties,
prop.ProfilePsetProperties,
prop.WorkSchedulePsetProperties,
prop.RenameProperties,
prop.AddEditProperties,
prop.DeletePsets,
@@ -71,14 +64,14 @@ classes = (
def register():
bpy.types.Object.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetPsetProperties = bpy.props.PointerProperty(type=prop.MaterialSetPsetProperties)
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.MaterialSetItemPsetProperties)
bpy.types.Material.PsetProperties = bpy.props.PointerProperty(type=prop.MaterialPsetProperties)
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.TaskPsetProperties)
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.ResourcePsetProperties)
bpy.types.Scene.GroupPsetProperties = bpy.props.PointerProperty(type=prop.GroupPsetProperties)
bpy.types.Scene.ProfilePsetProperties = bpy.props.PointerProperty(type=prop.ProfilePsetProperties)
bpy.types.Scene.WorkSchedulePsetProperties = bpy.props.PointerProperty(type=prop.WorkSchedulePsetProperties)
bpy.types.Object.MaterialSetPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Object.MaterialSetItemPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Material.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.GroupPsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.ProfilePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.WorkSchedulePsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties)
bpy.types.Scene.RenameProperties = bpy.props.CollectionProperty(type=prop.RenameProperties)
bpy.types.Scene.AddEditProperties = bpy.props.CollectionProperty(type=prop.AddEditProperties)
bpy.types.Scene.DeletePsets = bpy.props.CollectionProperty(type=prop.DeletePsets)
@@ -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 = {}
@@ -310,6 +144,8 @@ class EditPset(bpy.types.Operator, Operator):
for key, value in properties.items():
if isinstance(value, float):
properties[key] = round(value, 4)
elif not isinstance(value, int):
properties[key] = 0
ifcopenshell.api.run(
"pset.edit_qto",
self.file,
@@ -341,7 +177,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,11 +208,9 @@ 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)
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)
qto_name = tool.Pset.get_pset_name(self.obj, self.obj_type, pset_type="QTO")
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=props.qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
pset_id=0, pset_name=qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
)
@@ -634,3 +468,17 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator):
self.report({"INFO"}, "Finished applying changes")
return {"FINISHED"}
class AddProposedProp(bpy.types.Operator):
bl_idname = "bim.add_proposed_prop"
bl_label = "Add Proposed Prop"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
prop_name: bpy.props.StringProperty()
prop_value: bpy.props.StringProperty()
def execute(self, context):
core.add_proposed_prop(tool.Pset, self.obj, self.obj_type, self.prop_name, self.prop_value)
return {"FINISHED"}
@@ -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
@@ -58,6 +59,30 @@ def blender_formatted_enum_from_psets(psets):
def get_pset_name(self, context):
pset_type = repr(self)
prop_type = pset_type.split(".")[-1]
results = []
if "bpy.data.objects" in pset_type:
if prop_type == "PsetProperties":
results = get_object_pset_name(self, context)
elif prop_type == "MaterialSetPsetProperties":
results = get_material_set_pset_names(self, context)
elif prop_type == "MaterialSetItemPsetProperties":
results = get_material_set_item_pset_names(self, context)
elif "bpy.data.materials" in pset_type:
results = get_material_pset_names(self, context)
elif prop_type == "ResourcePsetProperties":
results = get_resource_pset_names(self, context)
elif prop_type == "GroupPsetProperties":
results = get_group_pset_names(self, context)
elif prop_type == "ProfilePsetProperties":
results = get_profile_pset_names(self, context)
elif prop_type == "WorkSchedulePsetProperties":
results = get_work_schedule_pset_names(self, context)
return [("BBIM_CUSTOM", "Custom Pset", "Create a property set without using a template."), None] + results
def get_object_pset_name(self, context):
if not ObjectPsetsData.is_loaded:
ObjectPsetsData.load()
return ObjectPsetsData.data["pset_name"]
@@ -167,6 +192,21 @@ def get_work_schedule_pset_names(self, context):
def get_qto_name(self, context):
pset_type = repr(self)
prop_type = pset_type.split(".")[-1]
if "bpy.data.objects" in pset_type:
if prop_type == "PsetProperties":
results = get_object_qto_name(self, context)
elif prop_type == "TaskPsetProperties":
results = get_task_qto_names(self, context)
elif prop_type == "ResourcePsetProperties":
results = get_resource_qto_names(self, context)
elif prop_type == "GroupPsetProperties":
results = get_group_qto_names(self, context)
return [("BBIM_CUSTOM", "Custom Qto", "Create a quantity set without using a template."), None] + results
def get_object_qto_name(self, context):
if not ObjectPsetsData.is_loaded:
ObjectPsetsData.load()
return ObjectPsetsData.data["qto_name"]
@@ -198,76 +238,14 @@ class IfcProperty(PropertyGroup):
class PsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_has_template: BoolProperty(name="Active Pset Has Template")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_pset_name, name="Pset Name")
qto_name: EnumProperty(items=get_qto_name, name="Qto Name")
class MaterialPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_material_pset_names, name="Pset Name")
class MaterialSetPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_material_set_pset_names, name="Pset Name")
class MaterialSetItemPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_material_set_item_pset_names, name="Pset Name")
class TaskPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
qto_name: EnumProperty(items=get_task_qto_names, name="Qto Name")
class ResourcePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_resource_pset_names, name="Pset Name")
qto_name: EnumProperty(items=get_resource_qto_names, name="Qto Name")
class GroupPsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_group_pset_names, name="Pset Name")
qto_name: EnumProperty(items=get_group_qto_names, name="Qto Name")
class ProfilePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_profile_pset_names, name="Pset Name")
class WorkSchedulePsetProperties(PropertyGroup):
active_pset_id: IntProperty(name="Active Pset ID")
active_pset_name: StringProperty(name="Pset Name")
active_pset_type: StringProperty(name="Active Pset Type")
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_work_schedule_pset_names, name="Pset Name")
prop_name: StringProperty(name="Property Name", default="MyProperty")
prop_value: StringProperty(name="Property Value", default="Some Value")
class RenameProperties(PropertyGroup):
@@ -101,6 +101,7 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
if props.active_pset_id == pset_id:
row.prop(props, "active_pset_name", icon="COPY_ID", text="")
op = row.operator("bim.edit_pset", icon="CHECKMARK", text="")
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
op = row.operator("bim.disable_pset_editing", icon="CANCEL", text="")
@@ -132,6 +133,16 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
if props.active_pset_id == pset_id:
for prop in props.properties:
draw_psetqto_editable_ui(box, props, prop)
if not props.active_pset_has_template:
row = box.row(align=True)
row.prop(props, "prop_name", text="")
row.prop(props, "prop_value", text="")
op = row.operator("bim.add_proposed_prop", text="", icon="ADD")
op.obj = obj_name
op.obj_type = obj_type
op.prop_name = props.prop_name
op.prop_value = props.prop_value
else:
has_props_displayed = False
for prop in pset["Properties"]:
@@ -143,7 +154,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()
@@ -190,4 +190,4 @@ class BIMPsetTemplateProperties(PropertyGroup):
active_prop_template_id: IntProperty(name="Active Prop Template Id")
active_pset_template: PointerProperty(type=PsetTemplate)
active_prop_template: PointerProperty(type=PropTemplate)
new_template_filename: StringProperty("New TemplateFileName")
new_template_filename: StringProperty(name="New TemplateFileName")
@@ -19,6 +19,7 @@
import bpy
import blenderbim.tool as tool
import ifcopenshell
import ifcopenshell.util.resource
def refresh():
@@ -28,7 +29,6 @@ def refresh():
class ResourceData:
data = {}
is_loaded = False
cost_values = {}
@classmethod
def load(cls):
@@ -139,7 +139,7 @@ class ResourceData:
return results
@classmethod
def active_resource_ids(cls):
def active_resource_ids(cls) -> list[int]:
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
if not element:
@@ -128,9 +128,9 @@ class ContractResource(bpy.types.Operator):
class AssignResource(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_resource"
bl_label = "Assign Resource"
bl_description = "Assign resource to the selected objects"
bl_options = {"REGISTER", "UNDO"}
resource: bpy.props.IntProperty()
related_object: bpy.props.StringProperty()
def _execute(self, context):
core.assign_resource(tool.Ifc, tool.Spatial, resource=tool.Ifc.get().by_id(self.resource))
@@ -139,9 +139,9 @@ class AssignResource(bpy.types.Operator, tool.Ifc.Operator):
class UnassignResource(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_resource"
bl_label = "Unassign Resource"
bl_description = "Unassign resource from the selected objects"
bl_options = {"REGISTER", "UNDO"}
resource: bpy.props.IntProperty()
related_object: bpy.props.StringProperty()
def _execute(self, context):
core.unassign_resource(tool.Ifc, tool.Spatial, resource=tool.Ifc.get().by_id(self.resource))
@@ -328,7 +328,7 @@ class EditResourceQuantity(bpy.types.Operator, tool.Ifc.Operator):
class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "import_resources.bim"
bl_idname = "bim.import_resources"
bl_label = "Import Resources"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
@@ -51,7 +51,7 @@ class BIM_PT_resources(Panel):
row.operator("bim.disable_resource_editing_ui", text="", icon="CANCEL")
else:
row.operator("bim.load_resources", text="", icon="GREASEPENCIL")
row.operator("import_resources.bim", text="", icon="IMPORT")
row.operator("bim.import_resources", text="", icon="IMPORT")
if not self.props.is_editing:
return
@@ -20,6 +20,7 @@ import re
import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.selector
@@ -214,7 +215,7 @@ class SaveSearch(Operator, tool.Ifc.Operator):
group = group[0]
group.Description = description
else:
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description)
if results:
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=list(results), group=group)
@@ -367,7 +368,7 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
description = json.dumps(
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -634,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
@@ -122,7 +122,7 @@ class ColourRgb(PropertyGroup):
name: StringProperty()
color_value: FloatVectorProperty(size=3, subtype="COLOR", default=(1, 1, 1))
# not exposed in the UI, here just to preserve the data
color_name: StringProperty("Color Name")
color_name: StringProperty(name="Color Name")
# to fit blender.bim.helper.export_attributes
def get_value(self):
@@ -512,10 +512,7 @@ class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
element = tool.Ifc.get().by_id(self.element)
obj = tool.Ifc.get_object(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
if obj:
tool.Ifc.unlink(obj=obj)
bpy.data.objects.remove(obj)
tool.Geometry.delete_ifc_object(obj)
class RenameType(bpy.types.Operator, tool.Ifc.Operator):
@@ -77,6 +77,10 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
self.report({"INFO"}, "You can't add an opening to another opening.")
continue
if not hasattr(element1, "HasOpenings"):
self.report({"INFO"}, f"An {element1.is_a()} is not allowed to have an opening.")
continue
if tool.Ifc.is_moved(obj1):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj1)
+19 -14
View File
@@ -19,6 +19,7 @@
import os
import bpy
import addon_utils
import platform
from pathlib import Path
from bpy.types import Panel
from bpy.props import StringProperty, IntProperty, BoolProperty
@@ -290,8 +291,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row = layout.row()
row.prop(self, "spatial_elements_unselectable")
row = layout.row()
row.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save")
row = layout.row()
@@ -320,28 +319,30 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.prop(context.scene.BIMProperties, "data_dir")
row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.BIMProperties, "pset_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "sheets_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "layouts_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "titleblocks_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "drawings_dir")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "stylesheet_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "markers_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "symbols_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "patterns_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "shadingstyles_path")
row = self.layout.row(align=True)
row = self.layout.row()
row.prop(context.scene.DocProperties, "shadingstyle_default")
row = self.layout.row()
row.prop(context.scene.DocProperties, "drawing_font")
# Scene panel groups
@@ -406,10 +407,14 @@ class BIM_PT_tabs(Panel):
if blenderbim.last_error:
box = self.layout.box()
box.alert=True
row = box.row(align=True)
row.label(text="BlenderBIM experienced an error :(", icon="ERROR")
row.operator("bim.close_error", text="", icon="CANCEL")
box.label(text="View the console for full logs.", icon="CONSOLE")
if platform.system() == "Windows":
box.operator("wm.console_toggle", text="View the console for full logs.", icon="CONSOLE")
else:
box.label(text="View the console for full logs.", icon="CONSOLE")
box.operator("bim.copy_debug_information", text="Copy Error Message To Clipboard")
op = box.operator("bim.open_uri", text="How Can I Fix This?")
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html"
+110 -61
View File
@@ -1,183 +1,230 @@
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, Union
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):
product_types = spatial.get_selected_product_types()
[
def assign_cost_item_type(
ifc: tool.Ifc, cost: tool.Cost, spatial: tool.Spatial, cost_item: ifcopenshell.entity_instance, prop_name
) -> list[ifcopenshell.entity_instance]:
"""
Returns:
List of found product types.
"""
product_types = list(spatial.get_selected_product_types())
rels = [
ifc.run("control.assign_control", relating_control=cost_item, related_object=product_type)
for product_type in product_types
]
cost.load_cost_item_types(cost_item)
return product_types
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: Optional[list[ifcopenshell.entity_instance]] = None,
) -> list[ifcopenshell.entity_instance]:
"""
Returns:
List of found product types.
"""
if not product_types:
product_types = spatial.get_selected_product_types()
product_types = list(spatial.get_selected_product_types())
[
ifc.run("control.unassign_control", relating_control=cost_item, related_object=product_type)
for product_type in product_types
]
cost.load_cost_item_types(cost_item)
return product_types
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 +233,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: str, format: str, 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 +318,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 +326,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 +339,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 +349,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 +357,7 @@ def copy_cost_item(ifc, cost):
cost.load_cost_schedule_tree()
def add_currency(ifc, cost):
def add_currency(ifc: tool.Ifc, cost: tool.Cost):
unit = ifc.run("unit.add_monetary_unit")
attributes = cost.get_currency_attributes()
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
+71 -35
View File
@@ -16,36 +16,44 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from pathlib import Path
import ifcopenshell
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def enable_editing_text(drawing, obj=None):
def enable_editing_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.enable_editing_text(obj)
drawing.import_text_attributes(obj)
def disable_editing_text(drawing, obj=None):
def disable_editing_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.disable_editing_text(obj)
def edit_text(drawing, obj=None):
def edit_text(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.synchronise_ifc_and_text_attributes(obj)
drawing.update_text_size_pset(obj)
drawing.update_text_value(obj)
drawing.disable_editing_text(obj)
def enable_editing_assigned_product(drawing, obj=None):
def enable_editing_assigned_product(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.enable_editing_assigned_product(obj)
drawing.import_assigned_product(obj)
def disable_editing_assigned_product(drawing, obj=None):
def disable_editing_assigned_product(drawing: tool.Drawing, obj: bpy.types.Object) -> None:
drawing.disable_editing_assigned_product(obj)
def edit_assigned_product(ifc, drawing, obj=None, product=None):
def edit_assigned_product(
ifc: tool.Ifc, drawing: tool.Drawing, obj: bpy.types.Object, product: Optional[ifcopenshell.entity_instance] = None
) -> None:
element = ifc.get_entity(obj)
existing_product = drawing.get_assigned_product(element)
if existing_product != product:
@@ -58,16 +66,16 @@ def edit_assigned_product(ifc, drawing, obj=None, product=None):
drawing.disable_editing_assigned_product(obj)
def load_sheets(drawing):
def load_sheets(drawing: tool.Drawing) -> None:
drawing.import_sheets()
drawing.enable_editing_sheets()
def disable_editing_sheets(drawing):
def disable_editing_sheets(drawing: tool.Drawing) -> None:
drawing.disable_editing_sheets()
def add_sheet(ifc, drawing, titleblock: ifcopenshell.entity_instance):
def add_sheet(ifc: tool.Ifc, drawing, titleblock: ifcopenshell.entity_instance) -> None:
sheet = ifc.run("document.add_information")
layout = ifc.run("document.add_reference", information=sheet)
titleblock_reference = ifc.run("document.add_reference", information=sheet)
@@ -93,7 +101,7 @@ def add_sheet(ifc, drawing, titleblock: ifcopenshell.entity_instance):
drawing.import_sheets()
def regenerate_sheet(drawing, sheet=None):
def regenerate_sheet(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
drawing.create_svg_sheet(sheet, drawing.sanitise_filename(Path(titleblock_uri).stem))
try:
@@ -104,11 +112,11 @@ def regenerate_sheet(drawing, sheet=None):
drawing.delete_file(path_layout)
def open_sheet(drawing, sheet=None):
def open_sheet(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
drawing.open_layout_svg(drawing.get_document_uri(sheet, "LAYOUT"))
def remove_sheet(ifc, drawing, sheet=None):
def remove_sheet(ifc: tool.Ifc, drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None:
for reference in drawing.get_document_references(sheet):
if drawing.get_reference_description(reference) in ("LAYOUT", "SHEET", "REVISION", "RASTER"):
uri = ifc.resolve_uri(drawing.get_document_uri(reference))
@@ -118,7 +126,7 @@ def remove_sheet(ifc, drawing, sheet=None):
drawing.import_sheets()
def rename_sheet(ifc, drawing, sheet: ifcopenshell.entity_instance, identification: str, name: str) -> None:
def rename_sheet(ifc: tool.Ifc, drawing, sheet: ifcopenshell.entity_instance, identification: str, name: str) -> None:
if ifc.get_schema() == "IFC2X3":
attributes = {"DocumentId": identification, "Name": name}
else:
@@ -144,30 +152,32 @@ def rename_sheet(ifc, drawing, sheet: ifcopenshell.entity_instance, identificati
drawing.move_file(old_location, ifc.resolve_uri(new_location))
def rename_reference(ifc, drawing, reference=None, identification=None):
def rename_reference(
ifc: tool.Ifc, drawing: tool.Drawing, reference: ifcopenshell.entity_instance, identification: str
) -> None:
attributes = drawing.generate_reference_attributes(reference, Identification=identification)
ifc.run("document.edit_reference", reference=reference, attributes=attributes)
def load_schedules(drawing):
def load_schedules(drawing: tool.Drawing) -> None:
drawing.import_documents("SCHEDULE")
drawing.enable_editing_schedules()
def load_references(drawing):
def load_references(drawing: tool.Drawing) -> None:
drawing.import_documents("REFERENCE")
drawing.enable_editing_references()
def disable_editing_schedules(drawing):
def disable_editing_schedules(drawing: tool.Drawing) -> None:
drawing.disable_editing_schedules()
def disable_editing_references(drawing):
def disable_editing_references(drawing: tool.Drawing) -> None:
drawing.disable_editing_references()
def add_document(ifc, drawing, document_type, uri=None):
def add_document(ifc: tool.Ifc, drawing: tool.Drawing, document_type: tool.Drawing.DOCUMENT_TYPE, uri: str) -> None:
document = ifc.run("document.add_information")
reference = ifc.run("document.add_reference", information=document)
name = drawing.get_path_filename(uri)
@@ -180,34 +190,43 @@ def add_document(ifc, drawing, document_type, uri=None):
drawing.import_documents(document_type)
def remove_document(ifc, drawing, document_type, document=None):
def remove_document(
ifc: tool.Ifc,
drawing: tool.Drawing,
document_type: tool.Drawing.DOCUMENT_TYPE,
document: ifcopenshell.entity_instance,
) -> None:
ifc.run("document.remove_information", information=document)
drawing.import_documents(document_type)
def open_schedule(drawing, schedule=None):
def open_schedule(drawing: tool.Drawing, schedule: ifcopenshell.entity_instance) -> None:
drawing.open_spreadsheet(drawing.get_document_uri(schedule))
def open_reference(drawing, reference=None):
def open_reference(drawing: tool.Drawing, reference: ifcopenshell.entity_instance) -> None:
drawing.open_svg(drawing.get_document_uri(reference))
def update_document_name(ifc, drawing, document=None, name=None):
def update_document_name(
ifc: tool.Ifc, drawing: tool.Drawing, document: ifcopenshell.entity_instance, name=None
) -> None:
if drawing.get_name(document) != name:
ifc.run("document.edit_information", information=document, attributes={"Name": name})
def load_drawings(drawing):
def load_drawings(drawing: tool.Drawing) -> None:
drawing.import_drawings()
drawing.enable_editing_drawings()
def disable_editing_drawings(drawing):
def disable_editing_drawings(drawing: tool.Drawing) -> None:
drawing.disable_editing_drawings()
def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
def add_drawing(
ifc: tool.Ifc, collector: tool.Collector, drawing: tool.Drawing, target_view=None, location_hint=None
) -> None:
drawing_name = drawing.ensure_unique_drawing_name(drawing.generate_drawing_name(target_view, location_hint))
drawing_matrix = drawing.generate_drawing_matrix(target_view, location_hint)
camera = drawing.create_camera(drawing_name, drawing_matrix, location_hint)
@@ -265,7 +284,12 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
drawing.import_drawings()
def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotations=False):
def duplicate_drawing(
ifc: tool.Ifc,
drawing_tool: tool.Drawing,
drawing: ifcopenshell.entity_instance,
should_duplicate_annotations: bool = False,
) -> ifcopenshell.entity_instance:
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
new_drawing = ifc.run("root.copy_class", product=drawing)
drawing_tool.copy_representation(drawing, new_drawing)
@@ -302,7 +326,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati
return new_drawing
def remove_drawing(ifc, drawing_tool, drawing=None):
def remove_drawing(ifc: tool.Ifc, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance) -> None:
if drawing_tool.is_active_drawing(drawing):
drawing_tool.run_drawing_activate_model()
@@ -330,7 +354,9 @@ def remove_drawing(ifc, drawing_tool, drawing=None):
drawing_tool.import_drawings()
def update_drawing_name(ifc, drawing_tool, drawing=None, name=None):
def update_drawing_name(
ifc: tool.Ifc, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance, name=None
) -> None:
if drawing_tool.get_name(drawing) != name:
ifc.run("attribute.edit_attributes", product=drawing, attributes={"Name": name})
group = drawing_tool.get_drawing_group(drawing)
@@ -364,7 +390,13 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None):
drawing_tool.import_sheets()
def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None):
def add_annotation(
ifc: tool.Ifc,
collector: tool.Collector,
drawing_tool: tool.Drawing,
drawing: ifcopenshell.entity_instance,
object_type: str,
) -> None:
target_view = drawing_tool.get_drawing_target_view(drawing)
context = drawing_tool.get_annotation_context(target_view, object_type)
if not context:
@@ -387,12 +419,14 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
drawing_tool.enable_editing(obj)
def build_schedule(drawing, schedule=None):
def build_schedule(drawing: tool.Drawing, schedule: ifcopenshell.entity_instance) -> None:
drawing.create_svg_schedule(schedule)
drawing.open_svg(drawing.get_path_with_ext(drawing.get_document_uri(schedule), "svg"))
def sync_references(ifc, collector, drawing_tool, drawing=None):
def sync_references(
ifc: tool.Ifc, collector: tool.Collector, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance
) -> None:
if not drawing_tool.has_annotation(drawing):
return
@@ -437,11 +471,13 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
drawing_tool.sync_object_representation(reference_obj)
def select_assigned_product(drawing, context):
def select_assigned_product(drawing: tool.Drawing, context: bpy.types.Context) -> None:
drawing.select_assigned_product(context)
def activate_drawing_view(ifc, blender, drawing_tool, drawing):
def activate_drawing_view(
ifc: tool.Ifc, blender: tool.Blender, drawing_tool: tool.Drawing, drawing: ifcopenshell.entity_instance
) -> None:
camera = ifc.get_object(drawing)
if not camera:
camera = drawing_tool.import_drawing(drawing)
+21 -13
View File
@@ -18,10 +18,11 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import blenderbim.core.tool as tool
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def edit_object_placement(
@@ -37,8 +38,15 @@ def edit_object_placement(
def add_representation(
ifc, geometry, style, surveyor, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None
):
ifc: tool.Ifc,
geometry: tool.Geometry,
style: tool.Style,
surveyor: tool.Surveyor,
obj: bpy.types.Object,
context: ifcopenshell.entity_instance,
ifc_representation_class: Optional[str] = None,
profile_set_usage: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
element = ifc.get_entity(obj)
if not element:
return
@@ -47,7 +55,7 @@ def add_representation(
data = geometry.get_object_data(obj)
if not data and ifc_representation_class != "IfcTextLiteral":
raise IncompatibleRepresentationError()
return
representation = ifc.run(
"geometry.add_representation",
@@ -89,15 +97,15 @@ def add_representation(
def switch_representation(
ifc,
geometry,
obj=None,
representation=None,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
):
ifc: tool.Ifc,
geometry: tool.Geometry,
obj: bpy.types.Object,
representation: ifcopenshell.entity_instance,
should_reload: bool = True,
is_global: bool = True,
should_sync_changes_first: bool = False,
apply_openings: bool = True,
) -> None:
"""Function can switch to representation that wasn't yet assigned to that object. See #2766.
`should_sync_changes_first` - sync ifc representation with current state of `obj.data`;
@@ -85,6 +85,7 @@ def enable_editing_material(material_tool, material):
def edit_material(ifc, material_tool, material):
attributes = material_tool.get_material_attributes()
ifc.run("material.edit_material", material=material, attributes=attributes)
material_tool.sync_blender_material_name(material)
material_tool.disable_editing_material()
material_type = material_tool.get_active_material_type()
material_tool.import_material_definitions(material_type)
+26 -2
View File
@@ -31,7 +31,7 @@ def copy_property_to_selection(ifc, pset, is_pset=True, obj=None, pset_name=None
def add_pset(ifc, pset, blender, obj_name, obj_type):
pset_name = pset.get_pset_name(obj_name, obj_type)
pset_name = pset.get_pset_name(obj_name, obj_type, pset_type="PSET")
if obj_type == "Object":
elements = [ifc.get_entity(obj) for obj in blender.get_selected_objects()]
else:
@@ -39,6 +39,30 @@ def add_pset(ifc, pset, blender, obj_name, obj_type):
for element in elements:
if not element:
continue
if not pset.is_pset_applicable(element, pset_name):
if pset_name and 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)
has_template = True
else:
has_template = False
if pset:
pset_tool.import_pset_from_existing(pset, props)
pset_tool.set_active_pset(props, pset, has_template)
else:
pset_tool.enable_proposed_pset(props, pset_name, pset_type, has_template)
def add_proposed_prop(pset, obj_name, obj_type, name, value):
props = pset.get_pset_props(obj_name, obj_type)
pset.add_proposed_property(name, pset.cast_string_to_primitive(value), props)
+78 -33
View File
@@ -18,42 +18,57 @@
# ############################################################################ #
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union, Iterable
def load_resources(resource):
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def load_resources(resource: tool.Resource) -> None:
resource.load_resources()
def add_resource(tool_ifc, resource_tool, ifc_class, parent_resource=None):
def add_resource(
tool_ifc: tool.Ifc,
resource_tool: tool.Resource,
ifc_class,
parent_resource: Optional[ifcopenshell.entity_instance] = None,
) -> None:
tool_ifc.run("resource.add_resource", ifc_class=ifc_class, parent_resource=parent_resource)
resource_tool.load_resources()
def disable_editing_resource(resource_tool):
def disable_editing_resource(resource_tool: tool.Resource) -> None:
resource_tool.disable_editing_resource()
def disable_resource_editing_ui(resource_tool):
def disable_resource_editing_ui(resource_tool: tool.Resource) -> None:
resource_tool.disable_resource_editing_ui()
def enable_editing_resource(resource_tool, resource):
def enable_editing_resource(resource_tool: tool.Resource, resource) -> None:
resource_tool.enable_editing_resource(resource)
resource_tool.load_resource_attributes(resource)
def edit_resource(ifc, resource_tool, resource):
def edit_resource(ifc: tool.Ifc, resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None:
attributes = resource_tool.get_resource_attributes()
ifc.run("resource.edit_resource", resource=resource, attributes=attributes)
resource_tool.load_resource_properties()
resource_tool.disable_editing_resource()
def remove_resource(ifc, resource_tool, resource=None):
def remove_resource(ifc: tool.Ifc, resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None:
ifc.run("resource.remove_resource", resource=resource)
resource_tool.load_resources()
def enable_editing_resource_time(ifc_tool, resource_tool, resource):
def enable_editing_resource_time(
ifc_tool, resource_tool: tool.Resource, resource: ifcopenshell.entity_instance
) -> None:
resource_time = resource_tool.get_resource_time(resource)
if resource_time is None:
resource_time = ifc_tool.run("resource.add_resource_time", resource=resource)
@@ -61,17 +76,21 @@ def enable_editing_resource_time(ifc_tool, resource_tool, resource):
resource_tool.load_resource_time_attributes(resource_time)
def edit_resource_time(ifc, resource_tool, resource_time):
def edit_resource_time(
ifc: tool.Ifc, resource_tool: tool.Resource, resource_time: ifcopenshell.entity_instance
) -> None:
attributes = resource_tool.get_resource_time_attributes()
ifc.run("resource.edit_resource_time", resource_time=resource_time, attributes=attributes)
resource_tool.disable_editing_resource()
def disable_editing_resource_time(resource_tool):
def disable_editing_resource_time(resource_tool: tool.Resource) -> None:
resource_tool.disable_editing_resource()
def calculate_resource_work(ifc, resource_tool, resource):
def calculate_resource_work(
ifc: tool.Ifc, resource_tool: tool.Resource, resource: ifcopenshell.entity_instance
) -> None:
if resource_tool.get_task_assignments(resource):
ifc.run("resource.calculate_resource_work", resource=resource)
else:
@@ -81,92 +100,112 @@ def calculate_resource_work(ifc, resource_tool, resource):
resource_tool.load_resources()
def enable_editing_resource_costs(resource_tool, resource):
def enable_editing_resource_costs(resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None:
resource_tool.enable_editing_resource_costs(resource)
resource_tool.disable_editing_resource_cost_value()
def disable_editing_resource_cost_value(resource_tool):
def disable_editing_resource_cost_value(resource_tool: tool.Resource) -> None:
resource_tool.disable_editing_resource_cost_value()
def enable_editing_resource_cost_value(resource_tool, cost_value):
def enable_editing_resource_cost_value(resource_tool: tool.Resource, cost_value: ifcopenshell.entity_instance) -> None:
resource_tool.enable_editing_cost_value_attributes(cost_value)
resource_tool.load_cost_value_attributes(cost_value)
def enable_editing_resource_cost_value_formula(resource_tool, cost_value):
def enable_editing_resource_cost_value_formula(
resource_tool: tool.Resource, cost_value: ifcopenshell.entity_instance
) -> None:
resource_tool.enable_editing_resource_cost_value_formula(cost_value)
def edit_resource_cost_value_formula(ifc, resource_tool, cost_value):
def edit_resource_cost_value_formula(
ifc: tool.Ifc, resource_tool: tool.Resource, cost_value: ifcopenshell.entity_instance
) -> None:
formula = resource_tool.get_resource_cost_value_formula()
ifc.run("cost.edit_cost_value_formula", cost_value=cost_value, formula=formula)
resource_tool.disable_editing_resource_cost_value()
def edit_resource_cost_value(ifc, resource_tool, cost_value):
def edit_resource_cost_value(
ifc: tool.Ifc, resource_tool: tool.Resource, cost_value: ifcopenshell.entity_instance
) -> None:
attributes = resource_tool.get_resource_cost_value_attributes()
ifc.run("cost.edit_cost_value", cost_value=cost_value, attributes=attributes)
resource_tool.disable_editing_resource_cost_value()
def enable_editing_resource_base_quantity(resource_tool, resource):
def enable_editing_resource_base_quantity(resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None:
resource_tool.enable_editing_resource_base_quantity(resource)
def add_resource_quantity(ifc, ifc_class, resource):
def add_resource_quantity(ifc: tool.Ifc, ifc_class: str, resource: ifcopenshell.entity_instance) -> None:
ifc.run("resource.add_resource_quantity", resource=resource, ifc_class=ifc_class)
def remove_resource_quantity(ifc, resource):
def remove_resource_quantity(ifc: tool.Ifc, resource: ifcopenshell.entity_instance) -> None:
ifc.run("resource.remove_resource_quantity", resource=resource)
def enable_editing_resource_quantity(resource_tool, resource_quantity=None):
def enable_editing_resource_quantity(
resource_tool: tool.Resource, resource_quantity: ifcopenshell.entity_instance
) -> None:
resource_tool.enable_editing_resource_quantity(resource_quantity)
def disable_editing_resource_quantity(resource_tool):
def disable_editing_resource_quantity(resource_tool: tool.Resource) -> None:
resource_tool.disable_editing_resource_quantity()
def edit_resource_quantity(resource_tool, ifc, physical_quantity=None):
def edit_resource_quantity(
resource_tool: tool.Resource, ifc: tool.Ifc, physical_quantity: ifcopenshell.entity_instance
) -> None:
attributes = resource_tool.get_resource_quantity_attributes()
ifc.run("resource.edit_resource_quantity", physical_quantity=physical_quantity, attributes=attributes)
resource_tool.disable_editing_resource_quantity()
def import_resources(resource_tool, file_path):
def import_resources(resource_tool: tool.Resource, file_path: str) -> None:
resource_tool.import_resources(file_path)
resource_tool.load_resources()
def expand_resource(resource_tool, resource):
def expand_resource(resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None:
resource_tool.expand_resource(resource)
resource_tool.load_resources()
def contract_resource(resource_tool, resource):
def contract_resource(resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None:
resource_tool.contract_resource(resource)
resource_tool.load_resources()
def assign_resource(ifc, spatial, resource=None, products=None):
def assign_resource(
ifc: tool.Ifc,
spatial: tool.Spatial,
resource: ifcopenshell.entity_instance,
products: Optional[Iterable[ifcopenshell.entity_instance]] = None,
) -> None:
if not products:
products = spatial.get_selected_products()
for product in products:
rel = ifc.run("resource.assign_resource", relating_resource=resource, related_object=product)
def unassign_resource(ifc, spatial, resource=None, products=None):
def unassign_resource(
ifc: tool.Ifc,
spatial: tool.Spatial,
resource: ifcopenshell.entity_instance,
products: Optional[Iterable[ifcopenshell.entity_instance]] = None,
) -> None:
if not products:
products = spatial.get_selected_products()
for product in products:
ifc.run("resource.unassign_resource", relating_resource=resource, related_object=product)
def edit_productivity_pset(ifc, resource_tool):
def edit_productivity_pset(ifc: tool.Ifc, resource_tool: tool.Resource) -> None:
resource = resource_tool.get_highlighted_resource()
if resource is None:
return
@@ -178,7 +217,9 @@ def edit_productivity_pset(ifc, resource_tool):
ifc.run("pset.edit_pset", pset=pset, properties=resource_tool.get_productivity_attributes())
def add_usage_constraint(ifc, resource_tool, resource=None, reference_path=None):
def add_usage_constraint(
ifc: tool.Ifc, resource_tool: tool.Resource, resource: ifcopenshell.entity_instance, reference_path: str
) -> None:
metric = resource_tool.has_metric_constraint(resource, "Usage")
if metric:
return print("Must remove existing metric first")
@@ -198,7 +239,9 @@ def add_usage_constraint(ifc, resource_tool, resource=None, reference_path=None)
ifc.run("constraint.assign_constraint", products=[resource], constraint=objective)
def remove_usage_constraint(ifc, resource_tool, resource, reference_path):
def remove_usage_constraint(
ifc: tool.Ifc, resource_tool: tool.Resource, resource: ifcopenshell.entity_instance, reference_path: str
) -> None:
constraints = resource_tool.get_constraints(resource)
for constraint in constraints:
metrics = resource_tool.get_metrics(constraint)
@@ -210,10 +253,12 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path):
ifc.run("constraint.remove_constraint", constraint=constraint)
def go_to_resource(resource_tool, resource):
def go_to_resource(resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None:
resource_tool.go_to_resource(resource)
def calculate_resource_usage(ifc, resource_tool, resource):
def calculate_resource_usage(
ifc: tool.Resource, resource_tool: tool.Resource, resource: ifcopenshell.entity_instance
) -> None:
ifc.run("resource.calculate_resource_usage", resource=resource)
resource_tool.load_resources()
+21 -11
View File
@@ -16,8 +16,18 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
def copy_class(ifc, collector, geometry, root, obj=None):
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def copy_class(
ifc: tool.Ifc, collector: tool.Collector, geometry: tool.Geometry, root: tool.Root, obj: bpy.types.Object
) -> ifcopenshell.entity_instance:
element = ifc.get_entity(obj)
if not element:
return
@@ -48,16 +58,16 @@ def copy_class(ifc, collector, geometry, root, obj=None):
def assign_class(
ifc,
collector,
root,
obj=None,
ifc_class=None,
predefined_type=None,
should_add_representation=True,
context=None,
ifc_representation_class=None,
):
ifc: tool.Ifc,
collector: tool.Collector,
root: tool.Root,
obj: bpy.types.Object,
ifc_class: str,
context: ifcopenshell.entity_instance,
predefined_type: Optional[str] = None,
should_add_representation: bool = True,
ifc_representation_class: Optional[str] = None,
) -> ifcopenshell.entity_instance:
if ifc.get_entity(obj):
return
+1 -1
View File
@@ -18,11 +18,11 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union
import blenderbim.core.tool as tool
if TYPE_CHECKING:
import bpy
import ifcopenshell
import blenderbim.tool as tool
def reference_structure(
+15 -3
View File
@@ -487,12 +487,12 @@ class Material:
def disable_editing_materials(cls): pass
def enable_editing_material(cls, material): pass
def enable_editing_materials(cls): pass
def get_active_material_type(cls): pass
def get_active_material(cls): pass
def get_active_material_type(cls): pass
def get_active_object_material(cls, obj): pass
def get_elements_by_material(cls, material): pass
def get_material_attributes(cls): pass
def get_material(cls, element, should_inherit): pass
def get_material_attributes(cls): pass
def get_name(cls, obj): pass
def get_type(cls, element): pass
def has_material_profile(cls, element): pass
@@ -503,6 +503,7 @@ class Material:
def is_material_used_in_sets(cls, material): pass
def load_material_attributes(cls, material): pass
def replace_material_with_material_profile(cls, element): pass
def sync_blender_material_name(cls, material): pass
@interface
@@ -617,9 +618,20 @@ class Profile:
@interface
class Pset:
def add_proposed_property(cls, name, value, props): pass
def cast_string_to_primitive(cls, value: str): pass
def clear_blender_pset_properties(cls, props): pass
def enable_proposed_pset(cls, props, pset_name, pset_type, has_template): pass
def get_element_pset(cls, element, pset_name): pass
def get_pset_name(cls, obj, obj_type): pass
def get_prop_template_primitive_type(cls, prop_template): pass
def get_pset_name(cls, obj, obj_type, pset_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, has_template): pass
@interface
+32 -6
View File
@@ -468,15 +468,41 @@ 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 get_enum_safe(cls, props: bpy.types.PropertyGroup, prop_name: str) -> Union[str, None]:
"""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'`
"""
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)
# Yes, accessing items through annotations is a bit hacky
# but it's the only way to get the dynamic enum items
# besides providing them to get_enum_safe explicitly.
prop_keywords = props.__annotations__[prop_name].keywords
items = prop_keywords.get("items")
if items is None:
return None
if not isinstance(items, (list, tuple)):
# items are retrieved through a callback, not a static list / tuple :
items = items(props, bpy.context)
items_amount = len(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 None
index = props.get(prop_name)
# If value was never changed (still default), we can just retrieve it from the enum.
if index is None:
default_value = prop_keywords.get("default", 0)
if isinstance(default_value, int):
index = default_value
else:
# If default value is a string then it's a static enum
# and we can just return it.
return default_value
# Ensure index is valid.
if items_amount > index >= 0:
return items[index][0]
return None
@classmethod
def append_data_block(cls, filepath: str, data_block_type: str, name: str, link=False, relative=False) -> dict:
+44 -23
View File
@@ -1,11 +1,13 @@
import os
import bpy
import blenderbim.core.tool
import blenderbim.tool as tool
import ifcopenshell.util.date
import ifcopenshell.util.cost
import ifcopenshell.util.unit
import blenderbim.bim.helper
import json
from typing import Optional, Any, Generator, Union
class Cost(blenderbim.core.tool.Cost):
@@ -36,7 +38,7 @@ class Cost(blenderbim.core.tool.Cost):
blenderbim.bim.helper.import_attributes2(cost_schedule, props.cost_schedule_attributes, callback=special_import)
@classmethod
def enable_editing_cost_items(cls, cost_schedule):
def enable_editing_cost_items(cls, cost_schedule: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_schedule_id = cost_schedule.id()
props.is_editing = "COST_ITEMS"
@@ -147,7 +149,7 @@ class Cost(blenderbim.core.tool.Cost):
return blenderbim.bim.helper.export_attributes(props.cost_item_attributes)
@classmethod
def get_active_cost_item(cls):
def get_active_cost_item(cls) -> Union[ifcopenshell.entity_instance, None]:
props = bpy.context.scene.BIMCostProperties
if not props.active_cost_item_id:
return None
@@ -163,7 +165,7 @@ class Cost(blenderbim.core.tool.Cost):
return
@classmethod
def load_cost_item_types(cls, cost_item=None):
def load_cost_item_types(cls, cost_item: Optional[ifcopenshell.entity_instance] = None) -> None:
if not cost_item:
cost_item = cls.get_highlighted_cost_item()
if not cost_item:
@@ -259,7 +261,7 @@ class Cost(blenderbim.core.tool.Cost):
blenderbim.bim.helper.import_attributes2(physical_quantity, props.quantity_attributes)
@classmethod
def enable_editing_cost_item_values(cls, cost_item=None):
def enable_editing_cost_item_values(cls, cost_item: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_item_id = cost_item.id()
props.cost_item_editing_type = "VALUES"
@@ -287,7 +289,7 @@ class Cost(blenderbim.core.tool.Cost):
return attributes
@classmethod
def load_cost_item_value_attributes(cls, cost_value=None):
def load_cost_item_value_attributes(cls, cost_value: ifcopenshell.entity_instance) -> None:
def import_attributes(name, prop, data, cost_value, is_rates, props_collection):
if name == "AppliedValue":
# TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure)
@@ -334,36 +336,38 @@ class Cost(blenderbim.core.tool.Cost):
blenderbim.bim.helper.import_attributes2(cost_value, props.cost_value_attributes, callback=callback)
@classmethod
def calculate_applied_value(cls, cost_item, cost_value):
def calculate_applied_value(
cls, cost_item: ifcopenshell.entity_instance, cost_value: ifcopenshell.entity_instance
) -> float:
return ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value)
@classmethod
def is_active_schedule_of_rates(cls):
def is_active_schedule_of_rates(cls) -> bool:
return (
tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id).PredefinedType
== "SCHEDULEOFRATES"
)
@classmethod
def enable_editing_cost_item_value(cls, cost_value=None):
def enable_editing_cost_item_value(cls, cost_value: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_value_id = cost_value.id()
props.cost_value_editing_type = "ATTRIBUTES"
@classmethod
def disable_editing_cost_item_value(cls):
def disable_editing_cost_item_value(cls) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_value_id = 0
props.cost_value_editing_type = ""
@classmethod
def load_cost_item_value_formula_attributes(cls, cost_value=None):
def load_cost_item_value_formula_attributes(cls, cost_value: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.cost_value_attributes.clear()
bpy.context.scene.BIMCostProperties.cost_value_formula = ifcopenshell.util.cost.serialise_cost_value(cost_value)
@classmethod
def enable_editing_cost_item_value_formula(cls, cost_value=None):
def enable_editing_cost_item_value_formula(cls, cost_value: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.active_cost_value_id = cost_value.id()
props.cost_value_editing_type = "FORMULA"
@@ -373,7 +377,7 @@ class Cost(blenderbim.core.tool.Cost):
return bpy.context.scene.BIMCostProperties.cost_value_formula
@classmethod
def get_cost_value_attributes(cls):
def get_cost_value_attributes(cls) -> dict[str, Any]:
def export_attributes(attributes, prop):
if prop.name == "UnitBasisValue":
if prop.is_null:
@@ -392,13 +396,18 @@ class Cost(blenderbim.core.tool.Cost):
return blenderbim.bim.helper.export_attributes(props.cost_value_attributes, callback)
@classmethod
def get_cost_value_unit_component(cls):
def get_cost_value_unit_component(cls) -> ifcopenshell.entity_instance:
return tool.Ifc.get().by_id(
int(bpy.context.scene.BIMCostProperties.cost_value_attributes.get("UnitBasisUnit").enum_value)
)
@classmethod
def get_cost_item_assignments(cls, cost_item, filter_by_type=None, is_deep=False):
def get_cost_item_assignments(
cls,
cost_item: ifcopenshell.entity_instance,
filter_by_type: Optional[ifcopenshell.util.cost.FILTER_BY_TYPE] = None,
is_deep: bool = False,
) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.cost.get_cost_item_assignments(
cost_item, filter_by_type=filter_by_type, is_deep=is_deep
)
@@ -408,30 +417,40 @@ class Cost(blenderbim.core.tool.Cost):
return bpy.context.scene.BIMCostProperties.show_nested_elements
@classmethod
def get_cost_item_products(cls, cost_item, is_deep=False):
def get_cost_item_products(
cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False
) -> list[ifcopenshell.entity_instance]:
return cls.get_cost_item_assignments(cost_item, filter_by_type="PRODUCT", is_deep=is_deep)
@classmethod
def get_cost_item_resources(cls, cost_item, is_deep=False):
def get_cost_item_resources(
cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False
) -> list[ifcopenshell.entity_instance]:
return cls.get_cost_item_assignments(cost_item, filter_by_type="RESOURCE", is_deep=is_deep)
@classmethod
def get_cost_item_processes(cls, cost_item, is_deep=False):
def get_cost_item_processes(
cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False
) -> list[ifcopenshell.entity_instance]:
return cls.get_cost_item_assignments(cost_item, filter_by_type="PROCESS", is_deep=is_deep)
@classmethod
def get_schedule_cost_items(cls, cost_schedule):
def get_schedule_cost_items(
cls, cost_schedule: ifcopenshell.entity_instance
) -> Generator[ifcopenshell.entity_instance, None, None]:
return ifcopenshell.util.cost.get_schedule_cost_items(cost_schedule)
@classmethod
def get_cost_schedule_products(cls, cost_schedule):
def get_cost_schedule_products(
cls, cost_schedule: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
products = []
for cost_item in ifcopenshell.util.cost.get_schedule_cost_items(cost_schedule):
products.extend(cls.get_cost_item_products(cost_item))
return products
@classmethod
def import_cost_schedule_csv(cls, file_path=None, is_schedule_of_rates=False):
def import_cost_schedule_csv(cls, file_path: Optional[str] = None, is_schedule_of_rates: bool = False) -> None:
if not file_path:
return
from ifc5d.csv2ifc import Csv2Ifc
@@ -473,7 +492,9 @@ class Cost(blenderbim.core.tool.Cost):
cls.load_schedule_of_rates_tree(schedule_of_rates=tool.Ifc.get().by_id(int(props.schedule_of_rates)))
@classmethod
def create_new_cost_item_li(cls, props_collection, cost_item, level_index, type="cost_rate"):
def create_new_cost_item_li(
cls, props_collection, cost_item: ifcopenshell.entity_instance, level_index: int, type: str = "cost_rate"
) -> None:
new = props_collection.add()
new.ifc_definition_id = cost_item.id()
new.name = cost_item.Name or "Unnamed"
@@ -508,7 +529,7 @@ class Cost(blenderbim.core.tool.Cost):
props.is_cost_update_enabled = True
@classmethod
def export_cost_schedules(cls, filepath, format=None, cost_schedule=None):
def export_cost_schedules(cls, filepath: str, format: str, cost_schedule=None):
import subprocess
import os
import sys
@@ -621,7 +642,7 @@ class Cost(blenderbim.core.tool.Cost):
return bool(cost_items)
@classmethod
def load_product_cost_items(cls, product):
def load_product_cost_items(cls, product: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMCostProperties
props.is_cost_update_enabled = False
props.product_cost_items.clear()
+37 -55
View File
@@ -51,11 +51,14 @@ from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT
from lxml import etree
from mathutils import Vector, Matrix
from fractions import Fraction
from typing import Optional, Union, Iterable, Any
from typing import Optional, Union, Iterable, Any, Literal
from pathlib import Path
class Drawing(blenderbim.core.tool.Drawing):
ANNOTATION_DATA_TYPE = Literal["empty", "curve", "mesh"]
DOCUMENT_TYPE = Literal["SCHEDULE", "REFERENCE"]
@classmethod
def canonicalise_class_name(cls, name):
return re.sub("[^0-9a-zA-Z]+", "", name)
@@ -68,11 +71,11 @@ class Drawing(blenderbim.core.tool.Drawing):
)
@classmethod
def get_annotation_data_type(cls, object_type):
def get_annotation_data_type(cls, object_type: str) -> ANNOTATION_DATA_TYPE:
return ANNOTATION_TYPES_DATA[object_type][3]
@classmethod
def create_annotation_object(cls, drawing, object_type):
def create_annotation_object(cls, drawing: ifcopenshell.entity_instance, object_type: str) -> bpy.types.Object:
data_type = cls.get_annotation_data_type(object_type)
obj = annotation.Annotator.get_annotation_obj(drawing, object_type, data_type)
if object_type == "FILL_AREA":
@@ -298,15 +301,15 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.context.scene.DocProperties.is_editing_sheets = False
@classmethod
def disable_editing_text(cls, obj):
def disable_editing_text(cls, obj: bpy.types.Object) -> None:
obj.BIMTextProperties.is_editing = False
@classmethod
def disable_editing_assigned_product(cls, obj):
def disable_editing_assigned_product(cls, obj: bpy.types.Object) -> None:
obj.BIMAssignedProductProperties.is_editing_product = False
@classmethod
def enable_editing(cls, obj):
def enable_editing(cls, obj: bpy.types.Object) -> None:
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
@@ -330,11 +333,11 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.context.scene.DocProperties.is_editing_sheets = True
@classmethod
def enable_editing_text(cls, obj):
def enable_editing_text(cls, obj: bpy.types.Object) -> None:
obj.BIMTextProperties.is_editing = True
@classmethod
def enable_editing_assigned_product(cls, obj):
def enable_editing_assigned_product(cls, obj: bpy.types.Object) -> None:
obj.BIMAssignedProductProperties.is_editing_product = True
@classmethod
@@ -428,7 +431,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return location
@classmethod
def get_path_filename(cls, path):
def get_path_filename(cls, path: str) -> str:
return os.path.splitext(os.path.basename(path))[0]
@classmethod
@@ -483,7 +486,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return ""
@classmethod
def get_name(cls, element):
def get_name(cls, element: ifcopenshell.entity_instance) -> Union[str, None]:
return element.Name
@classmethod
@@ -563,7 +566,7 @@ class Drawing(blenderbim.core.tool.Drawing):
ifcopenshell.util.element.remove_deep2(ifc_file, literal)
@classmethod
def synchronise_ifc_and_text_attributes(cls, obj):
def synchronise_ifc_and_text_attributes(cls, obj: bpy.types.Object) -> None:
literals = cls.get_text_literal(obj, return_list=True)
literals_attributes = cls.export_text_literal_attributes(obj)
defined_ifc_ids = [l.ifc_definition_id for l in obj.BIMTextProperties.literals]
@@ -784,7 +787,7 @@ class Drawing(blenderbim.core.tool.Drawing):
new.ifc_definition_id = drawing.id() # Last, to prevent unnecessary prop callbacks
@classmethod
def import_documents(cls, document_type):
def import_documents(cls, document_type: DOCUMENT_TYPE) -> None:
dprops = bpy.context.scene.DocProperties
if document_type == "SCHEDULE":
documents_collection = dprops.schedules
@@ -845,7 +848,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return next(s for s in props.sheets[: props.active_sheet_index + 1][::-1] if s.is_sheet)
@classmethod
def import_text_attributes(cls, obj):
def import_text_attributes(cls, obj: bpy.types.Object) -> None:
props = obj.BIMTextProperties
props.literals.clear()
@@ -863,7 +866,7 @@ class Drawing(blenderbim.core.tool.Drawing):
props.font_size = str(text_data["FontSize"])
@classmethod
def import_assigned_product(cls, obj):
def import_assigned_product(cls, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
product = cls.get_assigned_product(element)
if product:
@@ -934,38 +937,16 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.context.scene.DocProperties.should_draw_decorations = True
@classmethod
def update_text_value(cls, obj):
element = tool.Ifc.get_entity(obj)
if element.is_a("IfcTypeProduct"):
objs = [obj]
for occurrence in ifcopenshell.util.element.get_types(element):
obj = tool.Ifc.get_object(occurrence)
if obj:
objs.append(obj)
else:
objs = []
element_type = ifcopenshell.util.element.get_type(element)
if element_type and element_type.RepresentationMaps:
obj = tool.Ifc.get_object(element_type)
if obj:
objs.append(obj)
for occurrence in ifcopenshell.util.element.get_types(element_type):
obj = tool.Ifc.get_object(occurrence)
if obj:
objs.append(obj)
else:
objs = [obj]
for obj in objs:
props = obj.BIMTextProperties
literals = cls.get_text_literal(obj, return_list=True)
cls.import_text_attributes(obj)
for i, literal in enumerate(literals):
product = cls.get_assigned_product(tool.Ifc.get_entity(obj)) or tool.Ifc.get_entity(obj)
props.literals[i].value = cls.replace_text_literal_variables(literal.Literal, product)
def update_text_value(cls, obj: bpy.types.Object) -> None:
props = obj.BIMTextProperties
literals = cls.get_text_literal(obj, return_list=True)
cls.import_text_attributes(obj)
for i, literal in enumerate(literals):
product = cls.get_assigned_product(tool.Ifc.get_entity(obj)) or tool.Ifc.get_entity(obj)
props.literals[i].value = cls.replace_text_literal_variables(literal.Literal, product)
@classmethod
def update_text_size_pset(cls, obj):
def update_text_size_pset(cls, obj: bpy.types.Object) -> None:
"""updates pset `EPset_Annotation.Classes` value
based on current font size from `obj.BIMTextProperties.font_size`
"""
@@ -1544,7 +1525,7 @@ class Drawing(blenderbim.core.tool.Drawing):
tool.Geometry.record_object_position(obj)
@classmethod
def get_document_references(cls, document):
def get_document_references(cls, document: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
if tool.Ifc.get_schema() == "IFC2X3":
return document.DocumentReferences or []
return document.HasDocumentReferences or []
@@ -1575,7 +1556,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return reference.Description
@classmethod
def generate_reference_attributes(cls, reference: ifcopenshell.entity_instance, **attributes: Any) -> dict[str, Any]:
def generate_reference_attributes(
cls, reference: ifcopenshell.entity_instance, **attributes: Any
) -> dict[str, Any]:
"""will automatically convert attributes below for IFC2X3 compatibility:
- Identification -> ItemReference
@@ -1657,23 +1640,22 @@ class Drawing(blenderbim.core.tool.Drawing):
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
updated_set = set()
for i in elements:
# exclude annotations to avoid including annotations from other drawings
if not i.is_a("IfcAnnotation"):
if not i.is_a("IfcAnnotation"):
updated_set.add(i)
#add aggregate too, if element is host by one
# add aggregate too, if element is host by one
if i.Decomposes:
aggregate = i.Decomposes[0].RelatingObject
#remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
if not aggregate.is_a("IfcProject"):
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
if not aggregate.is_a("IfcProject"):
updated_set.add(aggregate)
# After the iteration is complete, update elements with updated set
# After the iteration is complete, update elements with updated set
elements.update(updated_set)
# add annotations from the current drawing
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
elements.update(annotations)
@@ -1716,7 +1698,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return reference.ReferencedDocument
@classmethod
def select_assigned_product(cls, context):
def select_assigned_product(cls, context: bpy.types.Context) -> None:
obj = context.active_object
element = tool.Ifc.get_entity(obj)
product = cls.get_assigned_product(element)
@@ -1735,7 +1717,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return True if (camera and camera.data.type == "ORTHO") else False
@classmethod
def is_active_drawing(cls, drawing):
def is_active_drawing(cls, drawing: ifcopenshell.entity_instance) -> bool:
return drawing.id() == bpy.context.scene.DocProperties.active_drawing_id
@classmethod
+21 -8
View File
@@ -24,8 +24,10 @@ import logging
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.geom
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.system
import blenderbim.core.tool
import blenderbim.core.drawing
@@ -88,7 +90,7 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.data.meshes.remove(data)
@classmethod
def delete_ifc_object(cls, obj):
def delete_ifc_object(cls, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
if not element:
return
@@ -319,7 +321,7 @@ class Geometry(blenderbim.core.tool.Geometry):
return new_mesh
@classmethod
def get_active_representation(cls, obj):
def get_active_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
"""< IfcShapeRepresentation or None"""
if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
@@ -459,7 +461,9 @@ class Geometry(blenderbim.core.tool.Geometry):
return f"{representation.ContextOfItems.id()}/{representation.id()}"
@classmethod
def get_styles(cls, obj, only_assigned_to_faces=False):
def get_styles(
cls, obj: bpy.types.Object, only_assigned_to_faces: bool = False
) -> list[Union[ifcopenshell.entity_instance, None]]:
styles = [tool.Style.get_style(s.material) for s in obj.material_slots if s.material]
if not only_assigned_to_faces:
return styles
@@ -467,8 +471,15 @@ class Geometry(blenderbim.core.tool.Geometry):
usage_count = [0] * len(obj.material_slots)
if not usage_count: # if there are no materials, polygons will still use index 0
return []
for poly in obj.data.polygons:
usage_count[poly.material_index] += 1
# remove usages for empty material slots
for i, slot in reversed(list(enumerate(obj.material_slots))):
if not slot.material:
del usage_count[i]
styles = [style for style, usage in zip(styles, usage_count, strict=True) if usage > 0]
return styles
@@ -560,11 +571,11 @@ class Geometry(blenderbim.core.tool.Geometry):
new.value = element[i]
@classmethod
def is_body_representation(cls, representation):
def is_body_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
return representation.ContextOfItems.ContextIdentifier == "Body"
@classmethod
def is_box_representation(cls, representation):
def is_box_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
return representation.ContextOfItems.ContextIdentifier == "Box"
@classmethod
@@ -572,11 +583,11 @@ class Geometry(blenderbim.core.tool.Geometry):
return not all([tool.Cad.is_x(o, 1.0) for o in obj.scale]) or obj in IfcStore.edited_objs
@classmethod
def is_mapped_representation(cls, representation):
def is_mapped_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
return representation.RepresentationType == "MappedRepresentation"
@classmethod
def is_meshlike(cls, representation):
def is_meshlike(cls, representation: ifcopenshell.entity_instance) -> bool:
if ifcopenshell.util.representation.resolve_representation(representation).RepresentationType in (
"AdvancedBrep",
"Annotation2D",
@@ -656,7 +667,9 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.data.objects.remove(obj)
@classmethod
def resolve_mapped_representation(cls, representation):
def resolve_mapped_representation(
cls, representation: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
if representation.RepresentationType == "MappedRepresentation":
return cls.resolve_mapped_representation(representation.Items[0].MappingSource.MappedRepresentation)
return representation
@@ -242,3 +242,16 @@ class Material(blenderbim.core.tool.Material):
meshes_to_objects[mesh] = obj
for obj in meshes_to_objects.values():
tool.Geometry.reload_representation(obj)
@classmethod
def sync_blender_material_name(cls, material):
name = material.Name or "Unnamed"
obj = tool.Ifc.get_object(material)
if obj:
obj.name = name
style = tool.Style.get_style(obj)
if style:
style.Name = name
obj = tool.Ifc.get_object(style)
if obj:
obj.name = name
+3 -1
View File
@@ -97,7 +97,9 @@ class Misc(blenderbim.core.tool.Misc):
IfcStore.edited_objs.add(obj)
@classmethod
def split_objects_with_cutter(cls, objs, cutter):
def split_objects_with_cutter(
cls, objs: list[bpy.types.Object], cutter: bpy.types.Object
) -> list[bpy.types.Object]:
cutter_mesh = cutter.data
bm = bmesh.new()
+3 -2
View File
@@ -23,6 +23,7 @@ import collections
import collections.abc
import numpy as np
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
import ifcopenshell.util.placement
import ifcopenshell.util.representation
@@ -468,7 +469,7 @@ class Model(blenderbim.core.tool.Model):
has_deleted_opening = True
@classmethod
def get_material_layer_parameters(cls, element):
def get_material_layer_parameters(cls, element: ifcopenshell.entity_instance) -> dict[str, Any]:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layer_set_direction = "AXIS2"
offset = 0.0
@@ -551,7 +552,7 @@ class Model(blenderbim.core.tool.Model):
data = tool.Ifc.get().createIfcText(json.dumps(data))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
else:
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
@classmethod
def get_flow_segment_axis(cls, obj):
@@ -21,6 +21,7 @@ import bpy
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.core.tool
import blenderbim.core.root
import blenderbim.bim.schema
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
+205 -4
View File
@@ -23,6 +23,7 @@ import blenderbim.core.tool
import blenderbim.tool as tool
import blenderbim.bim.schema
from typing import Union
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
class Pset(blenderbim.core.tool.Pset):
@@ -56,9 +57,12 @@ class Pset(blenderbim.core.tool.Pset):
return bpy.context.scene.GroupPsetProperties
@classmethod
def get_pset_name(cls, obj, obj_type):
pset = cls.get_pset_props(obj, obj_type)
return pset.pset_name
def get_pset_name(cls, obj, obj_type, pset_type="PSET"):
props = cls.get_pset_props(obj, obj_type)
name = props.pset_name if pset_type == "PSET" else props.qto_name
if name == "BBIM_CUSTOM":
return ""
return name
@classmethod
def is_pset_applicable(cls, element: ifcopenshell.entity_instance, pset_name: str) -> bool:
@@ -82,5 +86,202 @@ class Pset(blenderbim.core.tool.Pset):
def enable_pset_editing(cls, pset_id=None, pset_name=None, pset_type=None, obj=None, obj_type=None):
# TODO REFACTOR ONCE toll/CORE functions are available
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=tool.Pset.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
pset_id=0, pset_name=cls.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
)
@classmethod
def import_pset_from_existing(cls, pset, props):
pset_props = []
if pset.is_a("IfcElementQuantity"):
pset_props = pset.Quantities
elif pset.is_a("IfcPropertySet"):
pset_props = pset.HasProperties
elif pset.is_a("IfcMaterialProperties") or pset.is_a("IfcProfileProperties"):
pset_props = pset.Properties
for prop in pset_props:
if props.properties.get(prop.Name):
continue # This property has already been added from a template
if prop.is_a("IfcPropertyEnumeratedValue"):
simple_prop = 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 = 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)
@classmethod
def get_prop_template_primitive_type(cls, 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(
tool.Ifc.schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
)
@classmethod
def import_enumerated_value_from_template(cls, prop_template, data, props):
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
selected_enum_items = data.get(prop_template.Name, []) or []
prop = 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
@classmethod
def import_single_value_from_template(cls, pset_template, prop_template, data, props):
prop = 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 = cls.get_prop_template_primitive_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)
@classmethod
def import_pset_from_template(cls, pset_template, pset, props):
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":
cls.import_single_value_from_template(pset_template, prop_template, data, props)
elif prop_template.TemplateType.startswith("Q_"):
cls.import_single_value_from_template(pset_template, prop_template, data, props)
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
cls.import_enumerated_value_from_template(prop_template, data, props)
else:
# NOTE: currently unsupported types:
# - P_BOUNDEDVALUE
# - P_LISTVALUE
# - P_REFERENCEVALUE
# - P_TABLEVALUE
pass
@classmethod
def clear_blender_pset_properties(cls, props):
props.properties.clear()
@classmethod
def set_active_pset(cls, props, pset, has_template):
props.active_pset_id = pset.id()
props.active_pset_name = pset.Name
props.active_pset_has_template = has_template
@classmethod
def enable_proposed_pset(cls, props, pset_name, pset_type, has_template):
props.active_pset_id = 0
props.active_pset_name = pset_name or "My_Data"
props.active_pset_type = pset_type
props.active_pset_has_template = has_template
@classmethod
def get_pset_template(cls, name):
return blenderbim.bim.schema.ifc.psetqto.get_by_name(name)
@classmethod
def add_proposed_property(cls, name, value, props):
if props.properties.get(name):
return
prop = props.properties.add()
prop.name = name
metadata = prop.metadata
metadata.set_value(value)
metadata.name = name
metadata.is_null = value is None
metadata.is_optional = True
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
@classmethod
def cast_string_to_primitive(cls, value: str):
value = value.strip()
if value.lower() == "true":
return True
elif value.lower() == "false":
return False
elif value.lower() == "null" or value == "":
return None
try:
value = int(value)
return value
except:
try:
value = float(value)
return value
except:
return value
return value
+49 -46
View File
@@ -33,10 +33,12 @@ import ifcopenshell.util.cost
import ifcopenshell.util.resource
import blenderbim.bim.schema
import ifcopenshell.util.constraint
from typing import Any, Union
class Resource(blenderbim.core.tool.Resource):
@classmethod
def load_resources(cls):
def load_resources(cls) -> None:
def create_new_resource_li(resource, level_index):
new = bpy.context.scene.BIMResourceTreeProperties.resources.add()
new.ifc_definition_id = resource.id()
@@ -66,42 +68,44 @@ class Resource(blenderbim.core.tool.Resource):
props.is_editing = True
@classmethod
def load_resource_properties(cls):
def load_resource_properties(cls) -> None:
props = bpy.context.scene.BIMResourceProperties
tprops = bpy.context.scene.BIMResourceTreeProperties
props.is_resource_update_enabled = False
for item in tprops.resources:
resource = tool.Ifc.get().by_id(item.ifc_definition_id)
item.name = resource.Name if resource.Name else "Unnamed"
item.schedule_usage = resource.Usage.ScheduleUsage if (resource.Usage and resource.Usage.ScheduleUsage) else 0
item.schedule_usage = (
resource.Usage.ScheduleUsage if (resource.Usage and resource.Usage.ScheduleUsage) else 0
)
props.is_resource_update_enabled = True
@classmethod
def disable_editing_resource(cls):
def disable_editing_resource(cls) -> None:
bpy.context.scene.BIMResourceProperties.active_resource_id = 0
bpy.context.scene.BIMResourceProperties.active_resource_time_id = 0
@classmethod
def disable_resource_editing_ui(cls):
def disable_resource_editing_ui(cls) -> None:
bpy.context.scene.BIMResourceProperties.is_editing = False
@classmethod
def load_resource_attributes(cls, resource):
def load_resource_attributes(cls, resource: ifcopenshell.entity_instance) -> None:
blenderbim.bim.helper.import_attributes2(resource, bpy.context.scene.BIMResourceProperties.resource_attributes)
@classmethod
def enable_editing_resource(cls, resource):
def enable_editing_resource(cls, resource: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
props.active_resource_id = resource.id()
props.resource_attributes.clear()
props.editing_resource_type = "ATTRIBUTES"
@classmethod
def get_resource_attributes(cls):
def get_resource_attributes(cls) -> dict[str, Any]:
return blenderbim.bim.helper.export_attributes(bpy.context.scene.BIMResourceProperties.resource_attributes)
@classmethod
def enable_editing_resource_time(cls, resource):
def enable_editing_resource_time(cls, resource: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
props.resource_time_attributes.clear()
props.active_resource_time_id = resource.Usage.id()
@@ -109,11 +113,11 @@ class Resource(blenderbim.core.tool.Resource):
props.editing_resource_type = "USAGE"
@classmethod
def get_resource_time(cls, resource):
return resource.Usage if resource.Usage else None
def get_resource_time(cls, resource: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return resource.Usage or None
@classmethod
def load_resource_time_attributes(cls, resource_time):
def load_resource_time_attributes(cls, resource_time: ifcopenshell.entity_instance) -> None:
def callback(name, prop, data):
if prop.data_type == "string":
if isinstance(data[name], datetime):
@@ -128,7 +132,7 @@ class Resource(blenderbim.core.tool.Resource):
)
@classmethod
def get_resource_time_attributes(cls):
def get_resource_time_attributes(cls) -> dict[str, Any]:
def callback(attributes, prop):
if "Start" in prop.name or "Finish" in prop.name or prop.name == "StatusTime":
if prop.is_null:
@@ -147,20 +151,19 @@ class Resource(blenderbim.core.tool.Resource):
return blenderbim.bim.helper.export_attributes(props.resource_time_attributes, callback)
@classmethod
def enable_editing_resource_costs(cls, resource):
def enable_editing_resource_costs(cls, resource: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
props.active_resource_id = resource.id()
props.editing_resource_type = "COSTS"
resource
@classmethod
def disable_editing_resource_cost_value(cls):
def disable_editing_resource_cost_value(cls) -> None:
props = bpy.context.scene.BIMResourceProperties
props.active_cost_value_id = 0
props.cost_value_editing_type = ""
@classmethod
def enable_editing_resource_cost_value_formula(cls, cost_value):
def enable_editing_resource_cost_value_formula(cls, cost_value: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
props.cost_value_attributes.clear()
props.active_cost_value_id = cost_value.id()
@@ -168,7 +171,7 @@ class Resource(blenderbim.core.tool.Resource):
props.cost_value_formula = ifcopenshell.util.cost.serialise_cost_value(cost_value) if cost_value else ""
@classmethod
def load_cost_value_attributes(cls, cost_value):
def load_cost_value_attributes(cls, cost_value: ifcopenshell.entity_instance):
def callback(name, prop, data):
if name == "AppliedValue":
# TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure)
@@ -221,7 +224,7 @@ class Resource(blenderbim.core.tool.Resource):
blenderbim.bim.helper.import_attributes2(cost_value, props.cost_value_attributes, callback)
@classmethod
def enable_editing_cost_value_attributes(cls, cost_value):
def enable_editing_cost_value_attributes(cls, cost_value: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
props.cost_value_attributes.clear()
props.active_cost_value_id = cost_value.id()
@@ -232,7 +235,7 @@ class Resource(blenderbim.core.tool.Resource):
return bpy.context.scene.BIMResourceProperties.cost_value_formula
@classmethod
def get_resource_cost_value_attributes(cls):
def get_resource_cost_value_attributes(cls) -> dict[str, Any]:
def callback(attributes, prop):
if prop.name == "UnitBasisValue":
if prop.is_null:
@@ -257,28 +260,28 @@ class Resource(blenderbim.core.tool.Resource):
)
@classmethod
def enable_editing_resource_base_quantity(cls, resource):
def enable_editing_resource_base_quantity(cls, resource: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
props.active_resource_id = resource.id()
props.editing_resource_type = "QUANTITY"
@classmethod
def enable_editing_resource_quantity(cls, resource_quantity):
def enable_editing_resource_quantity(cls, resource_quantity: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
props.quantity_attributes.clear()
props.is_editing_quantity = True
blenderbim.bim.helper.import_attributes2(resource_quantity, props.quantity_attributes)
@classmethod
def disable_editing_resource_quantity(cls):
def disable_editing_resource_quantity(cls) -> None:
bpy.context.scene.BIMResourceProperties.is_editing_quantity = False
@classmethod
def get_resource_quantity_attributes(cls):
def get_resource_quantity_attributes(cls) -> dict[str, Any]:
return blenderbim.bim.helper.export_attributes(bpy.context.scene.BIMResourceProperties.quantity_attributes)
@classmethod
def expand_resource(cls, resource):
def expand_resource(cls, resource: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
contracted_resources = json.loads(props.contracted_resources)
if not resource.id() in contracted_resources:
@@ -287,14 +290,14 @@ class Resource(blenderbim.core.tool.Resource):
props.contracted_resources = json.dumps(contracted_resources)
@classmethod
def contract_resource(cls, resource):
def contract_resource(cls, resource: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMResourceProperties
contracted_resources = json.loads(props.contracted_resources)
contracted_resources.append(resource.id())
props.contracted_resources = json.dumps(contracted_resources)
@classmethod
def import_resources(cls, file_path):
def import_resources(cls, file_path: str) -> None:
from ifc4d.csv2ifc import Csv2Ifc
start = time.time()
@@ -305,7 +308,7 @@ class Resource(blenderbim.core.tool.Resource):
print("Importing Resources CSV finished in {:.2f} seconds".format(time.time() - start))
@classmethod
def get_highlighted_resource(cls):
def get_highlighted_resource(cls) -> Union[ifcopenshell.entity_instance, None]:
resources = len(bpy.context.scene.BIMResourceTreeProperties.resources)
if resources and resources > bpy.context.scene.BIMResourceProperties.active_resource_index:
return tool.Ifc.get().by_id(
@@ -315,7 +318,7 @@ class Resource(blenderbim.core.tool.Resource):
)
@classmethod
def clear_productivity_data(cls, props):
def clear_productivity_data(cls, props: bpy.types.PropertyGroup) -> None:
for duration_prop in props.quantity_consumed or []:
if duration_prop.name == "BaseQuantityConsumed":
duration_prop.years = 0
@@ -328,7 +331,7 @@ class Resource(blenderbim.core.tool.Resource):
props.quantity_produced_name = ""
@classmethod
def load_productivity_data(cls):
def load_productivity_data(cls) -> None:
duration_props = None
for collection_prop in bpy.context.scene.BIMResourceProductivity.quantity_consumed:
duration_props = collection_prop if collection_prop.name == "BaseQuantityConsumed" else None
@@ -358,7 +361,7 @@ class Resource(blenderbim.core.tool.Resource):
duration_props.seconds = durations_attributes["seconds"]
@classmethod
def get_productivity_attributes(cls):
def get_productivity_attributes(cls) -> dict[str, Any]:
props = bpy.context.scene.BIMResourceProductivity
productivity = {}
if props.quantity_consumed:
@@ -370,7 +373,9 @@ class Resource(blenderbim.core.tool.Resource):
return productivity
@classmethod
def get_productivity(cls, resource, should_inherit=False):
def get_productivity(
cls, resource: ifcopenshell.entity_instance, should_inherit: bool = False
) -> ifcopenshell.util.resource.PRODUCTIVITY_PSET_DATA:
return ifcopenshell.util.resource.get_productivity(resource, should_inherit=should_inherit)
@classmethod
@@ -380,29 +385,29 @@ class Resource(blenderbim.core.tool.Resource):
return
return tool.Ifc.run(
"pset.edit_pset",
pset= tool.Ifc.get().by_id(productivity["id"]),
pset=tool.Ifc.get().by_id(productivity["id"]),
properties=attributes,
)
@classmethod
def get_constraints(cls, resource):
def get_constraints(cls, resource: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.constraint.get_constraints(product=resource)
@classmethod
def get_metrics(cls, constraint):
def get_metrics(cls, constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.constraint.get_metrics(constraint)
@classmethod
def get_metric_reference(cls, metric, is_deep=True):
def get_metric_reference(cls, metric: ifcopenshell.entity_instance, is_deep: bool = True):
return ifcopenshell.util.constraint.get_metric_reference(metric, is_deep=is_deep)
@classmethod
def has_metric_constraint(cls, resource, attribute):
def has_metric_constraint(cls, resource: ifcopenshell.entity_instance, attribute):
metrics = ifcopenshell.util.constraint.get_metric_constraints(resource, attribute)
return True if metrics else False
@classmethod
def run_edit_resource_time(cls, resource, attributes):
def run_edit_resource_time(cls, resource: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
if not resource.Usage:
tool.Ifc.run(
"resource.add_resource_time",
@@ -411,7 +416,7 @@ class Resource(blenderbim.core.tool.Resource):
tool.Ifc.run("resource.edit_resource_time", resource_time=resource.Usage, attributes=attributes)
@classmethod
def go_to_resource(cls, resource):
def go_to_resource(cls, resource: ifcopenshell.entity_instance) -> None:
def get_ancestors_ids(resource):
ids = []
for rel in resource.Nests or []:
@@ -427,24 +432,22 @@ class Resource(blenderbim.core.tool.Resource):
bpy.context.scene.BIMResourceProperties.contracted_resources = json.dumps(contracted_resources)
cls.load_resources()
resource_props = bpy.context.scene.BIMResourceTreeProperties
expanded_resources = [item.ifc_definition_id for item in resource_props.resources]
bpy.context.scene.BIMResourceProperties.active_resource_index = expanded_resources.index(resource.id())
@classmethod
def run_calculate_resource_usage(cls, resource):
def run_calculate_resource_usage(cls, resource: ifcopenshell.entity_instance) -> None:
tool.Ifc.run("resource.calculate_resource_usage", resource=resource)
@classmethod
def get_task_assignments(cls, resource):
def get_task_assignments(cls, resource: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.resource.get_task_assignments(resource)
@classmethod
def get_nested_resources(cls, resource):
def get_nested_resources(cls, resource: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
return ifcopenshell.util.resource.get_nested_resources(resource)
@classmethod
def is_attribute_locked(cls, resource, attribute):
return ifcopenshell.util.constraint.is_attribute_locked(resource, attribute)
def is_attribute_locked(cls, resource: ifcopenshell.entity_instance, attribute) -> bool:
return ifcopenshell.util.constraint.is_attribute_locked(resource, attribute)
+8 -3
View File
@@ -29,6 +29,7 @@ import blenderbim.core.style
import blenderbim.tool as tool
from mathutils import Vector
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
from typing import Union, Optional
class Root(blenderbim.core.tool.Root):
@@ -129,7 +130,7 @@ class Root(blenderbim.core.tool.Root):
return ifcopenshell.util.representation.get_representation(element, context=context.ContextType)
@classmethod
def get_element_type(cls, element):
def get_element_type(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_type(element)
@classmethod
@@ -282,8 +283,12 @@ class Root(blenderbim.core.tool.Root):
@classmethod
def run_geometry_add_representation(
cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None
):
cls,
obj: bpy.types.Object,
context: ifcopenshell.entity_instance,
ifc_representation_class: Optional[str] = None,
profile_set_usage: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
return blenderbim.core.geometry.add_representation(
tool.Ifc,
tool.Geometry,
+1 -1
View File
@@ -1630,7 +1630,7 @@ class Sequence(blenderbim.core.tool.Sequence):
group.Description = json.dumps(description)
else:
description = json.dumps({"type": "BBIM_AnimationColorScheme", "colourscheme": colour_scheme})
group = tool.Ifc.run("group.add_group", Name=name, Description=description)
group = tool.Ifc.run("group.add_group", name=name, description=description)
return group[0]
@classmethod
+4 -3
View File
@@ -31,6 +31,7 @@ import json
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon, MultiPolygon
from typing import Generator
class Spatial(blenderbim.core.tool.Spatial):
@@ -187,15 +188,15 @@ class Spatial(blenderbim.core.tool.Spatial):
]
@classmethod
def get_selected_products(cls):
def get_selected_products(cls) -> Generator[ifcopenshell.entity_instance, None, None]:
for obj in bpy.context.selected_objects:
entity = tool.Ifc.get_entity(obj)
if entity and entity.is_a("IfcProduct"):
yield entity
@classmethod
def get_selected_product_types(cls):
for obj in bpy.context.selected_objects:
def get_selected_product_types(cls) -> Generator[ifcopenshell.entity_instance, None, None]:
for obj in tool.Blender.get_selected_objects():
entity = tool.Ifc.get_entity(obj)
if entity and entity.is_a("IfcTypeProduct"):
yield entity
@@ -105,6 +105,7 @@ For Linux or Mac:
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/bcf
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
$ rm -r $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
@@ -117,6 +118,7 @@ For Linux or Mac:
$ ln -s $PWD/src/ifccsv/ifccsv.py $BLENDER_ADDON_PATH/libs/site/packages/ifccsv.py
$ ln -s $PWD/src/ifcdiff/ifcdiff.py $BLENDER_ADDON_PATH/libs/site/packages/ifcdiff.py
$ ln -s $PWD/src/bsdd/bsdd.py $BLENDER_ADDON_PATH/libs/site/packages/bsdd.py
$ ln -s $PWD/src/bcf/src/bcf $BLENDER_ADDON_PATH/libs/site/packages/bcf
$ ln -s $PWD/src/ifc4d/ifc4d $BLENDER_ADDON_PATH/libs/site/packages/ifc4d
$ ln -s $PWD/src/ifc5d/ifc5d $BLENDER_ADDON_PATH/libs/site/packages/ifc5d
$ ln -s $PWD/src/ifccityjson/ifccityjson $BLENDER_ADDON_PATH/libs/site/packages/ifccityjson
@@ -172,6 +174,7 @@ Before running it follow the instructions descibed after `rem` tags.
del "%blenderbim%\libs\site\packages\ifccsv.py"
del "%blenderbim%\libs\site\packages\ifcdiff.py"
del "%blenderbim%\libs\site\packages\bsdd.py"
rd /S /Q "%blenderbim%\libs\site\packages\bcf"
rd /S /Q "%blenderbim%\libs\site\packages\ifc4d"
rd /S /Q "%blenderbim%\libs\site\packages\ifc5d"
rd /S /Q "%blenderbim%\libs\site\packages\ifccityjson"
@@ -184,6 +187,7 @@ Before running it follow the instructions descibed after `rem` tags.
mklink "%blenderbim%\libs\site\packages\ifccsv.py" "%cd%\src\ifccsv\ifccsv.py"
mklink "%blenderbim%\libs\site\packages\ifcdiff.py" "%cd%\src\ifcdiff\ifcdiff.py"
mklink "%blenderbim%\libs\site\packages\bsdd.py" "%cd%\src\bsdd\bsdd.py"
mklink /D "%blenderbim%\libs\site\packages\bcf" "%cd%\src\bcf\src\bcf"
mklink /D "%blenderbim%\libs\site\packages\ifc4d" "%cd%\src\ifc4d\ifc4d"
mklink /D "%blenderbim%\libs\site\packages\ifc5d" "%cd%\src\ifc5d\ifc5d"
mklink /D "%blenderbim%\libs\site\packages\ifccityjson" "%cd%\src\ifccityjson\ifccityjson"
+1 -1
View File
@@ -42,7 +42,7 @@ Installation
You do not need to unzip the add-on file. You should install it as a zipped file.
You should now see **Import-Export: BlenderBIM** available in your add-ons list. Enable the add-on by pressing the checkbox.
You should now see **System: BlenderBIM** available in your add-ons list. Enable the add-on by pressing the checkbox.
.. image:: images/install-blenderbim-3.png
+1 -1
View File
@@ -960,7 +960,7 @@ def run_test_code():
def saving_sample_test_files(and_open_in_blender=None):
filepath = f"{variables['cwd']}/test/files/temp/sample_test_file"
blend_filepath = f"{filepath}.blend"
bpy.ops.export_ifc.bim(filepath=f"{filepath}.ifc", should_save_as=True)
bpy.ops.bim.save_project(filepath=f"{filepath}.ifc", should_save_as=True)
bpy.ops.wm.save_as_mainfile(filepath=f"{filepath}.blend")
@@ -19,6 +19,7 @@
import bpy
import math
import ifcopenshell
import ifcopenshell.api
import blenderbim.core.tool
import blenderbim.tool as tool
from mathutils import Vector
+52 -24
View File
@@ -23,22 +23,25 @@ import ifcopenshell.util.unit
import ifcopenshell.util.selector
import ifcopenshell.util.element
import locale
from typing import Any, Optional
CostItem = dict[str, Any]
class Csv2Ifc:
def __init__(self):
self.csv = None
self.file = None
self.cost_items = []
self.cost_schedule = None
self.is_schedule_of_rates = False
self.units = {}
self.csv: str = None
self.file: ifcopenshell.file = None
self.cost_items: list[CostItem] = []
self.cost_schedule: ifcopenshell.entity_instance = None
self.is_schedule_of_rates: bool = False
self.units: dict[str, ifcopenshell.entity_instance] = {}
def execute(self):
def execute(self) -> None:
self.parse_csv()
self.create_ifc()
def parse_csv(self):
def parse_csv(self) -> None:
self.parents = {}
self.headers = {}
locale.setlocale(locale.LC_ALL, "") # set the system locale
@@ -47,6 +50,7 @@ class Csv2Ifc:
for row in reader:
if not row[0]:
continue
# parse header
if row[0] == "Hierarchy":
self.has_categories = True
for i, col in enumerate(row):
@@ -55,6 +59,17 @@ class Csv2Ifc:
if col == "Value":
self.has_categories = False
self.headers[col] = i
# validate header
mandatory_fields = {"Name", "Quantity", "Unit"}
if not self.is_schedule_of_rates:
mandatory_fields.update({"Property", "Query"})
available_fields = set(self.headers.keys())
if not mandatory_fields.issubset(available_fields):
raise Exception(
f"Missing mandatory fields in CSV header: {', '.join(mandatory_fields-available_fields)}"
)
continue
cost_data = self.get_row_cost_data(row)
hierarchy_key = int(row[0])
@@ -64,7 +79,7 @@ class Csv2Ifc:
self.parents[hierarchy_key - 1]["children"].append(cost_data)
self.parents[hierarchy_key] = cost_data
def get_row_cost_data(self, row):
def get_row_cost_data(self, row: list[str]) -> CostItem:
name = row[self.headers["Name"]]
identification = row[self.headers["Identification"]] if "Identification" in self.headers else None
quantity = row[self.headers["Quantity"]]
@@ -99,7 +114,7 @@ class Csv2Ifc:
"children": [],
}
def create_ifc(self):
def create_ifc(self) -> None:
if not self.file:
self.create_boilerplate_ifc()
if not self.cost_schedule:
@@ -108,11 +123,13 @@ class Csv2Ifc:
self.cost_schedule.PredefinedType = "SCHEDULEOFRATES"
self.create_cost_items(self.cost_items)
def create_cost_items(self, cost_items, parent=None):
def create_cost_items(
self, cost_items: list[CostItem], parent: Optional[ifcopenshell.entity_instance] = None
) -> None:
for cost_item in cost_items:
self.create_cost_item(cost_item, parent)
def create_cost_item(self, cost_item, parent):
def create_cost_item(self, cost_item: CostItem, parent: Optional[ifcopenshell.entity_instance] = None) -> None:
if parent is None:
cost_item["ifc"] = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=self.cost_schedule)
else:
@@ -156,23 +173,30 @@ class Csv2Ifc:
cost_value.UnitBasis = self.file.createIfcMeasureWithUnit(value_component, unit_component)
if not self.is_schedule_of_rates and cost_item["Quantity"]:
quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"])
quantity = None
quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"])
if not cost_item["assignments"]["PropertyName"] or cost_item["assignments"]["PropertyName"].upper() == "COUNT":
prop_name = ""
else:
prop_name = cost_item["assignments"]["PropertyName"]
if not self.is_schedule_of_rates and cost_item["Quantity"] is not None:
quantity = ifcopenshell.api.run(
"cost.add_cost_item_quantity", self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class
)
# 3 IfcPhysicalSimpleQuantity Value
quantity[3] = cost_item["Quantity"]
if prop_name:
quantity.Name = prop_name
if cost_item["assignments"]["Query"]:
if (
not cost_item["assignments"]["PropertyName"]
or cost_item["assignments"]["PropertyName"].upper() == "COUNT"
):
prop_name = ""
else:
prop_name = cost_item["assignments"]["PropertyName"]
results = ifcopenshell.util.selector.filter_elements(self.file, cost_item["assignments"]["Query"])
results = [r for r in results if has_property(self.file, r, prop_name)]
# NOTE: currently we do not support count quantities that have
# both defined quantity in .csv "Quantity" column
# and some query in "Query" column.
# If query is provided it will override the defined value
# due current behaviour in cost.assign_cost_item_quantity.
if results:
ifcopenshell.api.run(
"cost.assign_cost_item_quantity",
@@ -181,10 +205,14 @@ class Csv2Ifc:
products=results,
prop_name=prop_name,
)
elif not quantity:
quantity = ifcopenshell.api.run(
"cost.add_cost_item_quantity", self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class
)
self.create_cost_items(cost_item["children"], cost_item["ifc"])
def create_unit(self, symbol):
def create_unit(self, symbol: str) -> ifcopenshell.entity_instance:
unit = self.units.get(symbol, None)
if unit:
return unit
@@ -194,12 +222,12 @@ class Csv2Ifc:
self.units[symbol] = unit
return unit
def create_boilerplate_ifc(self):
def create_boilerplate_ifc(self) -> None:
self.file = ifcopenshell.file(schema="IFC4")
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
def has_property(self, product, property_name):
def has_property(self, product: ifcopenshell.entity_instance, property_name: str) -> bool:
if not property_name:
return True
qtos = ifcopenshell.util.element.get_psets(product, qtos_only=True)
+19 -3
View File
@@ -26,13 +26,18 @@ import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.cost
import ifcopenshell.util.date
from typing import Union, Optional
class IfcDataGetter:
@staticmethod
def get_schedules(file, filter_by_schedule=None):
def get_schedules(
file: ifcopenshell.file, filter_by_schedule: Optional[ifcopenshell.entity_instance] = None
) -> list[ifcopenshell.entity_instance]:
return [
schedule for schedule in file.by_type("IfcCostSchedule") if not filter_by_schedule or schedule == filter_by_schedule
schedule
for schedule in file.by_type("IfcCostSchedule")
if not filter_by_schedule or schedule == filter_by_schedule
]
@staticmethod
@@ -195,7 +200,18 @@ class IfcDataGetter:
class Ifc5Dwriter:
def __init__(self, file=None, output=None, cost_schedule=None):
file: ifcopenshell.file
def __init__(
self,
file: Union[str, ifcopenshell.file],
output: str,
cost_schedule: Optional[ifcopenshell.entity_instance] = None,
):
"""
Args:
cost_schedule: exported cost schedule. If not provided, will export all available schedules.
"""
self.output = output
if isinstance(file, str):
self.file = ifcopenshell.open(file)
+15 -3
View File
@@ -29,7 +29,7 @@ import ifcopenshell.util.selector
import ifcopenshell.util.element
import ifcopenshell.util.schema
from statistics import mean
from typing import Optional, Union
from typing import Optional, Union, Literal
try:
from odf.namespaces import OFFICENS
@@ -53,6 +53,14 @@ except:
pass # No Pandas support
FILE_FORMAT = Literal[
"csv",
"ods",
"xlsx",
"pd",
]
class IfcCsv:
def __init__(self):
self.headers = []
@@ -66,7 +74,7 @@ class IfcCsv:
attributes,
headers=None,
output=None,
format=None,
format: FILE_FORMAT = None,
should_preserve_existing: bool = False,
include_global_id: bool = True,
delimiter: str = ",",
@@ -392,7 +400,11 @@ class IfcCsv:
bool_true: str = "YES",
bool_false: str = "NO",
) -> None:
ext = table.split(".")[-1].lower()
"""
Args:
table: filepath to the table.
"""
ext: FILE_FORMAT = table.split(".")[-1].lower()
if ext == "csv":
self.import_csv(ifc_file, table, attributes, delimiter, null, empty, bool_true, bool_false)
@@ -59,7 +59,6 @@
#include <GeomAPI_ProjectPointOnSurf.hxx>
#include <Geom_Plane.hxx>
#include <IntTools_FaceFace.hxx>
#include <STEPConstruct_PointHasher.hxx>
#include "clash_utils.h"
#ifdef WITH_HDF5
@@ -254,7 +254,7 @@ def schema_by_name(
return ifcopenshell_wrapper.schema_by_name(schema)
def guess_format(path: Path) -> Union[str | None]:
def guess_format(path: Path) -> Union[str, None]:
"""Guesses the IFC format using file extension
IFCs may be serialised as different formats. The most common is a ``.ifc``
@@ -67,6 +67,20 @@ def batching_argument_deprecation(
return (replace_usecase or usecase_path, settings)
def renamed_arguments_deprecation(
usecase_path: str, settings: dict, arguments_remapped: dict[str, str]
) -> tuple[str, dict]:
for prev_argument, new_argument in arguments_remapped.items():
if prev_argument in settings:
print(
f"WARNING. `{prev_argument}` argument is deprecated for API method "
f'"{usecase_path}" and should be replaced with `{new_argument}`.'
)
settings = settings | {new_argument: settings[prev_argument]}
settings.pop(prev_argument)
return (usecase_path, settings)
ARGUMENTS_DEPRECATION = {
"spatial.assign_container": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products"
@@ -143,6 +157,10 @@ ARGUMENTS_DEPRECATION = {
"project.unassign_declaration": partial(
batching_argument_deprecation, prev_argument="definition", new_argument="definitions"
),
"group.add_group": partial(
renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
),
"layer.add_layer": partial(renamed_arguments_deprecation, arguments_remapped={"Name": "name"}),
}
@@ -325,7 +343,17 @@ def wrap_usecase(usecase_path, usecase):
try:
result = usecase(*args, **settings)
except TypeError as e:
msg = f"Incorrect function arguments provided for {usecase_path}\n{str(e)}. You specified args {args} and settings {settings}\n\nCorrect signature is {inspect.signature(usecase)}\nSee help(ifcopenshell.api.{usecase_path}) for documentation."
if not e.args[0].startswith(f"{usecase.__name__}()"):
# signature errors typically start with function name
# e.g. "TypeError: edit_library() got an unexpected keyword argument 'test'"
# otherwise it's an error inside api call and we shouldn't get in the way
raise e
msg = (
f"Incorrect function arguments provided for {usecase_path}\n{str(e)}. "
f"You specified args {args} and settings {settings}\n\n"
f"Correct signature is {inspect.signature(usecase)}\n"
f"See help(ifcopenshell.api.{usecase_path}) for documentation."
)
raise TypeError(msg) from e
if should_run_listeners:
@@ -89,7 +89,6 @@ def assign_connection_geometry(
usecase.axis = axis
usecase.ref_direction = ref_direction
usecase.unit_scale = unit_scale
usecase.ifc_vertices = []
return usecase.execute()
@@ -110,7 +110,9 @@ class Usecase:
"IfcCalendarDate", **ifcopenshell.util.date.datetime2ifc(edition_date, "IfcCalendarDate")
)
else:
result.EditionDate = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
if edition_date:
edition_date = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
result.EditionDate = edition_date
self.relate_to_project(result)
@@ -16,8 +16,17 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Optional, Literal
def add_context(file, context_type=None, context_identifier=None, target_view=None, parent=None) -> None:
def add_context(
file: ifcopenshell.file,
context_type: Optional[Literal["Model", "Plan"]] = None,
context_identifier: Optional[str] = None,
target_view: Optional[str] = None,
parent: Optional[ifcopenshell.entity_instance] = None,
) -> ifcopenshell.entity_instance:
"""Adds a new geometric representation context
In IFC, physical objects may have zero, one, or multiple geometric
@@ -87,7 +96,7 @@ def add_context(file, context_type=None, context_identifier=None, target_view=No
:param context_type: The type of the context, must be one of "Model" or
"Plan" only.
:type context_type: str
:type context_type: str, optional
:param context_identifier: The identifier of the context, chosen from
one of the common identifiers above or consult the IFC documentation
(under the IfcShapeRepresentation page) for more details. Optional
@@ -104,7 +113,7 @@ def add_context(file, context_type=None, context_identifier=None, target_view=No
:type parent: ifcopenshell.entity_instance, optional
:return: the newly created IfcGeometricRepresentationContext or
IfcGeometricRepresentationSubContext entity
:rtype: ifcopenshell.entity_instance, optional
:rtype: ifcopenshell.entity_instance
Example:
@@ -16,8 +16,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_context(file, context, attributes) -> None:
def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcGeometricRepresentationContext
For more information about the attributes and data types of an
@@ -26,7 +29,7 @@ def edit_context(file, context, attributes) -> None:
:param context: The IfcGeometricRepresentationContext entity you want to edit
:type context: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:type attributes: dict
:return: None
:rtype: None
@@ -44,7 +47,7 @@ def edit_context(file, context, attributes) -> None:
ifcopenshell.api.run("context.edit_context", model,
context=body, attributes={"ContextIdentifier": "Body"})
"""
settings = {"context": context, "attributes": attributes or {}}
settings = {"context": context, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["context"], name, value)
@@ -17,10 +17,13 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
import ifcopenshell.util.unit
def add_cost_item_quantity(
file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance, ifc_class: str = "IfcQuantityCount"
file: ifcopenshell.file,
cost_item: ifcopenshell.entity_instance,
ifc_class: ifcopenshell.util.unit.QUANTITY_CLASS = "IfcQuantityCount",
) -> ifcopenshell.entity_instance:
"""Adds a new quantity associated with a cost item
@@ -22,7 +22,9 @@ from datetime import datetime
from typing import Optional
def add_cost_schedule(file: ifcopenshell.file, name: Optional[str] = None, predefined_type="NOTDEFINED") -> None:
def add_cost_schedule(
file: ifcopenshell.file, name: Optional[str] = None, predefined_type: str = "NOTDEFINED"
) -> ifcopenshell.entity_instance:
"""Add a new cost schedule
A cost schedule is a group of cost items which typically represent a
@@ -17,14 +17,14 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
from typing import Optional
from typing import Any
def assign_cost_item_quantity(
file: ifcopenshell.file,
cost_item: ifcopenshell.entity_instance,
products: list[ifcopenshell.entity_instance],
prop_name: Optional[str] = "",
prop_name: str = "",
) -> None:
"""Adds a cost item quantity that is parametrically connected to a product
@@ -44,6 +44,10 @@ def assign_cost_item_quantity(
cost item and the product, so it is not necessary to use
ifcopenshell.api.control.assign_control.
If cost item has just 1 quantity and it's IfcQuantityCount, API will
assume that quantity is used for counting controlled objects
and it will recalculate the quantity value at the end of the API call.
:param cost_item: The IfcCostItem to assign parametric quantities to
:type cost_item: ifcopenshell.entity_instance
:param products: The IfcObjects to assign parametric quantities to
@@ -92,6 +96,9 @@ def assign_cost_item_quantity(
class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self):
if self.settings["prop_name"]:
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
@@ -109,7 +116,9 @@ class Usecase:
else:
self.update_cost_item_count()
def assign_cost_control(self, related_object, cost_item):
def assign_cost_control(
self, related_object: ifcopenshell.entity_instance, cost_item: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
return ifcopenshell.api.run(
"control.assign_control",
self.file,
@@ -117,12 +126,12 @@ class Usecase:
relating_control=cost_item,
)
def add_quantity_from_related_object(self, element):
def add_quantity_from_related_object(self, element: ifcopenshell.entity_instance) -> None:
for relationship in element.IsDefinedBy:
if relationship.is_a("IfcRelDefinesByProperties"):
self.add_quantity_from_qto(relationship.RelatingPropertyDefinition)
def add_quantity_from_qto(self, qto):
def add_quantity_from_qto(self, qto: ifcopenshell.entity_instance) -> None:
if not qto.is_a("IfcElementQuantity"):
return
for prop in qto.Quantities:

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