Compare commits

...

483 Commits

Author SHA1 Message Date
Dion Moult d4dc9af1be Setup builds for IfcSverchok 2024-06-11 15:41:18 +10:00
Dion Moult fbe25b314b Whoops 2024-06-11 14:59:29 +10:00
Dion Moult bfe36dddb7 Update deprecated set-output Github command 2024-06-11 14:48:37 +10:00
Dion Moult dea2b6e367 New workflow for stable IfcOpenShell-Python releases
Typically every 2 months
2024-06-11 14:42:22 +10:00
Dion Moult 5e6fe5e3cf Attempt to fix daily BBIM builds 2024-06-11 14:11:40 +10:00
Dion Moult b009913cf4 Fix #4511. IfcOpenShell builds now read from the VERSION file and expose a __version__ var. 2024-06-11 13:11:14 +10:00
Thomas Krijnen 783675f78c Fix IfcGeomServer build after c01263d721 2024-06-10 21:41:52 +02:00
Thomas Krijnen 6fb05fa743 Fix segfault on grid placement. Don't fail on nullptr transformation. #4832 2024-06-10 21:40:39 +02:00
Thomas Krijnen 2ccc598270 Re #4833 2024-06-10 21:30:26 +02:00
Thomas Krijnen d125699ca6 Re #4835 2024-06-10 21:30:01 +02:00
Thomas Krijnen 593fba98c1 Fix USE_PYTHON_OPENCASCADE setting #4832 2024-06-10 21:12:37 +02:00
Thomas Krijnen 74a42ba006 More precisely target cityjson code #4835 2024-06-10 20:56:50 +02:00
Thomas Krijnen 19933e0102 Reintroduce schema def for early bound example part 2024-06-10 20:24:26 +02:00
Thomas Krijnen c01263d721 shared_ptr styles all over #4833 2024-06-10 20:13:12 +02:00
Thomas Krijnen f2074d95fd Adapt USD serializer for v0.8 and define nominmax for windows.h compatibility 2024-06-10 20:13:12 +02:00
Thomas Krijnen 83a929604c Disable building USD-related things for now 2024-06-10 20:12:09 +02:00
D4ve-R d8c5414ed1 build scripts 2024-06-10 20:12:09 +02:00
Andrej730 ab31f9ae85 fix mistake in de80e416c 2024-06-10 19:56:02 +05:00
Dion Moult 293cee414a New VERSION file and make BBBIM and docs based on version string 2024-06-11 00:09:28 +10:00
Andrej730 8067d0c4f6 IfcParseExample to support IFC4 after 2948d08 2024-06-10 17:43:07 +05:00
Andrej730 a70b7e319b cmake - build alignment examples only if 4x3 schema is available
As they doesn't support other schemas.
2024-06-10 17:43:07 +05:00
Andrej730 de80e416c3 use get_shape_matrix util function where it's possible 2024-06-10 17:43:07 +05:00
Andrej730 51f859d8e4 typing 2024-06-10 17:43:06 +05:00
Andrej730 6552463ed9 sequence.remove_task to remove IfcRecurrencePattern 2024-06-10 16:55:21 +05:00
Andrej730 d56da70cae sequence.assign_recurrence_pattern - fix bug assigning pattern to a task time recurring
It wasn't considering that IfcTaskTimeRecurring might be just created and IfcTaskTimeRecurring.Recurrence is None resulting in error below

  File "\api\sequence\assign_recurrence_pattern.py", line 120, in assign_recurrence_pattern
    if len(file.get_inverse(settings["parent"].Recurrence)) == 1:
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\file.py", line 524, in get_inverse
    inverses = [entity_instance(e, self) for e in self.wrapped_data.get_inverse(inst.wrapped_data)]

AttributeError: 'NoneType' object has no attribute 'wrapped_data'
2024-06-10 16:55:21 +05:00
Andrej730 6cab7d964e sequence.remove_work_time to remove IfcRecurrencePattern
Previously it was leaving them orphaned.
2024-06-10 16:55:21 +05:00
Andrej730 1343741cfd sequence.remove_work_calendar to remove related work times
Previously they were leaved orphaned
2024-06-10 16:55:20 +05:00
Andrej730 7c713eee65 nest.remove_task to clean up ownerhistory in more cases
1) It wasn't removing ownerhistory in the case if task was RelatingObject in IfcRelNests
2) Replaced handling of IfcRelNests as an inverse with direct api calls.
3) Added some basic tests.
2024-06-10 15:51:03 +05:00
Dion Moult 277e53d5ac General cleanup of collection code in covering tool and dead arguments.
It seems there is a lot of copy paste going on here. There needs to be a more detailed review once the v0.8.0 blob is more stable because right now I cannot fully test - it's hard to say if create_shape fails due to our bug or due to v0.8.0 not yet being stable. I'll revisit this later.
2024-06-10 18:07:01 +10:00
Dion Moult 1b63d9e88f Railing/roof/stair/window now use default collection. Continue removing old "active collection" code. 2024-06-10 16:13:00 +10:00
Dion Moult e520c2ce8a Model module walls/slabs/products/mep now use the default container too. 2024-06-10 16:06:36 +10:00
Dion Moult 94f54fd4cb Generating a space now is based on the default collection 2024-06-10 15:36:55 +10:00
Dion Moult e6ac012f57 Loading IFCs now also uses tool.Collector.assign instead of implementing the logic twice 2024-06-09 16:55:19 +10:00
Dion Moult 72998b4eac Drawing style queries now use new selector syntax 2024-06-09 16:53:36 +10:00
Dion Moult 177b8218ee Creating grids now uses tool.Collector.assign instead of reimplementing collection management 2024-06-09 16:53:04 +10:00
Dion Moult 6e0665bd31 Fix bug where grid axes didn't accommodate cartesian offsets 2024-06-09 16:52:26 +10:00
Dion Moult cdce612566 Update all geom settings to new v0.8 syntax 2024-06-09 16:15:55 +10:00
Bruno Postle 52f9db4522 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-06-08 22:54:23 +01:00
Dion Moult 1e1066da3b Update and rename cd.yml to ci-ifcopenshell-docker.yml
Now only runs on manual run / tag.
2024-06-08 23:00:44 +10:00
Dion Moult 072b9fd147 Start simplifying collection tree to only place items in their container and only one level deep.
This should heavily simplify the complex logic around blender collections and give freedom to the user to do whatever they want.
2024-06-08 22:26:41 +10:00
Dion Moult 3a1f1af803 Bump docs for v0.8.0 2024-06-08 14:50:18 +10:00
Dion Moult 03b277626f See #4832. Use shape.transformation.matrix in v0.8.0 2024-06-08 14:50:02 +10:00
Andrej730 cd2e33721e Remove deprecated STRICT_TOLERANCE setting #4832
Mentioned in #4565, it's now "set" by default
2024-06-08 00:33:17 +05:00
Andrej730 2d7a7a8bc3 update blenderbim/ifcsverchok branches #4832 2024-06-08 00:23:54 +05:00
Andrej730 b1d9c2df64 bump ifcopenshell build #4832 2024-06-08 00:19:18 +05:00
Dion Moult c4dfbdbe2e Merge branch 'v0.7.0' into v0.8.0 2024-06-07 23:48:39 +10:00
Dion Moult e3cb811225 WARNING: Remove collection syncing. The outliner no longer is used to change spatial hierarchy.
This is a BREAKING change (on purpose). If you want to change containment / aggregation, use the UI panels, not drag and drop on the outliner.
2024-06-07 22:55:18 +10:00
Andrej730 db98f13982 typing 2024-06-07 17:15:52 +05:00
Andrej730 61c2a2c6c6 remove_work_schedule to unassign a work schedule from a work plan #4819 2024-06-07 17:15:51 +05:00
Andrej730 5c41baeaf1 remove_work_plan - unassign work schedules #4819 2024-06-07 17:15:51 +05:00
Andrej730 f8a5ce34a8 fix redeclarations in tests 2024-06-07 17:15:51 +05:00
Andrej730 4078140079 distance_limit, false_origin tooltips 2024-06-07 17:15:51 +05:00
Dion Moult c9d724fe18 You can now set a default spatial container.
This will supersede having an active Blender collection selected.
2024-06-07 21:53:21 +10:00
Dion Moult 03b2afbb31 Fix IfcCSV docs to use new selector syntax. 2024-06-07 20:08:23 +10:00
Dion Moult f0a19cb054 Fix #4815. Purge deprecated old selector syntax. 2024-06-07 20:08:11 +10:00
Dion Moult f565b4e8f0 Minor georeferencing offset fix after recent fixes 2024-06-07 20:06:57 +10:00
Dion Moult af5838cba6 Fix #4821. Bug where empty true norths were still set. 2024-06-07 16:30:19 +10:00
Dion Moult d0058a1bde Fix #4802. Fix ability to edit CARTESIAN_POINT offset meshes in offset models. 2024-06-07 14:32:00 +10:00
Dion Moult 698c648413 Fix #3757. Adding objects in offset models now correctly stores georeferenced coordinates. 2024-06-07 12:47:41 +10:00
Dion Moult 8f64ef4465 Fix bug where incorrect units were calculated in the geometry offset UI 2024-06-07 10:40:23 +10:00
Bernd Hahnebach a16c159b7f BBIM, bcf, ignore problematic topics in case of markup 2024-06-06 22:06:42 +02:00
Bernd Hahnebach 04b641c49c BBIM, bcf, fix colors for hex with alpha channel 2024-06-06 22:06:42 +02:00
Bernd Hahnebach 84d475cfd3 BBIM, bcf, fix setting the viewpoint components 2024-06-06 22:06:42 +02:00
Bernd Hahnebach afe7d526a5 BBIM, bcf, fix if either project or project name is missing in bcf 2024-06-06 22:06:42 +02:00
Dion Moult 98ec3e2301 Fix #4677. Fix #4809. Major improvement to local session offset during project loading (see description)
This improves four things:

 1. Previously, we either used OBJECT_PLACEMENT or CARTESIAN_POINT, but couldn't handle scenarios where simultaneously both the placement and the coords were rubbish for a single object. Now we offset all far cartesian points, so it consistently works and we keep track of a per-object offset.
 2. Previously, we applied the georeferencing conversion on every cartesian point which was very slow. The new method uses a simple XYZ translation which is super fast.
 3. We now use numpy which should be much faster too.
 4. Previously, objects were selectively offset based on whether they fell outside the distance limit. Now, we uniformly treat all non-geometric elements at 0,0,0 as insignificant positionally. This fixes the issue where half the model is offset and the other half isn't, but maintains the fix for situations where the site (typically) is at 0,0,0 and everything else is map coords.
2024-06-06 11:54:07 +10:00
Dion Moult cfd0ed60cf Reduce (arbitrary) element checking threshold down to 3 for georef guessing. 2024-06-06 09:34:32 +10:00
Ryan Schultz 802e1966fd small tweak 2024-06-05 08:21:28 -05:00
Ryan Schultz b57a1e5ea8 Adding a 'magic_font_scale'
https://community.osarch.org/discussion/2202/blenderbim-edit-text-preview-size
2024-06-05 08:12:22 -05:00
Ryan Schultz b92e78b137 added styles search 2024-06-05 07:43:17 -05:00
Gorgious56 8b69ea464f Fix variable name in code example 2024-06-05 08:19:29 +02:00
Dion Moult 1565575e8b Load spatial tree after new / open project 2024-06-05 15:47:55 +10:00
Dion Moult 18b68353b1 Minor UI polish: center tabbar, remove Blender tabs as no longer necessary for native IFC 2024-06-05 15:09:23 +10:00
Dion Moult 9699eb38c5 Only show Blender tab icon in dropdown, separated.
This helps for smaller screens and makes it less overwhelming for users.
2024-06-05 15:00:28 +10:00
Dion Moult d294be3433 Support adding a child space using the rules of spatial decomposition for each schema 2024-06-05 14:53:31 +10:00
Dion Moult 60ae4e80ce New get_contained util.element function 2024-06-05 13:04:50 +10:00
Dion Moult 05ec36a37d Spatial manager now shows aggregated objects inside selected space 2024-06-05 13:04:35 +10:00
Andrej730 dc8ea31138 Bump ladybug tools build in docs 2024-06-04 13:42:33 +05:00
Dion Moult faaea7db49 Spatial manager now allows inline editing of names and elevations for a slicker UI 2024-06-04 15:10:28 +10:00
Dion Moult b14ff94303 Spatial manager renamed to project tree and added icons 2024-06-04 13:57:57 +10:00
Dion Moult b3dcfbf0dc See #4796. Fix issue where orphaned aggregate is never shown when loading IFC. 2024-06-04 12:31:07 +10:00
Ryan Schultz b68109d32b small fix for #4745 2024-06-03 12:31:03 -05:00
Dion Moult 2ea3fe4ab9 Fix qto bug where cubic meter units are incorrectly converted. 2024-06-03 22:49:40 +10:00
Andrej730 94c159ccd0 Consider elements with openings reloading representations
Before - https://imgur.com/a/MMnGMBo .
Occurrences with openings wasn't reloaded if you'd change their representation items (add/remove a style, remove a representation item). Also type/occurrences representations were not always reloading.

After - https://imgur.com/a/vBDejb8

Similar thing will be added to switch representation and to representation update to solve issues like this - https://imgur.com/a/0DFIBj2 (issue is still present now).
2024-06-03 17:47:37 +05:00
Andrej730 e4eead6662 styles ui - bim.assign_style_to_selected
Just a simple operator to explicitly assign selected style to the selected objects from Styles UI

https://imgur.com/a/JOLVPz7
2024-06-03 17:44:06 +05:00
Andrej730 ee4e92dfa3 remove bim.update_current_style poll
as this operator can be executed without any objects selected
2024-06-03 17:44:05 +05:00
Andrej730 f4ce2965f9 fix error saving ifc file / assigning classes 6eb3c4814 2024-06-03 17:44:05 +05:00
Andrej730 5fb7e6e375 resource.remove_resource - fix issue removing resources in ifc2x3 2024-06-03 17:44:05 +05:00
Andrej730 c399eb259a resource.add_resource_quantity to ensure quantity type is supported 2024-06-03 17:44:05 +05:00
Andrej730 5ccdf187d5 change type of exception when setting non-optional property to TypeError
TypeError is more correct since error occurs when user is trying to set some attribute value with None while atttribute doesn't support None type. ValueError is typically raised when provided value has a correct type but unsupported value.
2024-06-03 17:44:05 +05:00
Andrej730 d53351d218 Remove redundant description repeating the label #4771 2024-06-03 17:44:05 +05:00
Andrej730 08a82c7b57 remove dead code after #4745 2024-06-03 17:44:05 +05:00
Andrej730 499cdbb9b8 bump old api argument deprecation date 2024-06-03 17:44:05 +05:00
Andrej730 4a5d1eba0b specify exported api methods in __all__
It's py.types requirement, similar to 6e2edbf

Example issue without __all__:
import ifcopenshell
import ifcopenshell.api.project

# "create_file" is not exported from module "ifcopenshell.api.project"
ifcopenshell.api.project.create_file()
2024-06-03 17:44:05 +05:00
Andrej730 70091aa28b typing 2024-06-03 17:44:04 +05:00
Manu Varkey 3d566cc401 Fix missing code in pull request #4784 (#4794)
One line of code was inadvertently missed out in the pull request https://github.com/IfcOpenShell/IfcOpenShell/pull/4784
2024-06-03 21:30:58 +10:00
Dion Moult 6eb3c48144 Fix #3973. Fix #4787. Clarify that you cannot have aggregated physical elements where the whole has a body representation. 2024-06-03 15:37:24 +10:00
Dion Moult 44d180007e Migrate test qto tool to new qto calculator 2024-06-03 15:16:27 +10:00
Dion Moult 7e8060b224 Fix #4791. Hide irrelevant spatial containment panel for spatial elements. 2024-06-03 14:32:01 +10:00
Dion Moult d408568d01 Fix #4793. Bug where autodetected patch UI failed with new signature typing. 2024-06-03 14:08:54 +10:00
Ryan Schultz 47704b1625 Keeps selected objects selected when switching from one drawing to an… (#4745)
Keeps selected objects selected when switching from one drawing to another. As discussed: proposed: https://community.osarch.org/discussion/2179/not-unselecting-objects-when-switching-from-one-drawing-to-the-other#latest
2024-06-02 09:05:38 -05:00
Dion Moult 23fee9c8c8 Bring back formwork calculation feature that uses remesh 2024-06-02 17:43:25 +10:00
Dion Moult c5d1801e1e Fix #4786. Speed up parent filter. 2024-06-02 17:39:23 +10:00
Ryan Schultz e2dc5f4a48 small tweak 2024-06-01 08:19:41 -05:00
Manu Varkey d28f62d327 Bug fix in editing material set item profile (#4784)
Fixes bug: Editing a profile under MaterialProfileSetUsage opened dialog boxes under all profiles.
2024-06-01 20:58:07 +10:00
Dion Moult 70064359c3 Whoops x2 2024-06-01 20:52:05 +10:00
Dion Moult d8b74cf125 Whoops 2024-06-01 20:30:32 +10:00
Dion Moult 00c13c5ea4 Fix #4756. You can now use parent as a key in a selector key. 2024-06-01 17:30:21 +10:00
Dion Moult e26dae67aa Convenience function for calculators to get maximum XY length
This is useful for 2D profiles
2024-06-01 15:11:29 +10:00
Dion Moult 6e30cd2ba7 Fix #4638. You can now filter by parent (in the spatial hierarchy) 2024-06-01 15:10:45 +10:00
Dion Moult 0f9ea1e1e7 New utility functions to get parent in spatial hierarchy, filled void, and voided element 2024-06-01 15:09:05 +10:00
Dion Moult 128ffc8fd7 Fix bug where selector didn't handle "not equals" correctly for locations or groups 2024-06-01 14:53:55 +10:00
Dion Moult f67911902f Remove obsolete config option for openlca port 2024-06-01 12:29:17 +10:00
Dion Moult 64a1a3eb29 Migrate PyP6Xer dep to install using pip 2024-06-01 12:03:41 +10:00
Dion Moult fcf70ad160 Attempt to bump support to intel mac 10.10 2024-06-01 10:48:36 +10:00
Dion Moult 116c9a5fd9 Continue migrating build deps to pip: pystache, lark, svgwrite, pillow, dateutil, isodate, networkx, deepdiff 2024-06-01 10:38:04 +10:00
Dion Moult 61b10a19c7 Attempt to make shapely be installed using pip 2024-06-01 10:21:40 +10:00
Andrej730 666f8e2459 Bump BBIM ifcopenshell build #4580 2024-05-31 18:28:52 +05:00
Andrej730 c303f216ad Migrate recipe - do not call .migrate twice for each element 2024-05-31 18:20:47 +05:00
Andrej730 162e66b775 typing 2024-05-31 18:20:46 +05:00
Andrej730 c05abc4d5b ifc4x3 support for adding IfcQuantityCount #4776
In IFC4X3 IfcQuantityCount is now more strict and requires only interger values.
Error for a reference:
TypeError: attribute 'CountValue' for entity 'IFC4X3.IfcQuantityCount' is expecting value of type 'INT', got 'float'.
2024-05-31 18:20:46 +05:00
Andrej730 dacb3b101c small docs fix 2024-05-31 18:20:46 +05:00
Andrej730 696c6b09b0 materials ui - preserve selected category on expanding/contracting all categories 2024-05-31 18:20:46 +05:00
Andrej730 6e738a0e3a materials ui - fix bug when it was impossible to contract category
E.g. it was impossible to contract category "TEST" if there was also a material with name "TEST" (and it's category was expanded). It occurred due to a mixup of materials and categories in expanded_categories.

Also changed is_expanded default value to False as it makes more sense (previous value didn't worked for materials and for categories you probably would want it to be False by default.
2024-05-31 18:20:46 +05:00
Andrej730 ba6cdc967c materials ui - shift+click to expand/contract all categories #4771 2024-05-31 18:20:46 +05:00
Andrej730 468baef5c8 materials ui - make expanding/contracting categories more natural
Previously if you had some item active before the category you're trying to expand/cotract it would jump back to that item which was confusing. Now it sets contracted/expanded category as active to prevent that jump.

Before - https://imgur.com/a/87ZDnVq
After - https://imgur.com/a/B9WOn8I

In Blender when we reload materials in collection property it's resetting scroll position in the template_list and setting it based template_list index, so the only way to preserve the scroll position is to manipulate template_list index.
2024-05-31 18:20:46 +05:00
Andrej730 bda9dfd4cc materials ui - avoid having multiple "Uncategorised" categories
Previously it would create multiple same named "Uncategorised" categories for each case when Category was "", None, "Uncategorised", which was confusing and some of them ("" and None) would expand simultaneously when you would try to expand one.

Removed `or "Uncategorised"` from ui.py as there shouldn't be a need for this since all cases are handled when items are added.
2024-05-31 18:20:46 +05:00
Dion Moult 930feb1d52 See #4768. Attempt to use pip for lxml packaging. 2024-05-31 23:06:12 +10:00
Dion Moult e0b11aa66e Fix #4434. User can now explicitly select whether they are choosing a whole or part when changing the aggregate. 2024-05-31 21:38:51 +10:00
Dion Moult b2e7044373 Fix bug where you couldn't change the aggregate of a spatial element (which has been locked during import) 2024-05-31 21:17:51 +10:00
Dion Moult 77abe9a0a5 [BBIM docs] change color of ui breadcrumb on dark background #4777 by @dirkolbrich
Sorry I messed up this merge and did a force push somehow
2024-05-31 21:15:33 +10:00
Dion Moult e2c67c2f37 Revert "fixes #4434"
This reverts commit cdd332d1ee.
2024-05-31 21:11:37 +10:00
Dion Moult a5d123fe83 Map a bunch more qto functions for the IfcOpenShell calculator 2024-05-31 13:09:05 +10:00
Dion Moult b7b99b6db7 Fix #4773. Bug where two materials that shared a style would cause the Blender <-> IFC link to break 2024-05-31 12:54:19 +10:00
Dion Moult 2be0cc697a Fix #4768. Support Python 3.12 version of BBIM. 2024-05-31 11:30:26 +10:00
Dion Moult dc92b973a0 Minor fix 2024-05-31 10:15:03 +10:00
Dion Moult 5466be9b7b Fix #4765. Only run Blender calculator on meshes. 2024-05-31 10:09:56 +10:00
Andrej730 b86dab2a76 fix removing aggregate's collections (and possible orphaned ifc data)
Probably have met this issue before - https://i.imgur.com/EOCSGg2.png

It occurred when you would remove e.g. IfcBuildingStorey's (or other aggregate's) collection from outliner. Deletion operator removes objects first and removing aggregate's main object is automatically removing it's collection. Then it would start removing collections and will break meeting an invalid collection.

Most of the times it was scary but harmless since all collections are probably removed either way but it's critical for batch removal as it would be never finished, possibly leaving unlinked ifc data (e.g. representations and it's items) that's not removed completely until batch removal is finalized.

Traceback:
Error: Python: Traceback (most recent call last):
  File "\blenderbim\bim\ifc.py", line 360, in execute_ifc_operator
    result = getattr(operator, "_execute")(context)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\bim\module\geometry\operator.py", line 720, in _execute
    bpy.data.collections.remove(collection)
ReferenceError: StructRNA of type Collection has been removed
2024-05-30 17:40:13 +05:00
Andrej730 a57e063c14 convert_to_blender to also unlink blender material from ifc material 2024-05-30 17:38:14 +05:00
Gorgious c7830e9e5b Using the "Select IFC class" button doesn't throw an error anymore if an object candidate is in an excluded collection
+ use walrus operator in a few instances
2024-05-29 16:23:05 +02:00
Gorgious d533ba1e6b Remove dead code going back to a time immemorial hack 2024-05-29 16:11:35 +02:00
Andrej730 45a1655485 typing 2024-05-29 18:00:44 +05:00
Andrej730 3679fccfbd reload_representation to support multple objects and optimize the process 2024-05-29 17:20:34 +05:00
Andrej730 cf9f792a3b fix issue unlinking objects after d219856
`unlink_style` doesn't need `tool.Style` anymore and `obj` argument was replaced with `style`
2024-05-29 17:20:34 +05:00
Dion Moult d87665de7e Minor fix 2024-05-29 22:03:22 +10:00
Dion Moult 95daf7efa3 Minor fix to expose both net and gross variants of IfcOpenShell Qto calculator 2024-05-29 21:58:02 +10:00
Dion Moult 435ce8d58a New qto tool is now unit aware 2024-05-29 21:46:23 +10:00
Dion Moult f92a2b3c8e Shift-Q QTO shortcut now works again 2024-05-29 19:37:35 +10:00
Dion Moult 5ea1c94822 Expose all previous calculation functions in new Qto UI 2024-05-29 19:29:48 +10:00
Andrej730 ab1bf7592e fix error exporting schedule of rates #4723
Issue occurred when cost item would have no quantities but had controlled objects with quantities.

1) just added a check to ensure that cost_item.CostQuantities are present
2) removed possible None return value in get_cost_item_quantity as it actually will break process_cost_data and it's never used anywhere else
3) has_changed_name was unused
4) fix possible similar issue in cost.data (though _get_object_quantities method is unused)
2024-05-29 11:49:27 +05:00
Dion Moult f4247c3527 Continue purging tools that were specific to base qtos
Base qtos shouldn't be too special, it should be based off a calculation mapping. This means we can also support IFC2X3 better (which has no base qtos)
2024-05-29 15:56:50 +10:00
Dion Moult 8c3d10a2bd Reimplement manual qty calculator using autodetected calculator functions and redo qto UI 2024-05-29 15:14:11 +10:00
Dion Moult 1bda47f76e Migrate calculator to qto module and mapper to Ifc5D. 2024-05-29 12:45:09 +10:00
Dion Moult 9001aa6383 Purge calculate quantities operator - to be replaced with new ifc5d qto tool
The calculate quantities operator assumes a single mapper, not a configurable calculator.
2024-05-29 12:39:05 +10:00
Ryan Schultz 097c5cee3c fix #3802 - Copy array attributes from another array. Big thank you to @brunoperdigao, for the help! 2024-05-28 21:17:23 -05:00
Dion Moult 9d789425ce Purge guess quantity feature. It will be reintroduced as a separate tool. 2024-05-29 11:19:33 +10:00
Dion Moult c0b86ea016 Fix incorrect colours in Basic FM template 2024-05-29 08:51:27 +10:00
Dion Moult 6806eb0a05 Create blank ifc4 qto base quantities ruleset 2024-05-29 08:41:43 +10:00
Andrej730 ce26d931c1 regenerate ifc4x3 pset templates after the release
Change log

Different props in pset 'Pset_BuildingCommon':
- new props: ElevationOfTerrain, ElevationOfRefHeight
New template 'Pset_DoorLiningProperties' with props: LiningDepth, LiningThickness, ThresholdDepth, ThresholdThickness, TransomThickness, TransomOffset, LiningOffset, ThresholdOffset, CasingThickness, CasingDepth, LiningToPanelOffsetX, LiningToPanelOffsetY
New template 'Pset_DoorPanelProperties' with props: PanelDepth, PanelOperation, PanelWidth, PanelPosition
New template 'Pset_PermeableCoveringProperties' with props: OperationType, PanelPosition, FrameDepth, FrameThickness
Different props in pset 'Pset_Stationing':
- new props: HasIncreasingStation
New template 'Pset_WindowLiningProperties' with props: LiningDepth, LiningThickness, TransomThickness, MullionThickness, FirstTransomOffset, SecondTransomOffset, FirstMullionOffset, SecondMullionOffset, LiningOffset, LiningToPanelOffsetX, LiningToPanelOffsetY
New template 'Pset_WindowPanelProperties' with props: OperationType, PanelPosition, FrameDepth, FrameThickness
2024-05-28 18:01:33 +05:00
Andrej730 eeaa0a5530 Fill IfcPropertySetTemplate.TemplateType in ifc2x3 pset templates 2024-05-28 18:00:22 +05:00
Andrej730 a09cb5dee4 typing 2024-05-28 18:00:21 +05:00
Andrej730 6035683492 sync get_base_qto after 70a4af2 2024-05-28 12:59:34 +05:00
Andrej730 f27dbacd28 prioritize qto base psets over other qto_ qsets after 70a4af2 #4740
1) e.g. in ifc4x3 previously for IfcWall it would prioritize Qto_BodyGeometryValidation over Qto_WallBaseQuantities

2) It would also prioritize general Qto_BodyGeometryValidation over Qto_BuildingElementProxyQuantities.

3) removed "Qto_" check as we do qto_only in get_applicable_names
2024-05-28 12:59:34 +05:00
Andrej730 8817a7e6d5 qtocalculator to support backported EQto_BodyGeometryValidation 2024-05-28 12:59:34 +05:00
Dion Moult 80c4c13998 Basic prototype of new Ifc5D QTO tool in Blender. 2024-05-28 17:33:58 +10:00
Dion Moult 5159e2daee Initial attempt at a qto module in Ifc5D
This is a more flexible, non-Blender approach to doing QTO which allows:

 - Doing QTO without Blender
 - Doing QTO using query filters
 - Using config files for QTOs
 - Choosing your calculation engine, either built-in or creating your own
2024-05-28 17:32:51 +10:00
Dion Moult 54951d2102 Add convenience max/min functions for xyz and side_area shape calculations 2024-05-28 17:30:01 +10:00
Dion Moult 8403c32e76 Adjust selector's behaviour to use the elements argument as a filtering subset
This was always the original intention, but never worked as designed. This is so that you don't need to filter the entire file (which can be huge).
2024-05-28 17:30:01 +10:00
ppaawweeuu 316934de97 Update selector_syntax.rst - small formating change (#4752)
typing
2024-05-28 09:43:51 +10:00
Andrej730 dd48fbcb0f remove dead code core.sequence.highlight_product_related_task
It's deprecated after UI changes in de3dfcd
2024-05-27 17:58:45 +05:00
Andrej730 54978a9868 bim.load_product_related_tasks - show info message when finished
to make ui a bit more responsive
2024-05-27 17:58:45 +05:00
Andrej730 02fd78df14 datetime2ifc to support None values as it was before ebd03e9 #4734 2024-05-27 17:34:28 +05:00
Andrej730 ed1202e1f2 limit quantity types for each resource based on IFC documentation
documentation - https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcConstructionResource.htm#Table-7.3.3.7.1.3.H

e.g. - https://i.imgur.com/PklMyfq.png (previously all quantity types were available for all resources)
2024-05-27 17:30:51 +05:00
Andrej730 d9de967197 specify editing_resource_type values for clarity 2024-05-27 17:30:51 +05:00
Dirk Olbrich e57076c84f [BBIM] Add preference option for default startup workspace, remove ambiguous wording (#4749)
* [BlenderBIM] change ambiguous wording from preferences

* [BlenderBIM] add option for BIM as default workspace

* [BlenderBIM] revert variable name change
2024-05-27 22:19:02 +10:00
Andrej730 75e6495999 load and link ifc files by drag'n'drop 🥳
Example - https://imgur.com/a/kIiqk3R

Now you can drag'n'drop files to Blender and they will get loaded as projects (currently there is no safe check whether current project is saved) or you can hold ALT while drag'n'dropping and link single or multiple IFC files.

Actually, it's possible to set it up that way so multiple files won't need ALT modifier (as we can't load multiple projects, only link them) but I've kept ALT modifier for that case too to avoid confusing situations.

Blender 4.1+ only as there wasn't bpy.types.FileHandler before.
2024-05-27 16:16:57 +05:00
Dion Moult fcf07bebfb Minor doc fix for IfcPatch ConvertLengthUnit
Helps indicate how to get the patched model - it's not an inplace patch
2024-05-27 17:30:17 +10:00
Dion Moult f190ddcd92 Move all N panels back into the scene tabs. No more panels spread around!
Qto, 4D tools, 5D tools now in the Costing/Scheduling tab. The misc panel is now in the sandbox. Git history panel is now with the main Git panel.
2024-05-27 17:29:51 +10:00
Dion Moult 39a841fb4e Fix #3174. Toggling edit mode no longer pops up annoying "do you want to save" dialog.
The default is now everything is saved which should be 90% of the usecase. If you want to discard changes, there is now a button in the top right next to the IFC mode.
2024-05-27 13:09:47 +10:00
Dion Moult bdfd0c4aa7 Minor fix to IfcCityJSON and migrate IfcCityJSON docs to sphinx 2024-05-27 12:24:55 +10:00
Dion Moult 37b14f2a3d Document difference between COBie 2.4 and COBie 2.4 Legacy in IfcFM 2024-05-27 11:59:25 +10:00
Dion Moult 70a4af2605 Fix #4740. Bug where building element proxies didn't work for the auto calculate all button. 2024-05-27 11:02:23 +10:00
Dion Moult 7142f3856c Add documentation for some unit utilities 2024-05-27 09:42:28 +10:00
Ryan Schultz f2ca030fab fix #4311: turn off local view when activating drawing 2024-05-26 18:27:44 -05:00
Dion Moult 37cacfcd06 Fix #4728. Bug where changing class doesn't work with IfcCSV if it didn't need to change anything. 2024-05-26 21:57:23 +10:00
ppaawweeuu 3e31309384 Update selector_syntax.rst - links to regex resources (#4741)
* Update selector_syntax.rst

add links to regex resources

* Update selector_syntax.rst

links misspelling change

* Update selector_syntax.rst

one misspealling more

* Update selector_syntax.rst

small misspelling in thousands separator location
2024-05-26 20:14:41 +10:00
Dion Moult a19b7614fa Fix #4737. Bug where loading profiles with a non-mesh selected would lead to trouble. 2024-05-26 20:12:12 +10:00
Dion Moult 763ee9198d See #3847. Preliminary support for including linked models when generating drawings. 2024-05-26 18:29:26 +10:00
Dion Moult 9ec8035a8d Add support for manual classification references for materials 2024-05-26 17:10:27 +10:00
Dion Moult 687bb32e37 Move material classification into scene tab. No more panels in the material tab! 2024-05-26 16:33:53 +10:00
Sigma Dimensions (Yass) 4279b769aa fix bcf viewpoint setup 2024-05-26 03:44:35 +01:00
Dion Moult 9a59b8cdae Material psets now include category-specific psets and docstrings
Previously it was not possible to get concrete / steel material psets. Yikes!
2024-05-26 11:23:08 +10:00
Dion Moult b9b0e81d25 Move object materials panels further up and profile panel down
Object materials panel is much more important and shouldn't be below the material manager.
2024-05-26 10:04:17 +10:00
Dion Moult 80d31b4e1a Move material psets to material manager. 2024-05-26 09:59:01 +10:00
Ryan Schultz dac9bfe3d4 Add backticks to copied error message 2024-05-25 10:37:52 -05:00
Ryan Schultz 58a7ef62d7 fix #4729: add a description and category when creating a new material 2024-05-25 09:49:15 -05:00
Dion Moult d2198568b2 Deprecate old style panel in the material tab and port over "unlink" operator.
All the functionality is now possible with the style manager and prevents the user needing to know how Blender materials work or wrangle nodes themselves and magically have to comply with the glTF spec.
2024-05-25 23:30:18 +10:00
Dion Moult b087aa09d9 Deprecate old material panel in the material tab and reimplement material duplication in the material manager
The material duplication now 1) actually creates a new blender material 2) uses the API to duplicate. One less panel outside our scene tab.
2024-05-25 15:15:12 +10:00
Dion Moult fd62ba63a5 Do not copy presentation styles when copying a material
Ideally, presentation styles are reused. For example, 5 types of concrete can all use the same presentation
2024-05-25 15:14:07 +10:00
Dion Moult 16c5fd56ca You can now copy a material list 2024-05-25 15:13:36 +10:00
Dion Moult d85f9af790 The API can now copy material sets too 2024-05-25 14:42:45 +10:00
Dion Moult 2f071bd809 Material total now shows the total of the select material class, not all material classes
This is more meaningful. Often the presence of sets will inflate the number artificially.
2024-05-25 14:11:48 +10:00
Andrej730 7a44f9e8a7 different columns for different cost schedules #4724
though columns are still stored in .blend file and not in .ifc

Example - https://imgur.com/a/N6epGZ4
2024-05-24 19:15:51 +05:00
Andrej730 59a19a1a70 disable adding cost schedule columns with an empty name
example - https://i.imgur.com/5lGdI2X.png
2024-05-24 19:15:51 +05:00
Andrej730 6a8b59f2cf remove unused code
1) core.enable_editing_task was outdated, tool.enable_editing_task doesn't exist
2) tool.get_task_outputs was duplicated
2024-05-24 19:15:51 +05:00
Andrej730 ec28d3d0af black format 2024-05-24 19:15:51 +05:00
Andrej730 f576aadee1 typing 2024-05-24 19:15:51 +05:00
Andrej730 fffd7360cb fix for d4ccadb
forgot to add it to global namespace
2024-05-24 19:15:43 +05:00
Andrej730 64ea455c01 move update_simple_openings to tool.Model 2024-05-24 16:52:33 +05:00
Andrej730 cf635986a6 same openings generator for non-/parametric doors/windows #4710 2024-05-24 16:51:37 +05:00
Andrej730 ac8040008a small refactor 2024-05-24 16:51:37 +05:00
Andrej730 df15a37127 fix core.root.assign_class error after 3884403 #4719 2024-05-24 15:22:55 +05:00
Andrej730 6d887d593e fix issues running core tests after 37c008487 2024-05-24 15:20:12 +05:00
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
Thomas Krijnen 90ae709156 Wrapping of mapping 2024-05-23 13:06:57 +02: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
Richard Brice 579768c35f Fixes problem with cant values 2024-05-22 13:51:55 -07: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
Richard Brice a976e63def Fixes divide by zero error for zero-length segments. See Issue #4713. 2024-05-21 16:22:44 -07: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
Richard Brice a45f6f980f Prevents divide by zero when clothoid constant is 0.0 2024-05-20 13:17:18 -07:00
Dion Moult 9122f006c7 Deprecate material attributes panel in favour of new material manager. 2024-05-20 23:21:13 +10:00
Andrej730 678dbaa66a nest api to maintain order of related objects #4698
order of objects in .RelatedObjects is important (e.g. for cost items, it's the order of their appearance), so we should maintain it and cannot use sets for .RelatedObjects
2024-05-20 17:19:00 +05:00
Andrej730 fb88499197 ifc5d - check for mandatory fields in csv header 2024-05-20 17:19:00 +05:00
Andrej730 f84adf67cd typing 2024-05-20 17:19:00 +05:00
Andrej730 cacb38a251 maintain "bim." as operators bl_idname prefix for BlenderBIM
1) removed unused operator import_ifc.bim
2) marked export_ifc.bim as deprecated
2024-05-20 17:19:00 +05:00
Andrej730 7c6c01e6b9 fix python 3.10 loading issue after bb8e84e
The error is below.

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

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

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

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

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

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

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

Specified xsdata version after that change in pyproject.toml.

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

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

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

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

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

Also fixed tests breaking due ifcownerhistory and user/application appearing in ifc2x3.
2024-05-13 18:01:16 +05:00
Gorgious56 3320accc7a The error box is now drawn in red to draw attention to it 2024-05-13 12:00:01 +02:00
Gorgious56 ac686db298 Prevent error when user deletes all spatial structure elements and the spatial manager panel is expanded 2024-05-13 11:57:48 +02:00
Dion Moult aca26c7597 Fix #4659. 2024-05-12 17:16:50 +10:00
Dion Moult b7737cb6d3 Fix #4655. profiles is now available as a keyword in the selector syntax. 2024-05-11 22:57:16 +10:00
Dion Moult 55026ca996 Fix #4616. You can now create a new representation based off any other object. This makes it easy to custom model whatever you want as a starting point. 2024-05-11 22:29:39 +10:00
Dion Moult 3fa573e3c1 Fix #4615. Report to user if they are attempting to create an incompatible context-representation combination. 2024-05-11 20:48:38 +10:00
Dion Moult 00ae6809a9 See #4657. Fallback to copying cache if moving cache fails due to permission error. 2024-05-11 19:59:39 +10:00
Dion Moult c3fbb11093 See #4657. Restrict annotation psets to their particular predefined type. 2024-05-11 19:38:22 +10:00
Kurt Battisti a0d27c3f1c Make Whole status UI message clearer (#4658)
Replace UI text "No Aggregate" with "No Whole relation defined" to avoid misinterpretation when instance is a whole with parts.
2024-05-11 19:04:35 +10:00
Dion Moult 811a908cc3 More segfault fixes in test suite (maybe order matters only on Linux?) 2024-05-11 19:01:57 +10:00
Dion Moult 94e5baf48a Fix #4647. Better error reporting if the user tries to activate a drawing that isn't available. 2024-05-11 19:01:21 +10:00
Dion Moult cd2570cdbb Fix #4649. Bug where editing property set template enumerations didn't clean up properly. 2024-05-11 15:18:18 +10:00
Dion Moult 6df6553935 Notify user if duration parsing not available in util.date 2024-05-11 15:14:39 +10:00
Dion Moult 8e6c372a4f Fix ability to run tests and segfaulting test 2024-05-11 15:00:30 +10:00
Andrej730 371e11de9b root.remove_product to consider IfcRelConnectsPortToElement 2024-05-10 19:18:09 +05:00
Andrej730 d2a708fcc4 fix bug in ifc2x3 tests bootstrap
it was creating new applications and users every time `ifcopenshell.api.owner.settings.get_user` or `ifcopenshell.api.owner.settings.get_application` was called
2024-05-10 19:18:09 +05:00
Andrej730 845d772a08 move sequence.get_related_products to util module 2024-05-10 19:18:09 +05:00
Andrej730 1e630ede08 ifc2x3 tests 2024-05-10 19:18:09 +05:00
Andrej730 c50d1ea2fe typing 2024-05-10 19:18:08 +05:00
Dion Moult e7eb00eaa3 See #4652. Fix bug where auto-detection of a false origin could be incorrect
We were incorrectly multiplying a shape matrix in SI units with a vertex in project units. We also didn't do a final check whether or not that final resultant coordinate was far away or not (for example, origin and vertex can cancel each other out)
2024-05-10 23:28:37 +10:00
Dion Moult cd6eac3c8f See #4652. Fix bug where using create_mesh won't work if you pass it a triangulation. 2024-05-10 22:39:56 +10:00
Dion Moult 69e21da60c Minor naming changes in IfcFM to be more true to IFC jargon 2024-05-10 22:13:39 +10:00
Dion Moult 57b905862a Fix #4650. When the bbim build hotfixes the bot build, it should replace, not copy over. 2024-05-10 18:20:41 +10:00
Dion Moult 51d3a2b77b Whoops 2024-05-10 16:16:43 +10:00
Andrej730 b2920996be remove deprecated api method type.get_related_objects 2024-05-09 18:15:49 +05:00
Andrej730 bdc5a7a01b more descriptive errors creating entities - setting incorrect attributes
example:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```
import ifcopenshell

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

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

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

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

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

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

more test coverage - test converting to more units and back from them
2024-04-24 15:15:30 +05:00
Andrej730 c5f084a4af ConvertLengthUnit test 2024-04-24 14:12:13 +05:00
Andrej730 606a0b46f2 MergeProject - support merging projects with different units 2024-04-24 14:12:13 +05:00
Andrej730 1c729a74d1 util.unit to support converting aggregate of aggregates 2024-04-24 14:12:13 +05:00
Andrej730 db31574103 typing 2024-04-24 14:12:13 +05:00
stefkeB d03b47e1e6 Fix HDF5 conditional inclusion in GeomTree
IfcGeomTree uses HDF5, but this is an optional module, so we have to wrap it with #ifdef WITH_HDF5
2024-04-23 18:46:10 +02:00
Ryan Schultz ae62ac2777 small tweak to b8a9674483 - duplicate profile has '_copy' suffix. 2024-04-23 07:42:02 -05:00
Andrej730 998f9c6a1e make styles ui more similar to profiles and materials ui
before - https://i.imgur.com/bTbyt2t.png
after - https://i.imgur.com/U53h8Yh.png
2024-04-23 16:39:49 +05:00
Andrej730 3ff31b577c bim.duplicate_style 2024-04-23 16:39:49 +05:00
Andrej730 cd013079f4 bim.duplicate_material 2024-04-23 16:39:49 +05:00
Andrej730 b8a9674483 bim.duplicate_profile
simple operator for profile duplication
example - https://imgur.com/a/dsP78iV
2024-04-23 16:39:49 +05:00
Andrej730 8beb1bef98 preserve selected profile index removing profiles 2024-04-23 16:39:38 +05:00
Andrej730 377676f881 dev environment - add libs/desktop and bsdd 2024-04-23 15:23:16 +05:00
Andrej730 4efe284338 use IFC4X3_ADD2 for ifcopenshell.schema_by_name
as it is the main IFC4X3 schema
2024-04-22 17:30:25 +05:00
Andrej730 34a4e3f37a util.doc - use release schema version for IFC4X3
As it will be more like to be available in the ifcopenshell package. Related to #4565
2024-04-22 17:30:25 +05:00
Andrej730 889c9a9e9f typing 2024-04-22 17:30:25 +05:00
Andrej730 9e79499532 small optimization 2024-04-22 17:30:24 +05:00
Kristof Semjen 589b98053e Fixes #4261
SWIG_Python_str_AsChar and SWIG_Python_str_DelForPy3 are no longer available in swig 4.2 (see : https://github.com/swig/swig/commit/f89dd59d4b82ece899087682fdb86e94d2611513 ),
this commit fixes the build for swig versions > 4.2.
2024-04-21 16:22:56 +02:00
Bruno Postle fc5bb230a9 Abandon SVG layout generation on missing drawings
Drawings need to be generated before layout can be generated, so
remove a partial failed layout caused by missing drawings.
2024-04-21 11:12:24 +01:00
Bruno Perdigão 01361cc8f2 Merge branch 'v0.7.0' into fix-normal-duplication-of-linked-aggregate 2024-04-18 21:00:39 -03:00
Bruno Perdigão e6bef16c45 small deletions 2024-03-18 23:39:06 -03:00
Bruno Perdigão 4829c4c02b fix: when subaggregate that is a linked aggregate is unassinged from the main aggregate, it remains a linked aggregate. 2024-03-16 15:27:04 -03:00
Bruno Perdigão 3bf4a8ef52 small refactor 2024-03-16 12:44:26 -03:00
Bruno Perdigão c95a47ca97 fix #4430. Shift+D whem selecting all the objects of a linked aggregate creates a normal copy that is not linked 2024-03-15 21:34:27 -03:00
825 changed files with 35590 additions and 28812 deletions
@@ -1,4 +1,4 @@
name: Publish-blenderbim-multiplatform
name: ci-blenderbim-daily
on:
push:
@@ -16,12 +16,8 @@ on:
- 'src/ifc5d/ifc5d/**'
- 'src/ifccityjson/**'
branches:
- v0.7.0
env:
major: 0
minor: 0
name: blenderbim
- v0.8.0
workflow_dispatch:
jobs:
activate:
@@ -39,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311]
pyver: [py39, py310, py311, py312]
config:
- {
name: "Windows Build",
@@ -63,21 +59,23 @@ jobs:
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
- run: echo ${{ env.DATE }}
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
- name: Compile
run: |
cp -r src/blenderbim src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
cd src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
- name: Upload Zip file to release
- name: Upload zip file to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }}/dist/blenderbim-${{steps.date.outputs.date}}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip
asset_name: blenderbim-${{steps.date.outputs.date}}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip
tag: "blenderbim-${{steps.date.outputs.date}}"
file: src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }}/dist/blenderbim-${{steps.version.outputs.version}}.${{steps.date.outputs.date}}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip
asset_name: blenderbim-${{steps.version.outputs.version}}.${{steps.date.outputs.date}}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip
release_name: "blenderbim-${{steps.version.outputs.version}}.${{steps.date.outputs.date}} (unstable)"
tag: "blenderbim-${{steps.version.outputs.version}}.${{steps.date.outputs.date}}"
overwrite: true
body: "Daily developer testing build blenderbim-${{steps.date.outputs.date}}"
+62
View File
@@ -0,0 +1,62 @@
name: ci-blenderbim
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312]
config:
- {
name: "Windows Build",
short_name: win,
}
- {
name: "Linux Build",
short_name: linux
}
- {
name: "MacOS Build",
short_name: macos
}
- {
name: "MacOS ARM Build",
short_name: macosm1
}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Compile
run: |
cp -r src/blenderbim src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
cd src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} IS_STABLE=TRUE
- name: Upload zip file to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/blenderbim_${{ matrix.config.short_name }}_${{ matrix.pyver }}/dist/blenderbim-${{steps.version.outputs.version}}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip
asset_name: blenderbim-${{steps.version.outputs.version}}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip
release_name: "blenderbim-${{steps.version.outputs.version}}"
tag: "blenderbim-${{steps.version.outputs.version}}"
overwrite: true
@@ -1,7 +1,10 @@
name: CD
name: ci-ifcopenshell-docker
on:
workflow_dispatch:
push:
tags:
- v0.**
jobs:
@@ -1,19 +1,7 @@
name: ci-ifcopenshell-pypi
name: ci-ifcopenshell-python-pypi
on:
workflow_dispatch:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# * * * * *
- cron: "0 0 18 * *"
push:
paths:
- '.github/workflows/ci-ifcopenshell-pypi.yml'
env:
major: 0
@@ -40,19 +28,19 @@ jobs:
config:
- {
name: "Windows Build",
short_name: win,
short_name: win64,
}
- {
name: "Linux Build",
short_name: linux
short_name: linux64
}
- {
name: "MacOS Build",
short_name: macos
short_name: macos64
}
- {
name: "MacOS ARM Build",
short_name: macosm1
short_name: macosm164
}
steps:
- uses: actions/checkout@v2
@@ -0,0 +1,63 @@
name: ci-ifcopenshell-python
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
name: ${{ matrix.config.name }}-${{ matrix.pyver }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
pyver: [py39, py310, py311, py312]
config:
- {
name: "Windows Build",
short_name: win64,
}
- {
name: "Linux Build",
short_name: linux64
}
- {
name: "MacOS Build",
short_name: macos64
}
- {
name: "MacOS ARM Build",
short_name: macosm164
}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Compile
run: |
cp -r src/ifcopenshell-python src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
cd src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }} &&
make zip PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
- name: Upload zip file to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/ifcopenshell_${{ matrix.config.short_name }}_${{ matrix.pyver }}/dist/ifcopenshell-python-${{ steps.version.outputs.version }}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip
asset_name: ifcopenshell-python-${{ steps.version.outputs.version }}-${{ matrix.pyver }}-${{ matrix.config.short_name }}.zip
release_name: "ifcopenshell-python-${{steps.version.outputs.version}}"
tag: "ifcopenshell-python-${{steps.version.outputs.version}}"
overwrite: true
@@ -1,17 +1,13 @@
name: Publish-ifcsverchok
name: ci-ifcsverchok-daily
on:
workflow_dispatch:
push:
paths:
- '.github/workflows/ci-ifcsverchok-build.yml'
- 'src/ifcsverchok/*'
branches:
- v0.7.0
env:
major: 0
minor: 0
name: ifcsverchok
- v0.8.0
jobs:
activate:
@@ -34,20 +30,22 @@ jobs:
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
- run: echo ${{ env.DATE }}
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%y%m%d')"
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
- name: Compile
run: |
cd src/ifcsverchok
make dist
- name: Upload Zip file to release
- name: Upload zip file to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/ifcsverchok/dist/ifcsverchok-${{steps.date.outputs.date}}.zip
asset_name: ifcsverchok-${{steps.date.outputs.date}}.zip
tag: "ifcsverchok-${{steps.date.outputs.date}}"
file: src/ifcsverchok/dist/ifcsverchok-${{steps.version.outputs.version}}.${{steps.date.outputs.date}}.zip
asset_name: ifcsverchok-${{steps.version.outputs.version}}.${{steps.date.outputs.date}}.zip
release_name: "ifcsverchok-${{steps.version.outputs.version}}.${{steps.date.outputs.date}}"
tag: "ifcsverchok-${{steps.version.outputs.version}}.${{steps.date.outputs.date}}"
overwrite: true
body: "ifcsverchok build for ${{steps.date.outputs.date}}"
+42
View File
@@ -0,0 +1,42 @@
name: ci-ifcsverchok
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
name: ifcsverchok
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
python-version: '3.11'
- name: Get current version
id: version
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Compile
run: |
cd src/ifcsverchok
make dist IS_STABLE=TRUE
- name: Upload zip file to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/ifcsverchok/dist/ifcsverchok-${{steps.version.outputs.version}}.zip
asset_name: ifcsverchok-${{steps.version.outputs.version}}.zip
release_name: "ifcsverchok-${{steps.version.outputs.version}}"
tag: "ifcsverchok-${{steps.version.outputs.version}}"
overwrite: true
+1 -1
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
+1
View File
@@ -0,0 +1 @@
0.7.9
+1251 -1244
View File
File diff suppressed because it is too large Load Diff
+46 -1
View File
@@ -91,6 +91,8 @@ HDF5_VERSION = "1.12.1"
GMP_VERSION = "6.2.1"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "5.3"
USD_VERSION = "23.05"
TBB_VERSION = "2021.9.0"
# binaries
cp = "cp"
@@ -203,6 +205,7 @@ dependency_tree = {
'cgal': (),
'eigen': (),
'freetype': (),
# 'usd': ('boost', 'oneTBB')
}
def v(dep):
@@ -770,7 +773,42 @@ if "cgal" in targets:
download_tool=download_tool_git,
revision=f"v{CGAL_VERSION}"
)
if "usd" in targets:
build_dependency(
name=f"oneTBB-{TBB_VERSION}",
mode="cmake",
build_tool_args=[
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/tbb-{TBB_VERSION}",
f"-DTBB_TEST=OFF"
],
download_url="https://github.com/oneapi-src/oneTBB",
download_name="oneTBB",
download_tool=download_tool_git,
revision=f"v{TBB_VERSION}"
)
build_dependency(
name=f"usd-{USD_VERSION}",
mode="cmake",
build_tool_args=[
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/usd-{USD_VERSION}",
f"-DBOOST_ROOT={DEPS_DIR}/install/boost-{BOOST_VERSION}",
f"-DTBB_ROOT_DIR={DEPS_DIR}/install/tbb-{TBB_VERSION}",
f"-DPXR_ENABLE_PYTHON_SUPPORT=FALSE",
f"-DPXR_ENABLE_GL_SUPPORT=FALSE",
f"-DPXR_BUILD_IMAGING=FALSE",
f"-DPXR_BUILD_TUTORIALS=FALSE",
f"-DPXR_BUILD_EXAMPLES=FALSE",
f"-DPXR_BUILD_USD_TOOLS=FALSE",
f"-DPXR_BUILD_TESTS=FALSE"
],
download_url="https://github.com/PixarAnimationStudios/USD",
download_name="USD",
download_tool=download_tool_git,
revision=f"v{USD_VERSION}"
)
cecho("Building IfcOpenShell:", GREEN)
IFCOS_DIR = os.path.join(DEPS_DIR, "build", "ifcopenshell")
@@ -854,6 +892,13 @@ if "hdf5" in targets:
else:
cmake_args.append("-DHDF5_SUPPORT=Off")
if "usd" in targets:
cmake_args.extend([
"-DUSD_SUPPORT=" "On",
"-DUSD_INCLUDE_DIR=" f"{DEPS_DIR}/install/usd-{USD_VERSION}/include",
"-DUSD_LIBRARY_DIR=" f"{DEPS_DIR}/install/usd-{USD_VERSION}/lib"
])
if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets):
logger.info("\rConfiguring executables...")
+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)
+78 -242
View File
@@ -16,8 +16,8 @@
# 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/>.
PYTHON:=python3.10
PIP:=pip3.10
PYTHON:=python3.11
PIP:=pip3.11
PATCH:=patch
SED:=sed -i
ifeq ($(OS),Windows_NT)
@@ -35,7 +35,12 @@ SED:=sed -i '' -e
endif
endif
VERSION:=`date '+%y%m%d'`
IS_STABLE:=FALSE
VERSION:=$(shell cat ../../VERSION)
VERSION_MAJOR:=$(shell cat '../../VERSION' | cut -d '.' -f 1)
VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
PYVERSION:=py310
PYPI_IMP:=cp
@@ -55,70 +60,28 @@ PYLIBDIR:=python3.11
PYNUMBER:=311
PYPI_VERSION:=3.11
endif
ifeq ($(PYVERSION), py312)
PYLIBDIR:=python3.12
PYNUMBER:=312
PYPI_VERSION:=3.12
endif
ifeq ($(PLATFORM), linux)
PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
LXML_ARCH:=manylinux_2_28_x86_64
SHAPELY_ARCH:=manylinux_2_17_x86_64.manylinux2014_x86_64
PILLOW_ARCH:=manylinux_2_28_x86_64
endif
ifeq ($(PLATFORM), macos)
PYPI_PLATFORM:=--platform macosx_10_9_x86_64
SHAPELY_ARCH:=macosx_10_9_x86_64
PILLOW_ARCH:=macosx_10_10_x86_64
ifeq ($(PYVERSION), py39)
LXML_ARCH:=macosx_11_0_x86_64
endif
ifeq ($(PYVERSION), py310)
LXML_ARCH:=macosx_11_0_x86_64
endif
ifeq ($(PYVERSION), py311)
LXML_ARCH:=macosx_11_0_universal2
endif
PYPI_PLATFORM:=--platform macosx_10_10_x86_64
endif
ifeq ($(PLATFORM), macosm1)
PYPI_PLATFORM:=--platform macosx_11_0_arm64
LXML_ARCH:=macosx_11_0_universal2
SHAPELY_ARCH:=macosx_11_0_arm64
PILLOW_ARCH:=macosx_11_0_arm64
endif
ifeq ($(PLATFORM), win)
PYPI_PLATFORM:=--platform win_amd64
LXML_ARCH:=win_amd64
SHAPELY_ARCH:=win_amd64
PILLOW_ARCH:=win_amd64
endif
LXML_NAME:=lxml
LXML_VER:=4.9.4
SHAPELY_NAME:=shapely
SHAPELY_VER:=2.0.2
PILLOW_NAME:=pillow
PILLOW_PKG_NAME:=Pillow
PILLOW_VER:=9.5.0
SHAPELY_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/s/$(SHAPELY_NAME)/$(SHAPELY_NAME)-$(SHAPELY_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(SHAPELY_ARCH).whl
PILLOW_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/p/$(PILLOW_NAME)/$(PILLOW_PKG_NAME)-$(PILLOW_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(PILLOW_ARCH).whl
ifeq ($(PLATFORM), macosm1)
ifeq ($(PYVERSION), py39)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py39h0520ce3_1.tar.bz2
endif
ifeq ($(PYVERSION), py310)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py310h02f21da_0.tar.bz2
endif
ifeq ($(PYVERSION), py311)
LXML_URL:=https://anaconda.org/conda-forge/lxml/4.9.1/download/osx-arm64/lxml-4.9.1-py311h246f609_1.tar.bz2
endif
else
LXML_URL:=https://files.pythonhosted.org/packages/cp$(PYNUMBER)/l/$(LXML_NAME)/$(LXML_NAME)-$(LXML_VER)-cp$(PYNUMBER)-cp$(PYNUMBER)-$(LXML_ARCH).whl
endif
$(info $$LXML_URL is [${LXML_URL}])
$(info $$SHAPELY_URL is [${SHAPELY_URL}])
$(info $$PILLOW_URL is [${PILLOW_URL}])
.PHONY: bump
bump:
cd . && $(SED) "s/$(OLD)/$(NEW)/" Makefile
@@ -137,52 +100,54 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.8.0-90ae709-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
# See bug #812
cd dist/working && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/blenderbim/occ_utils.py
cd dist/working && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.8.0/src/blenderbim/occ_utils.py
cd dist/working && mv occ_utils.py ../blenderbim/libs/site/packages/ifcopenshell/geom/occ_utils.py
rm -rf dist/working
# Provides dependencies that are part of IfcOpenShell
mkdir dist/working
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/v0.7.0.zip
cd dist/working && unzip v0.7.0.zip
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/archive/refs/heads/v0.8.0.zip
cd dist/working && unzip v0.8.0.zip
# IfcOpenBot sometimes lags behind, so we hotfix the Python utilities
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/*.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.7.0/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py dist/blenderbim/libs/site/packages/ifcopenshell/express/
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/util/
rm -rf dist/blenderbim/libs/site/packages/ifcopenshell/api/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifcopenshell-python/ifcopenshell/util dist/blenderbim/libs/site/packages/ifcopenshell/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifcopenshell-python/ifcopenshell/api dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.8.0/src/ifcopenshell-python/ifcopenshell/*.py dist/blenderbim/libs/site/packages/ifcopenshell/
cp dist/working/IfcOpenShell-0.8.0/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py dist/blenderbim/libs/site/packages/ifcopenshell/express/
# Provides bcf functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/bcf/src/bcf dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/bcf/src/bcf dist/blenderbim/libs/site/packages/
# Provides IFCClash functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcclash/ifcclash dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifcclash/ifcclash dist/blenderbim/libs/site/packages/
# Provides BIMTester functionality
cd dist/working && $(PYTHON) -m venv env
cd dist/working && . env/bin/activate && $(PIP) install pybabel
cd dist/working && . env/bin/activate && $(PIP) install babel
cd dist/working && . env/bin/activate && ./env/bin/pybabel compile -d ./IfcOpenShell-0.7.0/src/ifcbimtester/bimtester/locale/
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcbimtester/bimtester dist/blenderbim/libs/site/packages/
cd dist/working && . env/bin/activate && ./env/bin/pybabel compile -d ./IfcOpenShell-0.8.0/src/ifcbimtester/bimtester/locale/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifcbimtester/bimtester dist/blenderbim/libs/site/packages/
# Provides IFCTester functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifctester/ifctester dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifctester/ifctester dist/blenderbim/libs/site/packages/
# Provides IFCFM functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcfm/ifcfm dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifcfm/ifcfm dist/blenderbim/libs/site/packages/
# Provides bSDD functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/bsdd/* dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/bsdd/* dist/blenderbim/libs/site/packages/
# Provides IFCDiff functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcdiff/* dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifcdiff/* dist/blenderbim/libs/site/packages/
# Provides IFCCSV functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccsv/* dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifccsv/* dist/blenderbim/libs/site/packages/
# Provides IFCPatch functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifcpatch/ifcpatch dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifcpatch/ifcpatch dist/blenderbim/libs/site/packages/
# Provides IFC4D functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifc4d/ifc4d dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifc4d/ifc4d dist/blenderbim/libs/site/packages/
# Provides IFC5D functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifc5d/ifc5d dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifc5d/ifc5d dist/blenderbim/libs/site/packages/
# Provides IFCCityJSON functionality
cp -r dist/working/IfcOpenShell-0.7.0/src/ifccityjson dist/blenderbim/libs/site/packages/
cp -r dist/working/IfcOpenShell-0.8.0/src/ifccityjson dist/blenderbim/libs/site/packages/
# Provides IfcGit functionality
cd dist/working && . env/bin/activate && $(PIP) install GitPython --target=./site-packages
cp -r dist/working/site-packages/smmap dist/blenderbim/libs/site/packages/
@@ -190,49 +155,6 @@ endif
cp -r dist/working/site-packages/git dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides Mustache templating in construction documentation
# TODO: remove this dependency, it seems overkill and we seem to get by with str replaces
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/3f/e7/8750ba6c6101d6aa5ceeb20c013adf2c6f3554a12c71d75654b468404bfa/pystache-0.6.0.tar.gz
cd dist/working && tar -xzvf pystache*
cd dist/working/pystache-0.6.0/ && $(PYTHON) setup.py build && cp -r build/lib/pystache ../../blenderbim/libs/site/packages/
rm -rf dist/working
# Provides SVG export in construction documentation
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/79/e8/7eb2ba188eda14a4b47e33b51f3b4978985f4116655c699bcd18c79279b5/svgwrite-1.3.1.zip
cd dist/working && unzip svgwrite*
cp -r dist/working/svgwrite-1.3.1/svgwrite dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides fuzzy date parsing for construction sequencing
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/be/ed/5bbc91f03fa4c839c4c7360375da77f9659af5f7086b7a7bdda65771c8e0/python-dateutil-2.8.1.tar.gz
cd dist/working && tar -xzvf python-dateutil*
cp -r dist/working/python-dateutil-2.8.1/dateutil dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides duration parsing for construction sequencing
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b1/80/fb8c13a4cd38eb5021dc3741a9e588e4d1de88d895c1910c6fc8a08b7a70/isodate-0.6.0.tar.gz
cd dist/working && tar -xzvf isodate*
cp -r dist/working/isodate-0.6.0/src/isodate dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides networkx graph analysis for project dependency calculations
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b0/21/adfbf6168631e28577e4af9eb9f26d75fe72b2bb1d33762a5f2c425e6c2a/networkx-2.5.1.tar.gz
cd dist/working && tar -xzvf networkx*
cp -r dist/working/networkx-2.5.1/networkx dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by networkx
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/4f/51/15a4f6b8154d292e130e5e566c730d8ec6c9802563d58760666f1818ba58/decorator-5.0.9.tar.gz
cd dist/working && tar -xzvf decorator*
cp -r dist/working/decorator-5.0.9/src/decorator.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Provides audio playback for costing
mkdir -p dist/working
git clone https://github.com/Andrej730/aud.git --branch master-reduced-size --depth 1 dist/working/aud
@@ -247,107 +169,6 @@ endif
cp dist/working/jsgantt* dist/blenderbim/bim/data/gantt/
rm -rf dist/working
# Required by IFCDiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/0f/ca/caead2949fbb824c7142e3774fa841aa853bb4d4331b440da8c8514dfc6f/deepdiff-5.8.1.tar.gz
cd dist/working && tar -xzvf deepdiff*
cp -r dist/working/deepdiff-5.8.1/deepdiff dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by deepdiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/00/55/ce2cbc6d64034b30cad81a29ba61bdba456f190f5e83c09831304bf68d6b/jsonpickle-1.2.tar.gz
cd dist/working && tar -xzvf jsonpickle*
cp -r dist/working/jsonpickle-1.2/jsonpickle dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by deepdiff
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/a3/b7/d4d69641cbe707a45c23b190f2d717466ba5accc4c70b5f7a8a450387895/ordered-set-3.1.1.tar.gz
cd dist/working && tar -xzvf ordered-set*
cp -r dist/working/ordered-set-3.1.1/ordered_set.py dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by lark
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/00/32/8076fa13e832bb4dcff379f18f228e5a53412be0631808b9ca2610c0f566/pyparsing-2.4.5.tar.gz
cd dist/working && tar -xzvf pyparsing*
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
cd dist/working && tar -xzvf lark-parser*
cp -r dist/working/lark-parser-0.8.5/lark dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by IFC4D
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/b0/bb/9c4dddd3ca173cb56241cfb2eddfae24690dc676d357ac4cab17d0a36d9d/PyP6Xer-1.13.0.tar.gz
cd dist/working && tar -xzvf PyP6Xer*
cp -r dist/working/PyP6Xer-1.13.0/xerparser dist/blenderbim/libs/site/packages/
rm -rf dist/working
# Required by QTOCalculator
mkdir dist/working
cd dist/working && wget $(SHAPELY_URL)
cd dist/working && cp *.whl shapely.zip && unzip shapely.zip
cp -r dist/working/shapely dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), win)
cp -r dist/working/shapely.libs dist/blenderbim/libs/site/packages/
endif
ifeq ($(PLATFORM), linux)
cp -r dist/working/shapely.libs dist/blenderbim/libs/site/packages/
endif
rm -rf dist/working
# Required by the BIM tool thumbnail generator
mkdir dist/working
cd dist/working && wget $(PILLOW_URL)
cd dist/working && cp *.whl pillow.zip && unzip pillow.zip
cp -r dist/working/PIL dist/blenderbim/libs/site/packages/
ifeq ($(PLATFORM), linux)
cp -r dist/working/Pillow.libs dist/blenderbim/libs/site/packages/
endif
rm -rf dist/working
# Required by xerparser and IFC4D
# TODO: remove this dependency. It's only used to show a progress bar.
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/9f/7b/76c4e5ef1a1b528fcaada4133f972e77d33c252831676cf414119ca6093d/tqdm-4.50.0.tar.gz
cd dist/working && tar -xzvf tqdm*
cp -r dist/working/tqdm-4.50.0/tqdm dist/blenderbim/libs/site/packages/
rm -rf dist/working
mkdir dist/working
cd dist/working && $(PYTHON) -m venv env
cd dist/working && mkdir site-packages
@@ -364,7 +185,34 @@ 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
# Required by drawing module
cd dist/working && . env/bin/activate && $(PIP) install lxml $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Required by qto and drawing module
cd dist/working && . env/bin/activate && $(PIP) install shapely $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Required by the BIM type manager thumbnail generator
cd dist/working && . env/bin/activate && $(PIP) install pillow $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --target=./site-packages
# Provides mustache templating in construction docs and web UI data
cd dist/working && . env/bin/activate && $(PIP) install pystache --target=./site-packages
# Provides SVG export in construction documentation
cd dist/working && . env/bin/activate && $(PIP) install svgwrite --target=./site-packages
# Provides fuzzy date parsing for construction sequencing
cd dist/working && . env/bin/activate && $(PIP) install python-dateutil --target=./site-packages
# Provides duration parsing for construction sequencing
cd dist/working && . env/bin/activate && $(PIP) install isodate --target=./site-packages
# Provides networkx graph analysis for project dependency calculations
cd dist/working && . env/bin/activate && $(PIP) install networkx --target=./site-packages
# Required by IFCDiff
cd dist/working && . env/bin/activate && $(PIP) install deepdiff --target=./site-packages
# Required by IFCCSV and ifcopenshell.util.selector
cd dist/working && . env/bin/activate && $(PIP) install lark --target=./site-packages
# Required by IFC4D
cd dist/working && . env/bin/activate && $(PIP) install PyP6Xer --target=./site-packages
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
@@ -374,33 +222,15 @@ endif
cd dist/working && tar -xzvf behave*
cd dist/working/behave-1.2.6/ && cp -r behave ../../blenderbim/libs/site/packages/
# See bug #1294
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/model_core.patch
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/runner_util.patch
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.8.0/src/ifcbimtester/patch/model_core.patch
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.8.0/src/ifcbimtester/patch/runner_util.patch
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/model_core.py < model_core.patch
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/runner_util.py < runner_util.patch
# See bug #2159
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.7.0/src/ifcbimtester/patch/junit.patch
cd dist/working/ && wget https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.8.0/src/ifcbimtester/patch/junit.patch
cd dist/working/ && $(PATCH) ../blenderbim/libs/site/packages/behave/reporter/junit.py < junit.patch
rm -rf dist/working
# Required by ids - uh, not any more, but I'm trying it out in the drawing module
ifeq ($(PLATFORM), macosm1)
# No wheels available for MacOS M1, but turns out this Anaconda build works
mkdir dist/working
cd dist/working && wget $(LXML_URL)
cd dist/working && tar -xf lxml*
cp -r dist/working/lib/$(PYLIBDIR)/site-packages/lxml dist/blenderbim/libs/site/packages/
rm -rf dist/working
else
# Wheels are preferred for other OSes. A first attempt at using Anaconda
# builds for everything shows it breaks on windows. See #2422.
mkdir dist/working
cd dist/working && wget $(LXML_URL)
cd dist/working && cp *.whl lxml.zip && unzip lxml.zip
cp -r dist/working/lxml dist/blenderbim/libs/site/packages/
rm -rf dist/working
endif
# Required by behave
mkdir dist/working
cd dist/working && wget https://files.pythonhosted.org/packages/f4/65/220bb4075fddb09d5b3ea2c1c1fa66c1c72be9361ec187aab50fa161e576/parse-1.15.0.tar.gz
@@ -432,7 +262,7 @@ endif
# Required by brickschema
# This is a bit of a dodgy one, it should be provided by setuptools which Blender doesn't ship.
mkdir dist/working
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/raw/v0.7.0/src/blenderbim/scripts/pkg_resources.tar.gz
cd dist/working && wget https://github.com/IfcOpenShell/IfcOpenShell/raw/v0.8.0/src/blenderbim/scripts/pkg_resources.tar.gz
cd dist/working && tar -xzvf pkg_resources*
cd dist/working/ && cp -r pkg_resources ../blenderbim/libs/site/packages/
rm -rf dist/working
@@ -456,10 +286,16 @@ endif
rm -rf dist/blenderbim/libs/site/packages/numpy
rm -rf dist/blenderbim/libs/site/packages/numpy.libs
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" __init__.py
cd dist/blenderbim && $(SED) "s/999999/$(VERSION)/" blender_manifest.toml
cd dist/blenderbim/bim && $(SED) "s/8888888/$(LAST_COMMIT_HASH)/" __init__.py
ifeq ($(IS_STABLE), TRUE)
cd dist/blenderbim && $(SED) "s/0, 0, 0/$(VERSION_MAJOR), $(VERSION_MINOR), $(VERSION_PATCH)/" __init__.py
cd dist/blenderbim && $(SED) "s/0.0.0/$(VERSION)/" blender_manifest.toml
cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./*
else
cd dist/blenderbim && $(SED) "s/0, 0, 0/$(VERSION_MAJOR), $(VERSION_MINOR), $(VERSION_PATCH), $(VERSION_DATE)/" __init__.py
cd dist/blenderbim && $(SED) "s/0.0.0/$(VERSION).$(VERSION_DATE)/" blender_manifest.toml
cd dist/blenderbim/bim && $(SED) "s/8888888/$(LAST_COMMIT_HASH)/" __init__.py
cd dist && zip -r blenderbim-$(VERSION).$(VERSION_DATE)-$(PYVERSION)-$(PLATFORM).zip ./*
endif
rm -rf dist/blenderbim
.PHONY: test
+168 -8
View File
@@ -18,30 +18,190 @@
import os
import sys
import site
# Ensure we don't try to import bpy or blenderbim.bim
# to support running core tests.
# We assume if bpy was never loaded in current python session
# then we're not in Blender. It's still possible to use
# bpy in core and core tests for annotations using TYPE_CHECKING.
IN_BLENDER = sys.modules.get("bpy", None)
if IN_BLENDER:
import bpy
import platform
import traceback
import webbrowser
import addon_utils
from collections import deque
bl_info = {
"name": "BlenderBIM",
"description": "Transforms Blender into a native Building Information Model authoring platform using IFC.",
"author": "IfcOpenShell Contributors",
"blender": (3, 1, 0),
"version": (0, 0, 999999),
"version": (0, 0, 0),
"location": "File Menu, Scene Properties Tab. See documentation for more.",
"doc_url": "https://docs.blenderbim.org/",
"tracker_url": "https://github.com/IfcOpenShell/IfcOpenShell/issues",
"category": "System",
}
if sys.modules.get("bpy", None):
last_error = None
last_actions: deque = deque(maxlen=10)
def get_debug_info():
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
return {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"last_actions": last_actions,
"last_error": last_error,
}
def format_debug_info(info: dict):
last_actions = ""
for action in info["last_actions"]:
last_actions += f"\n# {action['type']}: {action['name']}"
if settings := action.get("settings"):
last_actions += f"\n>>> {settings}"
info["last_actions"] = last_actions
text = "\n".join(f"{k}: {v}" for k, v in info.items())
return text.strip()
if IN_BLENDER:
# Process *.pth in /libs/site/packages to setup globally importable modules
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
import blenderbim.bim
try:
import blenderbim.bim
import ifcopenshell.api
def register():
blenderbim.bim.register()
def log_api(usecase_path, ifc_file, settings):
last_actions.append(
{
"type": "ifcopenshell.api",
"name": usecase_path,
"settings": ifcopenshell.api.serialise_settings(settings),
}
)
def unregister():
blenderbim.bim.unregister()
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def register():
blenderbim.bim.register()
def unregister():
blenderbim.bim.unregister()
except:
last_error = traceback.format_exc()
print(last_error)
print(format_debug_info(get_debug_info()))
print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on")
class BIM_PT_fatal_error(bpy.types.Panel):
bl_label = "BlenderBIM Fatal Error"
bl_idname = "SCENE_PT_error_message"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
def draw(self, context):
info = get_debug_info()
layout = self.layout
layout.alert = True
layout.label(text="BlenderBIM could not load.", icon="ERROR")
if info["os"] == "Windows":
layout.operator("wm.console_toggle", text="View the console for full logs.", icon="CONSOLE")
else:
layout.label(text="View the console for full logs.", icon="CONSOLE")
box = layout.box()
py = ".".join(info["python_version"].split(".")[0:2])
b3d = ".".join(info["blender_version"].split(".")[0:2])
box.label(text=f"Blender {b3d} {info['os']} {info['machine']}", icon="BLENDER")
box.label(text=f"Python {py} BBIM {info['blenderbim_version']}", icon="SCRIPTPLUGINS")
layout.operator("bim.copy_debug_information", text="Copy Error Message To Clipboard")
op = layout.operator("bim.open_uri", text="How Can I Fix This?")
op.uri = "https://docs.blenderbim.org/users/troubleshooting.html#installation-issues"
layout.label(text="Try Reinstalling:", icon="IMPORT")
op = layout.operator("bim.open_uri", text="Re-download Add-on")
bbim_date = info["blenderbim_version"].split(".")[-1]
py_tag = py.replace(".", "")
if "Linux" in info["os"]:
os = "linux"
elif "Darwin" in info["os"]:
if "arm64" in info["machine"]:
os = "macosm1"
else:
os = "macos"
else:
os = "win"
op.uri = f"https://github.com/IfcOpenShell/IfcOpenShell/releases/download/blenderbim-{bbim_date}/blenderbim-{bbim_date}-py{py_tag}-{os}.zip"
class OpenUri(bpy.types.Operator):
bl_idname = "bim.open_uri"
bl_label = "Open URI"
uri: bpy.props.StringProperty()
def execute(self, context):
webbrowser.open(self.uri)
return {"FINISHED"}
class CopyDebugInformation(bpy.types.Operator):
bl_idname = "bim.copy_debug_information"
bl_label = "Copy Debug Information"
bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context):
info = format_debug_info(get_debug_info())
context.window_manager.clipboard = info
return {"FINISHED"}
class HiddenPanel:
@classmethod
def false_poll(cls, context):
return False
def register():
# Only show our error panel and nothing else in the scene tab
for item_name in dir(bpy.types):
item = getattr(bpy.types, item_name)
if not hasattr(item, "bl_rna") or not isinstance(item.bl_rna, bpy.types.Panel):
continue
if getattr(item, "bl_context", None) != "scene":
continue
# Reregister scene panel with a new poll to hide it
item.poll = HiddenPanel.false_poll
bpy.utils.unregister_class(item)
bpy.utils.register_class(item)
bpy.utils.register_class(BIM_PT_fatal_error)
bpy.utils.register_class(CopyDebugInformation)
bpy.utils.register_class(OpenUri)
def unregister():
bpy.utils.unregister_class(OpenUri)
bpy.utils.unregister_class(CopyDebugInformation)
bpy.utils.unregister_class(BIM_PT_fatal_error)
+6 -2
View File
@@ -17,11 +17,11 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import os
from pathlib import Path
import bpy
import bpy.utils.previews
import blenderbim
import importlib
from pathlib import Path
from . import handler, ui, prop, operator, helper
from typing import Callable, Union
@@ -103,6 +103,7 @@ classes = [
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.ClippingPlaneCutWithCappings,
operator.CloseError,
operator.EditBlenderCollection,
operator.FileAssociate,
operator.FileUnassociate,
@@ -141,6 +142,7 @@ classes = [
ui.BIM_PT_tabs,
# Project overview
ui.BIM_PT_tab_project_info,
ui.BIM_PT_tab_spatial_decomposition,
ui.BIM_PT_tab_project_setup,
ui.BIM_PT_tab_geometry,
ui.BIM_PT_tab_stakeholders,
@@ -153,9 +155,10 @@ classes = [
ui.BIM_PT_tab_representations,
ui.BIM_PT_tab_geometric_relationships,
ui.BIM_PT_tab_parametric_geometry,
ui.BIM_PT_tab_profiles,
ui.BIM_PT_tab_object_materials,
ui.BIM_PT_tab_materials,
ui.BIM_PT_tab_styles,
ui.BIM_PT_tab_profiles,
# Drawings and documents
ui.BIM_PT_tab_sheets,
ui.BIM_PT_tab_drawings,
@@ -168,6 +171,7 @@ classes = [
ui.BIM_PT_tab_structural,
# Construction scheduling
ui.BIM_PT_tab_status,
ui.BIM_PT_tab_qto,
ui.BIM_PT_tab_resources,
ui.BIM_PT_tab_cost,
ui.BIM_PT_tab_sequence,
@@ -0,0 +1,45 @@
/*
* BlenderBIM Add-on - OpenBIM Blender Add-on
* Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
*
* This file is part of BlenderBIM Add-on.
*
* BlenderBIM Add-on is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* BlenderBIM Add-on is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* You may copy this `schedule.css` template alongside your input schedule
* document with the same filename. For example if you have a schedule called
* `door_types.ods`, you can create a `door_types.css` in the same folder to
* style that schedule.
*/
/**
* If you specify a font size in CSS, such as text { font-size: 5; }, all fonts
* will be overriden to match that size.
*
* If your CSS, ODS, XLSX does not specify a font size, the variables below
* will specify the default font size.
*
* If your ODS, XLSX does specify a font size, they will scale linearly based
* on the variables below.
*/
:root {
--font-size-pt: 12;
--font-size-px: 4.13;
--font-width: 0.45;
}
text, tspan { /* 2.5mm */ fill: black; stroke: none; font-family: 'OpenGost Type B TT', 'DejaVu Sans Condensed', 'Liberation Sans', 'Arial Narrow', 'Arial'; }
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -5,6 +5,7 @@ FILE_NAME('EQto_BodyGeometryValidation.ifc','2020-01-01T00:00:00',(),(),'EQto_Bo
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
/* This quantity set is Qto_BodyGeometryValidation backport from IFC4X3 to support IFC4. */
#1=IFCPROPERTYSETTEMPLATE('2KS9su6r517uLXTGKwwDdn',$,'EQto_BodyGeometryValidation','Quantities supplied for validating the correct interpretation of the body shape representation at import. In case of multiple representation items, the quantities are summed for each of the items (irrespective of any overlap). Choosing a suitable tolerance value for comparing the supplied numbers to the numbers calculated from the reconstructed geometry is at the discretion of the importing application.',.QTO_OCCURRENCEDRIVEN.,'IfcProduct',(#2,#3,#4,#5,#6,#7));
#2=IFCSIMPLEPROPERTYTEMPLATE('3iCdOOLRjCwQ_HRTB41Xh8',$,'GrossSurfaceArea','Total gross area of the object, normally generated as perimeter * length + 2 * cross section area. It is the sum of OuterSurfaceArea + (2 x CrossSectionArea) and shall only be given, if the OuterSurfaceArea and CrossSectionArea cannot be established separately.\X2\000A000A\X0\Total gross surface area of the element before applying product-level geometric features such as openings and projections.',.Q_AREA.,$,$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('0juemEqF5889vg7HwR87Fv',$,'NetSurfaceArea','Net surface area of the object, normally generated as perimeter * length + 2 * cross section area taking into account possible processing features (cut-out''s, etc.) or openings and recesses.\X2\000A000A\X0\Total net surface area of the element after applying product-level geometric features such as openings and projections.',.Q_AREA.,$,$,$,$,$,$,.READWRITE.);
@@ -5,20 +5,20 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',(),(),'Psets_BBIM_An
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#2,#3,#4));
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separarated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#6,#7));
#5=IFCPROPERTYSETTEMPLATE('0iKwujnQL9IevVQato8f7Z',$,'BBIM_Batting','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/BATTING,IfcTypeProduct',(#6,#7));
#6=IFCSIMPLEPROPERTYTEMPLATE('0t2LEesGT1QRQtrIZUAR8L',$,'Thickness','Batting thickness',.P_SINGLEVALUE.,'IfcPositiveLengthMeasure',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('082PndS6v2kBOiJoSboMnh',$,'Reverse pattern direction','Reverse batting pattern (swap starting and ending points)',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#9,#10,#11,#12,#13));
#8=IFCPROPERTYSETTEMPLATE('1Dx2EiZnP67xotInXpwz90',$,'BBIM_Section','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/SECTION,IfcTypeProduct',(#9,#10,#11,#12,#13));
#9=IFCSIMPLEPROPERTYTEMPLATE('2a_9s8spHDc9dZHtgg71XL',$,'ShowStartArrow','Display start arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#10=IFCSIMPLEPROPERTYTEMPLATE('3i6SH_GbT7zhKea$wVE56A',$,'StartArrowSymbol','Custom symbol for the start of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#11=IFCSIMPLEPROPERTYTEMPLATE('1DtsPn5a9FG8$zXHDDMavY',$,'ShowEndArrow','Display end arrow.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#12=IFCSIMPLEPROPERTYTEMPLATE('2$6U0mLI9AiPRWeBabdY3u',$,'EndArrowSymbol','Custom symbol for the end of the section marker arrow. Need to make sure it''s present in "symbols.svg".',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#13=IFCSIMPLEPROPERTYTEMPLATE('1naFqntIL7igCY7hCaE7kq',$,'HasConnectedSectionLine','Connect or disconnect section markers with line (by default = True).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#14=IFCPROPERTYSETTEMPLATE('3V8oZ8YRD3_O7uR5vcUleS',$,'BBIM_Documentation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcProject',(#15,#16,#17,#18,#19,#20,#21,#22,#23));
#15=IFCSIMPLEPROPERTYTEMPLATE('0ulAhgk3v9qfGlDILauR6J',$,'SheetsDir','Default sheets directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#16=IFCSIMPLEPROPERTYTEMPLATE('2yvlVKiQXASucfH40deCvu',$,'LayoutsDir','Default layouts directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#17=IFCSIMPLEPROPERTYTEMPLATE('2kXZqXicL3jRwsOnLM_0ho',$,'TitleblocksDir','Default titleblocks directory',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -28,7 +28,7 @@ DATA;
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation,IfcTypeProduct',(#25,#26,#27,#28));
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28));
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
Binary file not shown.
+9 -6
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import bpy
import json
@@ -36,6 +37,7 @@ import blenderbim.core.style
from blenderbim.bim.ifc import IfcStore
from mathutils import Vector
from typing import Union
from logging import Logger
class IfcExporter:
@@ -97,7 +99,6 @@ class IfcExporter:
continue
if obj.library:
continue
tool.Collector.sync(obj, skip_unlinking)
result = self.sync_object_placement(obj)
if result:
results.append(result)
@@ -163,10 +164,10 @@ class IfcExporter:
bpy.ops.bim.update_representation(obj=obj.name)
tool.Geometry.record_object_position(obj)
def get_application_name(self):
def get_application_name(self) -> str:
return "BlenderBIM"
def get_application_version(self):
def get_application_version(self) -> str:
version = ".".join(
[
str(x)
@@ -184,11 +185,13 @@ class IfcExporter:
class IfcExportSettings:
def __init__(self):
self.logger = None
self.output_file = None
self.logger: Logger = None
self.output_file: str = None
self.json_version: str = None
self.json_compact: bool = None
@staticmethod
def factory(context, output_file, logger):
def factory(context: bpy.types.Context, output_file: str, logger: Logger) -> IfcExportSettings:
settings = IfcExportSettings()
settings.output_file = output_file
settings.logger = logger
+18 -4
View File
@@ -223,7 +223,7 @@ def redo_post(scene):
tool.Ifc.rebuild_element_maps()
def get_application(ifc):
def get_application(ifc: ifcopenshell.file) -> ifcopenshell.entity_instance:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
version = get_application_version()
for element in ifc.by_type("IfcApplication"):
@@ -238,7 +238,20 @@ def get_application(ifc):
)
def get_application_version():
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
# TODO: cache this for even faster application retrieval. It honestly makes a difference on long scripts.
if pao := next(iter(ifc.by_type("IfcPersonAndOrganization")), None):
return pao
elif ifc.schema == "IFC2X3":
if (person := next(iter(ifc.by_type("IfcPerson")), None)) is None:
person = tool.Ifc.run("owner.add_person")
if (organization := next(iter(ifc.by_type("IfcOrganization")), None)) is None:
organization = tool.Ifc.run("owner.add_organisation")
pao = tool.Ifc.run("owner.add_person_and_organisation", person=person, organisation=organization)
return pao
def get_application_version() -> str:
return ".".join(
[
str(x)
@@ -279,7 +292,7 @@ def load_post(scene):
key=key, owner=global_subscription_owner, args=(area,), notify=viewport_shading_changed_callback
)
ifcopenshell.api.owner.settings.get_user = lambda ifc: core_owner.get_user(tool.Owner)
ifcopenshell.api.owner.settings.get_user = get_user
ifcopenshell.api.owner.settings.get_application = get_application
AuthoringData.type_thumbnails = {}
@@ -288,7 +301,8 @@ def load_post(scene):
if bpy.context.preferences.addons["blenderbim"].preferences.should_setup_workspace:
if "BIM" in bpy.data.workspaces:
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
if bpy.context.preferences.addons["blenderbim"].preferences.activate_workspace:
bpy.context.window.workspace = bpy.data.workspaces["BIM"]
else:
bpy.ops.workspace.append_activate(idname="BIM", filepath=os.path.join(cwd, "data", "workspace.blend"))
+9 -11
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}
@@ -221,15 +225,6 @@ def get_enum_items(data, prop_name, context=None):
return items
# hack to close popup
# https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event):
x, y = event.mouse_x, event.mouse_y
bpy.context.window.cursor_warp(10, 10)
move_back = lambda: bpy.context.window.cursor_warp(x, y)
bpy.app.timers.register(move_back, first_interval=0.01)
def convert_property_group_from_si(property_group, skip_props=()):
"""Method converts property group values from si to current ifc project units
@@ -309,6 +304,9 @@ def draw_filter(layout, filter_groups, data, module):
elif ifc_filter.type == "group":
row = box.row(align=True)
row.prop(ifc_filter, "value", text="", icon="OUTLINER_COLLECTION")
elif ifc_filter.type == "parent":
row = box.row(align=True)
row.prop(ifc_filter, "value", text="", icon="FILE_PARENT")
elif ifc_filter.type == "query":
row = box.row(align=True)
row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA")
+27 -14
View File
@@ -19,12 +19,15 @@
import os
import bpy
import uuid
import shutil
import hashlib
import zipfile
import tempfile
import traceback
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper
import blenderbim
import blenderbim.bim.handler
import blenderbim.tool as tool
from pathlib import Path
@@ -37,19 +40,19 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
class IfcStore:
path: str = ""
file: ifcopenshell.file = None
schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None
cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None
cache_path: str = None
file: Optional[ifcopenshell.file] = None
schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None
cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None
cache_path: Optional[str] = None
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
edited_objs: Set[bpy.types.Object] = set()
pset_template_path: str = ""
pset_template_file: ifcopenshell.file = None
pset_template_file: Optional[ifcopenshell.file] = None
classification_path: str = ""
classification_file: ifcopenshell.file = None
classification_file: Optional[ifcopenshell.file] = None
library_path: str = ""
library_file: ifcopenshell.file = None
library_file: Optional[ifcopenshell.file] = None
current_transaction = ""
last_transaction = ""
history = []
@@ -97,7 +100,6 @@ class IfcStore:
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
cache_settings = ifcopenshell.geom.settings()
cache_settings.set(cache_settings.STRICT_TOLERANCE, True)
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
except:
@@ -119,7 +121,13 @@ class IfcStore:
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
IfcStore.cache = None
os.replace(IfcStore.cache_path, new_cache_path)
try:
shutil.move(IfcStore.cache_path, new_cache_path)
except PermissionError:
try:
shutil.copy2(IfcStore.cache_path, new_cache_path)
except PermissionError:
pass # Well we tried. No cache for you!
IfcStore.get_cache()
@staticmethod
@@ -213,7 +221,7 @@ class IfcStore:
elif existing_obj:
try:
existing_obj.name
IfcStore.unlink_element(obj=existing_obj)
IfcStore.unlink_element(element=element, obj=existing_obj)
except:
pass
IfcStore.id_map[element.id()] = obj
@@ -329,6 +337,7 @@ class IfcStore:
@staticmethod
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False):
blenderbim.last_actions.append({"type": "operator", "name": operator.bl_idname})
bpy.context.scene.BIMProperties.is_dirty = True
is_top_level_operator = not bool(IfcStore.current_transaction)
@@ -343,10 +352,14 @@ class IfcStore:
else:
operator.transaction_key = IfcStore.current_transaction
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
try:
if is_invoke:
result = getattr(operator, "_invoke")(context, None)
else:
result = getattr(operator, "_execute")(context)
except:
blenderbim.last_error = traceback.format_exc()
raise
if is_top_level_operator:
if tool.Ifc.get():
+161 -329
View File
@@ -26,6 +26,7 @@ import bmesh
import logging
import mathutils
import numpy as np
import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
@@ -34,7 +35,9 @@ import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import blenderbim.tool as tool
import blenderbim.core.spatial
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
from itertools import chain, accumulate
from blenderbim.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
@@ -215,7 +218,6 @@ class IfcImporter:
self.gross_elements: set[ifcopenshell.entity_instance] = set()
self.element_types: set[ifcopenshell.entity_instance] = set()
self.spatial_elements: set[ifcopenshell.entity_instance] = set()
self.type_collection: bpy.types.Collection = None
self.type_products = {}
self.meshes = {}
self.mesh_shapes = {}
@@ -229,19 +231,19 @@ class IfcImporter:
self.material_creator = MaterialCreator(ifc_import_settings, self)
def profile_code(self, message):
def profile_code(self, message: str) -> None:
if not self.time:
self.time = time.time()
print("{} :: {:.2f}".format(message, time.time() - self.time))
self.time = time.time()
self.update_progress(self.progress + 1)
def update_progress(self, progress):
def update_progress(self, progress: float) -> None:
if progress <= 100:
self.progress = progress
bpy.context.window_manager.progress_update(self.progress)
def execute(self):
def execute(self) -> None:
bpy.context.window_manager.progress_begin(0, 100)
self.profile_code("Starting import process")
self.load_file()
@@ -260,8 +262,6 @@ class IfcImporter:
self.profile_code("Create project")
self.process_element_filter()
self.profile_code("Process element filter")
self.create_collections()
self.profile_code("Create collections")
self.create_materials()
self.profile_code("Create materials")
self.create_styles()
@@ -298,28 +298,32 @@ class IfcImporter:
if self.ifc_import_settings.should_setup_viewport_camera:
self.setup_viewport_camera()
self.setup_arrays()
self.profile_code("Setup arrays")
tool.Spatial.run_spatial_import_spatial_decomposition()
if default_container := tool.Spatial.guess_default_container():
tool.Spatial.set_default_container(default_container)
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):
def process_context_filter(self) -> None:
# Annotation ContextType is to accommodate broken Revit files
# See https://github.com/Autodesk/revit-ifc/issues/187
type_priority = ["Model", "Plan", "Annotation"]
@@ -391,23 +395,21 @@ class IfcImporter:
for context in self.contexts:
settings = ifcopenshell.geom.settings()
settings.set('mesher-linear-deflection', self.ifc_import_settings.deflection_tolerance)
settings.set('mesher-angular-deflection', self.ifc_import_settings.angular_tolerance)
# settings.set(settings.STRICT_TOLERANCE, True)
settings.set('dimensionality', ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
settings.set('context-ids', [context.id()])
settings.set("mesher-linear-deflection", self.ifc_import_settings.deflection_tolerance)
settings.set("mesher-angular-deflection", self.ifc_import_settings.angular_tolerance)
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
settings.set("context-ids", [context.id()])
self.context_settings.append(settings)
settings = ifcopenshell.geom.settings()
settings.set('mesher-linear-deflection', self.ifc_import_settings.deflection_tolerance)
settings.set('mesher-angular-deflection', self.ifc_import_settings.angular_tolerance)
# settings.set(settings.STRICT_TOLERANCE, True)
settings.set('dimensionality', ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
settings.set('disable-opening-subtractions', True)
settings.set('context-ids', [context.id()])
settings.set("mesher-linear-deflection", self.ifc_import_settings.deflection_tolerance)
settings.set("mesher-angular-deflection", self.ifc_import_settings.angular_tolerance)
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
settings.set("disable-opening-subtractions", True)
settings.set("context-ids", [context.id()])
self.gross_context_settings.append(settings)
def process_element_filter(self):
def process_element_filter(self) -> None:
offset = self.ifc_import_settings.element_offset
offset_limit = offset + self.ifc_import_settings.element_limit
@@ -474,7 +476,7 @@ class IfcImporter:
break
return results
def parse_native_elements(self):
def parse_native_elements(self) -> None:
if not self.ifc_import_settings.should_load_geometry:
return
for element in self.elements:
@@ -482,13 +484,13 @@ class IfcImporter:
self.native_elements.add(element)
self.elements -= self.native_elements
def is_native(self, element):
def is_native(self, element: ifcopenshell.entity_instance) -> bool:
if (
not element.Representation
or not element.Representation.Representations
or getattr(element, "HasOpenings", None)
):
return
return False
representation = None
representation_priority = None
@@ -503,7 +505,7 @@ class IfcImporter:
context = rep.ContextOfItems
if not representation:
return
return False
matrix = np.eye(4)
representation_id = None
@@ -561,8 +563,11 @@ class IfcImporter:
"type": "IfcFaceBasedSurfaceModel",
}
return True
return False
def is_native_swept_disk_solid(self, element, representation):
def is_native_swept_disk_solid(
self, element: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> bool:
items = [i["item"] for i in ifcopenshell.util.representation.resolve_items(representation)]
if len(items) == 1 and items[0].is_a("IfcSweptDiskSolid"):
if tool.Blender.Modifier.is_railing(element):
@@ -578,20 +583,20 @@ class IfcImporter:
return True
return False
def is_native_faceted_brep(self, representation):
def is_native_faceted_brep(self, representation: ifcopenshell.entity_instance) -> bool:
# TODO handle mapped items
for i in representation.Items:
if i.is_a() != "IfcFacetedBrep":
return False
return True
def is_native_face_based_surface_model(self, representation):
def is_native_face_based_surface_model(self, representation: ifcopenshell.entity_instance) -> bool:
for i in representation.Items:
if i.is_a() != "IfcFaceBasedSurfaceModel":
return False
return True
def get_products_from_shape_representation(self, element):
def get_products_from_shape_representation(self, element: ifcopenshell.entity_instance) -> None:
products = [pr.ShapeOfProduct[0] for pr in element.OfProductRepresentation]
for rep_map in element.RepresentationMap:
for usage in rep_map.MapUsage:
@@ -600,10 +605,15 @@ class IfcImporter:
products.extend(self.get_products_from_shape_representation(inverse_element))
return products
def predict_dense_mesh(self):
def predict_dense_mesh(self) -> None:
if self.ifc_import_settings.should_use_native_meshes:
return
threshold = 10000 # Just from experience.
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
# The check for CfsFaces/Faces/CoordIndex accommodates invalid data from Cadwork
# 0 IfcClosedShell.CfsFaces
faces = [len(faces) for e in self.file.by_type("IfcClosedShell") if (faces := e[0])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
@@ -611,16 +621,18 @@ 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
def calculate_model_offset(self):
def calculate_model_offset(self) -> None:
props = bpy.context.scene.BIMGeoreferenceProperties
if props.has_blender_offset:
return
@@ -638,14 +650,14 @@ class IfcImporter:
return self.guess_false_origin_and_project_north(building)
return self.guess_false_origin()
def set_manual_blender_offset(self):
def set_manual_blender_offset(self) -> None:
props = bpy.context.scene.BIMGeoreferenceProperties
props.blender_eastings = str(self.ifc_import_settings.false_origin[0])
props.blender_northings = str(self.ifc_import_settings.false_origin[1])
props.blender_orthogonal_height = str(self.ifc_import_settings.false_origin[2])
props.has_blender_offset = True
def guess_false_origin_and_project_north(self, element):
def guess_false_origin_and_project_north(self, element: ifcopenshell.entity_instance) -> None:
if not element.ObjectPlacement or not element.ObjectPlacement.is_a("IfcLocalPlacement"):
return
placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
@@ -660,7 +672,7 @@ class IfcImporter:
props.blender_x_axis_ordinate = str(placement[1][0])
props.has_blender_offset = True
def guess_false_origin(self):
def guess_false_origin(self) -> None:
# Civil BIM applications like to work in absolute coordinates, where the
# ObjectPlacement is usually 0,0,0 (but not always, so we'll need to
# check for the actual transformation) but each individual coordinate of
@@ -674,10 +686,10 @@ 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
element_checking_threshold = 3
for element in self.file.by_type("IfcElement"):
if not element.Representation:
continue
@@ -693,18 +705,20 @@ class IfcImporter:
shape = self.create_generic_shape(element)
if not shape:
continue
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
point = np.array(
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
point = mat @ np.array(
(
shape.geometry.verts[0] / self.unit_scale,
shape.geometry.verts[1] / self.unit_scale,
shape.geometry.verts[2] / self.unit_scale,
shape.geometry.verts[0],
shape.geometry.verts[1],
shape.geometry.verts[2],
0.0,
)
)
return mat @ point
point = point / self.unit_scale
if self.is_point_far_away(point, is_meters=False):
return point
def does_element_likely_have_geometry_far_away(self, element):
def does_element_likely_have_geometry_far_away(self, element: ifcopenshell.entity_instance) -> bool:
for representation in element.Representation.Representations:
items = []
for item in representation.Items:
@@ -721,26 +735,47 @@ 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):
if (
not obj.data
and tool.Cad.is_x(matrix[0][3], 0)
and tool.Cad.is_x(matrix[1][3], 0)
and tool.Cad.is_x(matrix[2][3], 0)
):
# We assume any non-geometric matrix at 0,0,0 is not
# positionally significant and is left alone. This handles
# scenarios where often spatial elements are left at 0,0,0 and
# everything else is at map coordinates.
obj.BIMObjectProperties.blender_offset_type = "NOT_APPLICABLE"
return mathutils.Matrix(matrix.tolist())
elif 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])):
if cartesian_point_offset := obj.data.get("cartesian_point_offset", None):
obj.BIMObjectProperties.cartesian_point_offset = cartesian_point_offset
offset_x, offset_y, offset_z = map(float, cartesian_point_offset.split(","))
matrix[0][3] += offset_x
matrix[1][3] += offset_y
matrix[2][3] += offset_z
else:
obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
float(props.blender_eastings) * self.unit_scale,
float(props.blender_northings) * self.unit_scale,
float(props.blender_orthogonal_height) * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
)
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
float(props.blender_eastings) * self.unit_scale,
float(props.blender_northings) * self.unit_scale,
float(props.blender_orthogonal_height) * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
)
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
@@ -763,22 +798,16 @@ class IfcImporter:
if grid.Representation:
shape = self.create_generic_shape(grid)
grid_obj = self.create_product(grid, shape)
grid_placement = self.get_element_matrix(grid)
if bpy.context.preferences.addons["blenderbim"].preferences.lock_grids_on_import:
grid_obj.lock_location = (True, True, True)
grid_obj.lock_rotation = (True, True, True)
collection = bpy.data.collections.new(tool.Loader.get_name(grid))
u_axes = bpy.data.collections.new("UAxes")
collection.children.link(u_axes)
v_axes = bpy.data.collections.new("VAxes")
collection.children.link(v_axes)
self.create_grid_axes(grid.UAxes, u_axes, grid_obj)
self.create_grid_axes(grid.VAxes, v_axes, grid_obj)
self.create_grid_axes(grid.UAxes, grid_obj, grid_placement)
self.create_grid_axes(grid.VAxes, grid_obj, grid_placement)
if grid.WAxes:
w_axes = bpy.data.collections.new("WAxes")
collection.children.link(w_axes)
self.create_grid_axes(grid.WAxes, w_axes, grid_obj)
self.create_grid_axes(grid.WAxes, grid_obj)
def create_grid_axes(self, axes, grid_collection, grid_obj):
def create_grid_axes(self, axes, grid_obj, grid_placement):
for axis in axes:
shape = self.create_generic_shape(axis.AxisCurve)
mesh = self.create_mesh(axis, shape)
@@ -787,8 +816,7 @@ class IfcImporter:
obj.lock_location = (True, True, True)
obj.lock_rotation = (True, True, True)
self.link_element(axis, obj)
self.set_matrix_world(obj, grid_obj.matrix_world)
grid_collection.objects.link(obj)
self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, grid_placement.copy()))
def create_element_types(self):
for element_type in self.element_types:
@@ -868,7 +896,7 @@ class IfcImporter:
def create_generic_shape(
self, element: ifcopenshell.entity_instance
) -> Union[ifcopenshell_wrapper.TriangulationElement, None]:
) -> Union[ifcopenshell.geom.ShapeElementType, None]:
for settings in self.context_settings:
try:
result = ifcopenshell.geom.create_shape(settings, element)
@@ -1010,14 +1038,6 @@ class IfcImporter:
self.profile_code("Merging by material")
def create_structural_items(self):
# Create structural collections
self.structural_member_collection = bpy.data.collections.new("Members")
self.structural_connection_collection = bpy.data.collections.new("Connections")
self.structural_collection = bpy.data.collections.new("StructuralItems")
self.structural_collection.children.link(self.structural_member_collection)
self.structural_collection.children.link(self.structural_connection_collection)
self.project["blender"].children.link(self.structural_collection)
self.create_generic_elements(set(self.file.by_type("IfcStructuralCurveMember")))
self.create_generic_elements(set(self.file.by_type("IfcStructuralCurveConnection")))
self.create_generic_elements(set(self.file.by_type("IfcStructuralSurfaceMember")))
@@ -1178,7 +1198,6 @@ class IfcImporter:
styles.extend(style.Styles)
def create_native_faceted_brep(self, element, mesh_name, native_data):
# TODO: georeferencing?
# co [x y z x y z x y z ...]
# vertex_index [i i i i i ...]
# loop_start [0 3 6 9 ...] (for tris)
@@ -1203,45 +1222,27 @@ class IfcImporter:
mesh = bpy.data.meshes.new("Native")
props = bpy.context.scene.BIMGeoreferenceProperties
mat = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
if props.has_blender_offset and self.is_point_far_away(self.mesh_data["co"][0:3], is_meters=False):
offset_point = np.linalg.inv(mat) @ np.array(
(
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
0.0,
)
)
verts = [None] * len(self.mesh_data["co"])
for i in range(0, len(self.mesh_data["co"]), 3):
verts[i], verts[i + 1], verts[i + 2], _ = native_data["matrix"] @ mathutils.Vector(
(
*ifcopenshell.util.geolocation.enh2xyz(
self.mesh_data["co"][i] * self.unit_scale,
self.mesh_data["co"][i + 1] * self.unit_scale,
self.mesh_data["co"][i + 2] * self.unit_scale,
offset_point[0] * self.unit_scale,
offset_point[1] * self.unit_scale,
offset_point[2] * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
),
1,
)
)
verts_array = np.array(self.mesh_data["co"])
verts_array *= self.unit_scale
offset_x, offset_y, offset_z = verts_array[0:3]
offset = np.array([-offset_x, -offset_y, -offset_z])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
if np.allclose(native_data["matrix"], np.identity(4), atol=1e-8):
verts = offset_verts.tolist()
else:
verts = self.apply_matrix_to_flat_coords(offset_verts, native_data["matrix"])
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = f"{offset_x},{offset_y},{offset_z}"
else:
verts = [None] * len(self.mesh_data["co"])
for i in range(0, len(self.mesh_data["co"]), 3):
verts[i], verts[i + 1], verts[i + 2], _ = native_data["matrix"] @ mathutils.Vector(
(
self.mesh_data["co"][i] * self.unit_scale,
self.mesh_data["co"][i + 1] * self.unit_scale,
self.mesh_data["co"][i + 2] * self.unit_scale,
1,
)
)
verts_array = np.array(self.mesh_data["co"])
verts_array *= self.unit_scale
if np.allclose(native_data["matrix"], np.identity(4), atol=1e-8):
verts = verts_array.tolist()
else:
verts = self.apply_matrix_to_flat_coords(verts_array, native_data["matrix"])
mesh["has_cartesian_point_offset"] = False
mesh.vertices.add(self.mesh_data["total_verts"])
@@ -1258,6 +1259,13 @@ class IfcImporter:
mesh["ios_material_ids"] = self.mesh_data["material_ids"]
return mesh
def apply_matrix_to_flat_coords(self, coords, matrix):
coords_array = np.array(coords).reshape(-1, 3)
ones = np.ones((coords_array.shape[0], 1))
homogeneous_coords = np.hstack([coords_array, ones])
transformed_coords = homogeneous_coords @ matrix.T
return transformed_coords[:, :3].flatten().tolist()
def convert_representation_item_face_based_surface_model(self, item):
mesh = item.get_info_2(recursive=True)
for face_set in mesh["FbsmFaces"]:
@@ -1487,7 +1495,10 @@ class IfcImporter:
# Occurs when reloading a project
pass
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
project_collection.children[self.type_collection.name].hide_viewport = True
if types_collection := project_collection.children.get("IfcTypeProduct"):
types_collection.hide_viewport = False
for obj in types_collection.collection.objects: # turn off all objects inside Types collection.
obj.hide_set(True)
def clean_mesh(self):
obj = None
@@ -1501,18 +1512,19 @@ class IfcImporter:
if not last_obj:
return
# temporarily unhide types collection to make sure all objects will be cleaned
# Temporarily unhide types collection to make sure all objects will be cleaned
project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name]
types_collection = project_collection.children[self.type_collection.name]
types_collection.hide_viewport = False
bpy.context.view_layer.objects.active = last_obj
if types_collection := project_collection.children.get("IfcTypeProduct"):
types_collection.hide_viewport = False
bpy.context.view_layer.objects.active = last_obj
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.tris_convert_to_quads()
bpy.ops.mesh.normals_make_consistent()
bpy.ops.object.editmode_toggle()
types_collection.hide_viewport = True
if types_collection:
types_collection.hide_viewport = True
bpy.context.view_layer.objects.active = last_obj
IfcStore.edited_objs.clear()
@@ -1577,120 +1589,18 @@ class IfcImporter:
self.project["blender"].BIMCollectionProperties.obj = obj
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
def create_collections(self) -> None:
self.create_spatial_decomposition_collections()
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.create_aggregate_and_nest_collections()
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
pass
def create_spatial_decomposition_collections(self) -> None:
for rel_aggregate in self.project["ifc"].IsDecomposedBy or []:
self.create_spatial_decomposition_collection(self.project["blender"], rel_aggregate.RelatedObjects)
# Invalid IFCs may have orphaned spatial structure elements.
orphaned_spaces = [e for e in self.spatial_elements if e.GlobalId not in self.collections]
while orphaned_spaces:
self.create_spatial_decomposition_collection(self.project["blender"], orphaned_spaces)
orphaned_spaces = [e for e in self.spatial_elements if e.GlobalId not in self.collections]
tool.Loader.create_project_collection("Views")
self.type_collection = tool.Loader.create_project_collection("Types")
def create_spatial_decomposition_collection(
self, parent: Union[bpy.types.Collection, bpy.types.Object], related_objects: list[ifcopenshell.entity_instance]
) -> None:
for element in related_objects:
if element not in self.spatial_elements:
continue
is_existing = False
if self.has_existing_project:
obj = tool.Ifc.get_object(element)
if obj:
is_existing = True
collection = obj.BIMObjectProperties.collection
self.collections[element.GlobalId] = collection
if not is_existing:
collection = bpy.data.collections.new(tool.Loader.get_name(element))
self.collections[element.GlobalId] = collection
parent.children.link(collection)
if element.IsDecomposedBy:
for rel_aggregate in element.IsDecomposedBy:
self.create_spatial_decomposition_collection(collection, rel_aggregate.RelatedObjects)
def create_aggregate_and_nest_collections(self):
if self.ifc_import_settings.has_filter:
rel_aggregates = set()
for element in self.elements:
if decomposed_by := element.IsDecomposedBy:
rel_aggregates.add(decomposed_by[0])
elif decomposes := element.Decomposes:
rel_aggregates.add(decomposes[0])
elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None):
rel_aggregates.add(nested_by[0])
elif nests := getattr(element, "Nests", []):
rel_aggregates.add(nests[0])
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
rel_aggregates.add(element.AdheresToElement[0])
else:
rel_aggregates = [
r
for r in self.file.by_type("IfcRelAggregates")
if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType")
] + [
r
for r in self.file.by_type("IfcRelNests")
if (
(relating_obj := r.RelatingObject).is_a("IfcElement")
or relating_obj.is_a("IfcElementType")
or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid"))
)
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
]
if self.file.schema == "IFC4X3":
rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")]
if len(rel_aggregates) > 10000:
# More than 10,000 collections makes Blender unhappy
print("Skipping aggregate collections for performance.")
self.ifc_import_settings.collection_mode = "SPATIAL_DECOMPOSITION"
return
aggregates: dict[str, dict] = {}
for rel_aggregate in rel_aggregates:
element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr(
rel_aggregate, "RelatingElement"
)
collection = bpy.data.collections.new(tool.Loader.get_name(element))
aggregates[element.GlobalId] = {"element": element, "collection": collection}
self.collections[element.GlobalId] = collection
for global_id, aggregate in aggregates.items():
parent = ifcopenshell.util.element.get_aggregate(aggregate["element"])
if parent:
self.collections[parent.GlobalId].children.link(aggregate["collection"])
continue
parent = ifcopenshell.util.element.get_container(aggregate["element"])
if parent:
self.collections[parent.GlobalId].children.link(aggregate["collection"])
continue
if aggregate["element"].is_a("IfcElementType"):
self.type_collection.children.link(aggregate["collection"])
continue
def create_materials(self):
def create_materials(self) -> None:
for material in self.file.by_type("IfcMaterial"):
self.create_material(material)
def create_material(self, material):
def create_material(self, material: ifcopenshell.entity_instance) -> bpy.types.Material:
blender_material = bpy.data.materials.new(material.Name)
self.link_element(material, blender_material)
self.material_creator.materials[material.id()] = blender_material
blender_material.use_fake_user = True
return blender_material
def create_styles(self):
def create_styles(self) -> None:
parsed_styles = set()
for material_definition_representation in self.file.by_type("IfcMaterialDefinitionRepresentation"):
@@ -1735,75 +1645,7 @@ class IfcImporter:
def place_objects_in_collections(self) -> None:
for ifc_definition_id, obj in self.added_data.items():
if isinstance(obj, bpy.types.Object):
self.place_object_in_collection(self.file.by_id(ifc_definition_id), obj)
def place_object_in_collection(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.place_object_in_decomposition_collection(element, obj)
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
self.place_object_in_spatial_decomposition_collection(element, obj)
def place_object_in_decomposition_collection(
self, element: ifcopenshell.entity_instance, obj: bpy.types.Object
) -> None:
if element.is_a("IfcProject"):
return
elif element.is_a("IfcGridAxis"):
return
elif element.GlobalId in self.collections:
collection = self.collections[element.GlobalId]
collection.BIMCollectionProperties.obj = obj
obj.BIMObjectProperties.collection = collection
collection.name = obj.name
return collection.objects.link(obj)
elif getattr(element, "Decomposes", None):
aggregate = ifcopenshell.util.element.get_aggregate(element)
return self.collections[aggregate.GlobalId].objects.link(obj)
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
nest = ifcopenshell.util.element.get_nest(element)
return self.collections[nest.GlobalId].objects.link(obj)
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
adherend = element.AdheresToElement[0].RelatingElement
return self.collections[adherend.GlobalId].objects.link(obj)
return self.place_object_in_spatial_decomposition_collection(element, obj)
def place_object_in_spatial_decomposition_collection(
self, element: ifcopenshell.entity_instance, obj: bpy.types.Object
) -> None:
if element.is_a("IfcProject"):
return
elif element.is_a("IfcGridAxis"):
return
elif element.GlobalId in self.collections:
collection = self.collections[element.GlobalId]
collection.BIMCollectionProperties.obj = obj
obj.BIMObjectProperties.collection = collection
return collection.objects.link(obj)
elif element.is_a("IfcTypeObject"):
return self.type_collection.objects.link(obj)
elif element.is_a("IfcStructuralMember"):
return self.structural_member_collection.objects.link(obj)
elif element.is_a("IfcStructuralConnection"):
return self.structural_connection_collection.objects.link(obj)
elif element.is_a("IfcAnnotation"):
group = self.get_drawing_group(element)
if group:
return self.collections[group.GlobalId].objects.link(obj)
container = ifcopenshell.util.element.get_container(element)
if container:
if element.is_a("IfcGrid"): # TODO: refactor into a more holistic collection mode feature
grid_collection = bpy.data.collections.get(obj.name)
if grid_collection: # Just in case we run into invalid grids from Revit
self.collections[container.GlobalId].children.link(grid_collection)
grid_collection.objects.link(obj)
else:
self.collections[container.GlobalId].objects.link(obj)
else:
self.ifc_import_settings.logger.warning("Warning: this object is outside the spatial hierarchy %s", element)
bpy.context.scene.collection.objects.link(obj)
tool.Collector.assign(obj)
def is_curve_annotation(self, element):
object_type = element.ObjectType
@@ -1881,13 +1723,17 @@ class IfcImporter:
polyline.points[-1].co = mathutils.Vector(v2)
return curve
def create_mesh(self, element: ifcopenshell.entity_instance, shape) -> bpy.types.Mesh:
def create_mesh(
self,
element: ifcopenshell.entity_instance,
shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType],
) -> bpy.types.Mesh:
try:
if hasattr(shape, "geometry"):
# shape is ifcopenshell_wrapper.TriangulationElement
geometry: ifcopenshell_wrapper.Triangulation = shape.geometry
# shape is ShapeElementType
geometry = shape.geometry
else:
geometry: ifcopenshell_wrapper.Triangulation = shape
geometry = shape
mesh = bpy.data.meshes.new(tool.Loader.get_mesh_name(geometry))
@@ -1897,28 +1743,14 @@ class IfcImporter:
and geometry.verts
and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2]))
):
mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F")
offset_point = np.linalg.inv(mat) @ np.array(
(
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
0.0,
)
)
verts = [None] * len(geometry.verts)
for i in range(0, len(geometry.verts), 3):
verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz(
geometry.verts[i],
geometry.verts[i + 1],
geometry.verts[i + 2],
offset_point[0] * self.unit_scale,
offset_point[1] * self.unit_scale,
offset_point[2] * self.unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
)
# Shift geometry close to the origin based off that first vert it found
verts_array = np.array(geometry.verts)
offset = np.array([-geometry.verts[0], -geometry.verts[1], -geometry.verts[2]])
offset_verts = verts_array + np.tile(offset, len(verts_array) // 3)
verts = offset_verts.tolist()
mesh["has_cartesian_point_offset"] = True
mesh["cartesian_point_offset"] = f"{geometry.verts[0]},{geometry.verts[1]},{geometry.verts[2]}"
else:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
@@ -1933,7 +1765,9 @@ class IfcImporter:
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry)))
#
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
@@ -2029,7 +1863,7 @@ class IfcImporter:
class IfcImportSettings:
def __init__(self):
self.logger = None
self.logger: logging.Logger = None
self.input_file = None
self.diff_file = None
self.should_use_cpu_multiprocessing = True
@@ -2051,7 +1885,6 @@ class IfcImportSettings:
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.elements: set[ifcopenshell.entity_instance] = set()
self.collection_mode = "DECOMPOSITION"
@staticmethod
def factory(context=None, input_file=None, logger=None):
@@ -2063,7 +1896,6 @@ class IfcImportSettings:
logger = logging.getLogger("ImportIFC")
settings.logger = logger
settings.diff_file = scene_diff.diff_json_file
settings.collection_mode = props.collection_mode
settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing
settings.merge_mode = props.merge_mode
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
@@ -36,7 +36,6 @@ class AggregateData:
"relating_object_label": cls.get_relating_object_label(),
"has_related_objects": cls.has_related_objects(),
"total_parts": cls.total_parts(),
"ifc_class": cls.ifc_class(),
"total_linked_aggregate": cls.total_linked_aggregate(),
}
cls.is_loaded = True
@@ -68,12 +67,6 @@ class AggregateData:
def has_related_objects(cls) -> bool:
return bool(cls.get_related_objects())
@classmethod
def ifc_class(cls) -> str:
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
return element.is_a()
@classmethod
def total_linked_aggregate(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -40,36 +40,39 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, Operator):
bl_label = "Assign Object To Aggregation"
bl_options = {"REGISTER", "UNDO"}
relating_object: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
def _execute(self, context):
relating_obj = None
if self.relating_object:
element = tool.Ifc.get().by_id(self.relating_object)
if element.IsDecomposedBy:
relating_obj = tool.Ifc.get_object(element)
else:
assembly = element.Decomposes[0].RelatingObject
relating_obj = tool.Ifc.get_object(assembly)
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
elif self.related_object:
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get().by_id(self.related_object))
if aggregate:
relating_obj = tool.Ifc.get_object(aggregate)
elif context.active_object:
relating_obj = context.active_object
if not relating_obj:
return
for obj in bpy.context.selected_objects:
for obj in tool.Blender.get_selected_objects():
if obj == relating_obj:
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
result = core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=relating_obj,
related_obj=obj,
)
if not result:
self.report({"ERROR"}, f" Cannot aggregate {obj.name} to {relating_obj.name}")
try:
core.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=relating_obj,
related_obj=obj,
)
except core.IncompatibleAggregateError:
self.report({"ERROR"}, f"Cannot aggregate {obj.name} to {relating_obj.name}")
except core.AggregateRepresentationError:
self.report({"ERROR"}, f"Cannot aggregate to {relating_obj.name} with a body representation")
class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
@@ -96,12 +99,13 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
)
# Removes Pset related to Linked Aggregates
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
if not element.is_a('IfcElementAssembly'):
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
"""Enable editing aggregation relationship"""
@@ -132,6 +136,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -139,20 +144,21 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
row = self.layout
row.prop(self, "ifc_class")
row = self.layout
row.prop(self, "aggregate_name")
def _execute(self, context):
try:
ifc_class = tool.Ifc.schema().declaration_by_name(self.ifc_class).name()
except:
return
aggregate = self.create_aggregate(context, ifc_class)
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
tool.Collector.sync(obj)
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
current_container = ifcopenshell.util.element.get_container(element)
if current_aggregate:
@@ -173,8 +179,8 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context, ifc_class):
aggregate = bpy.data.objects.new("Assembly", None)
def create_aggregate(self, context, ifc_class, aggregate_name):
aggregate = bpy.data.objects.new(aggregate_name, None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
return aggregate
@@ -268,23 +274,23 @@ class BIM_OT_add_part_to_object(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
part_class: bpy.props.StringProperty(name="Class", options={"HIDDEN"})
part_name: bpy.props.StringProperty(name="Name")
obj: bpy.props.StringProperty(options={"HIDDEN"})
element: bpy.props.IntProperty(options={"HIDDEN"})
def invoke(self, context, event):
self.part_name = "My " + self.part_class.lstrip("Ifc")
return context.window_manager.invoke_props_dialog(self)
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) or context.active_object
core.add_part_to_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
tool.Blender,
obj=obj,
obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.element)) if self.element else context.active_object,
part_class=self.part_class,
part_name=self.part_name,
)
blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
@@ -34,15 +34,27 @@ from bpy.props import (
def update_relating_object(self, context):
def message(self, context):
self.layout.label(text="Please select a valid Ifc Element")
self.layout.label(text="Please select a valid IFC Element")
if self.relating_object is None:
return
if not self.relating_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
if self.relating_object:
self.related_object = None
if not self.relating_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
self.relating_object = None
def update_related_object(self, context):
def message(self, context):
self.layout.label(text="Please select a valid IFC Element")
if self.related_object:
self.relating_object = None
if not self.related_object.BIMObjectProperties.ifc_definition_id:
context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO")
self.related_object = None
class BIMObjectAggregateProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_object: PointerProperty(name="Aggregate", type=bpy.types.Object, update=update_relating_object)
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, update=update_relating_object)
related_object: PointerProperty(name="Related Part", type=bpy.types.Object, update=update_related_object)
@@ -20,6 +20,7 @@ from bpy.types import Panel
from blenderbim.bim.module.aggregate.data import AggregateData
from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
class BIM_PT_aggregate(Panel):
@@ -52,12 +53,21 @@ class BIM_PT_aggregate(Panel):
props = context.active_object.BIMObjectAggregateProperties
if props.is_editing:
row = layout.row()
row.prop(props, "relating_object", text="Whole")
row = layout.row()
row.prop(props, "related_object", text="Or Part")
row = layout.row(align=True)
row.prop(props, "relating_object", text="")
col = row.column(align=True)
if not props.relating_object and not props.related_object:
col.enabled = False
op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK")
if props.relating_object:
op = row.operator("bim.aggregate_assign_object", icon="CHECKMARK", text="")
op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id
elif props.related_object:
op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id
row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="")
return
else:
row = layout.row(align=True)
if AggregateData.data["has_relating_object"]:
@@ -72,7 +82,7 @@ class BIM_PT_aggregate(Panel):
row.operator("bim.add_aggregate", icon="ADD", text="")
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
else:
row.label(text="No Aggregate", icon="TRIA_UP")
row.label(text="No Whole Relationship Found", icon="TRIA_UP")
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -89,19 +99,6 @@ class BIM_PT_aggregate(Panel):
op = row.operator("bim.select_parts", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
ifc_class = AggregateData.data["ifc_class"]
part_class = ""
if ifc_class == "IfcBuilding":
part_class = "IfcBuildingStorey"
elif ifc_class == "IfcSite":
part_class = "IfcBuilding"
elif ifc_class == "IfcProject":
part_class = "IfcSite"
if part_class != "":
op = layout.operator("bim.add_part_to_object", text="Add " + part_class.lstrip("Ifc"))
op.part_class = part_class
op.obj = context.active_object.name
class BIM_PT_linked_aggregate(Panel):
bl_label = "Linked Aggregates"
@@ -131,22 +128,29 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
props = context.active_object.BIMObjectAggregateProperties
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
row = layout.row(align=True)
row.label(text="Advanced Users Only", icon="ERROR")
row = layout.row(align=True)
if type(AggregateData.data['total_linked_aggregate']) is int:
if AggregateData.data['total_linked_aggregate'] > 0:
row.label(text=f"{AggregateData.data['total_linked_aggregate']} Linked Aggregates")
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
op.select_parts = False
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
op.select_parts = True
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
if element.Decomposes:
Number_Linked_Aggregates = AggregateData.data['total_linked_aggregate']
if not Number_Linked_Aggregates:
row.label(text="Not a Linked Aggregate")
else:
row.label(text=f"{Number_Linked_Aggregates} Linked Aggregates")
op = row.operator("bim.duplicate_linked_aggregate_to_3d_cursor", text="", icon="DUPLICATE")
if type(Number_Linked_Aggregates) is int:
if Number_Linked_Aggregates > 0:
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD")
op.select_parts = False
op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD")
op.select_parts = True
row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH")
op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X")
else:
row.label(text="No Linked Aggregates")
row.label(text="Not an Aggregate")
@@ -27,15 +27,12 @@ classes = (
operator.CopyAttributeToSelection,
prop.BIMAttributeProperties,
ui.BIM_PT_object_attributes,
ui.BIM_PT_material_attributes,
)
def register():
bpy.types.Object.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
bpy.types.Material.BIMAttributeProperties = bpy.props.PointerProperty(type=prop.BIMAttributeProperties)
def unregister():
del bpy.types.Object.BIMAttributeProperties
del bpy.types.Material.BIMAttributeProperties
@@ -23,7 +23,6 @@ import blenderbim.tool as tool
def refresh():
AttributesData.is_loaded = False
MaterialAttributesData.is_loaded = False
class AttributesData:
@@ -51,28 +50,3 @@ class AttributesData:
key = "STEP ID"
results.append({"name": key, "value": str(value)})
return results
class MaterialAttributesData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {"ifc_definition_id": cls.ifc_definition_id(), "attributes": cls.attributes()}
cls.is_loaded = True
@classmethod
def ifc_definition_id(cls):
return bpy.context.active_object.active_material.BIMObjectProperties.ifc_definition_id
@classmethod
def attributes(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
data = element.get_info()
for key, value in data.items():
if value is None or isinstance(value, ifcopenshell.entity_instance) or key in ["id", "type"]:
continue
results.append({"name": key, "value": str(value)})
return results
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import blenderbim.bim.helper
import blenderbim.bim.handler
import blenderbim.tool as tool
@@ -39,14 +40,10 @@ class EnableEditingAttributes(bpy.types.Operator):
bl_label = "Enable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
self.file = IfcStore.get_file()
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.attributes.clear()
@@ -84,13 +81,9 @@ class DisableEditingAttributes(bpy.types.Operator):
bl_label = "Disable Editing Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def execute(self, context):
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
props.is_editing_attributes = False
return {"FINISHED"}
@@ -101,14 +94,10 @@ class EditAttributes(bpy.types.Operator, Operator):
bl_label = "Edit Attributes"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
if self.obj_type == "Object":
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
obj = bpy.data.objects.get(self.obj)
props = obj.BIMAttributeProperties
product = tool.Ifc.get_entity(obj)
@@ -125,7 +114,7 @@ class EditAttributes(bpy.types.Operator, Operator):
attributes = blenderbim.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.run("attribute.edit_attributes", self.file, product=product, attributes=attributes)
bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type)
bpy.ops.bim.disable_editing_attributes(obj=obj.name)
return {"FINISHED"}
@@ -19,34 +19,34 @@
import blenderbim.bim.helper
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.attribute.data import AttributesData, MaterialAttributesData
from blenderbim.bim.module.attribute.data import AttributesData
def draw_ui(context, layout, obj_type, attributes):
obj = context.active_object if obj_type == "Object" else context.active_object.active_material
def draw_ui(context, layout, attributes):
obj = context.active_object
oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
if props.is_editing_attributes:
row = layout.row(align=True)
op = row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
op.obj_type = obj_type
op.obj = obj.name
op = row.operator("bim.disable_editing_attributes", icon="CANCEL", text="")
op.obj_type = obj_type
op.obj = obj.name
blenderbim.bim.helper.draw_attributes(props.attributes, layout, copy_operator="bim.copy_attribute_to_selection")
else:
row = layout.row()
op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
op.obj_type = obj_type
op.obj = obj.name
for attribute in attributes:
row = layout.row(align=True)
row.label(text=attribute["name"])
row.label(text=attribute["value"])
# row.label(text=attribute["value"])
op = row.operator("bim.select_similar", text=attribute["value"], icon="NONE", emboss=False)
op.key = attribute['name']
# TODO: reimplement, see #1222
# if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name:
@@ -72,32 +72,4 @@ class BIM_PT_object_attributes(Panel):
def draw(self, context):
if not AttributesData.is_loaded:
AttributesData.load()
draw_ui(context, self.layout, "Object", AttributesData.data["attributes"])
class BIM_PT_material_attributes(Panel):
bl_label = "Material Attributes"
bl_idname = "BIM_PT_material_attributes"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
@classmethod
def poll(cls, context):
if not IfcStore.get_file():
return False
try:
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
except:
return False
def draw(self, context):
if not MaterialAttributesData.is_loaded:
MaterialAttributesData.load()
elif (
context.active_object.active_material.BIMObjectProperties.ifc_definition_id
!= MaterialAttributesData.data["ifc_definition_id"]
):
MaterialAttributesData.load()
draw_ui(context, self.layout, "Material", MaterialAttributesData.data["attributes"])
draw_ui(context, self.layout, AttributesData.data["attributes"])
@@ -59,6 +59,20 @@ class LoadBcfProject(bpy.types.Operator):
if self.filepath:
bcfstore.BcfStore.set_by_filepath(self.filepath)
bcfxml = bcfstore.BcfStore.get_bcfxml()
# a BCFv2.1 does not need to have a project, but BBIM likes to have one
# https://github.com/buildingSMART/BCF-XML/tree/release_2_1/Documentation#bcf-file-structure
nameless = "Unknown"
if bcfxml.project is None:
if bcfxml.version.version_id.startswith("2"):
print("No project, we will create one for BBIM.")
bcfxml.project_info = bcf.v2.model.ProjectExtension(
project=bcf.v2.model.Project(
name=nameless,
project_id=str(uuid.uuid4())
), extension_schema=""
)
if bcfxml.project.name is None:
bcfxml.project.name = nameless
context.scene.BCFProperties.name = bcfxml.project.name
bpy.ops.bim.load_bcf_topics()
return {"FINISHED"}
@@ -86,7 +100,19 @@ class LoadBcfTopics(bpy.types.Operator):
def execute(self, context):
bcfxml = bcfstore.BcfStore.get_bcfxml()
context.scene.BCFProperties.topics.clear()
for index, topic_guid in enumerate(bcfxml.topics.keys()):
# workaround, one non standard topic would break reading entire bcf
# ignored these topics ATM
# happens on non standard nodes or on missing nodes in markup
topics2use = []
for topic_guid in bcfxml.topics.keys():
# print("topic guid: {}".format(topic_guid))
try:
topic_titel = bcfxml.topics[topic_guid].topic.title
topics2use.append(topic_guid)
except:
print("Problems on reading topic, thus ignored: {}".format(topic_guid))
continue
for index, topic_guid in enumerate(topics2use):
new = context.scene.BCFProperties.topics.add()
bpy.ops.bim.load_bcf_topic(topic_guid=topic_guid, topic_index=index)
return {"FINISHED"}
@@ -938,33 +964,56 @@ class ActivateBcfViewpoint(bpy.types.Operator):
# Operators with context overrides are used because they are
# significantly faster than looping through all objects
exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions or []}
self.set_exceptions(viewpoint, context)
self.set_view_setup_hints(viewpoint, context)
# set selection at the end not to conflict with .hide_spaces
self.set_selection(viewpoint)
self.set_colours(viewpoint)
def set_exceptions(self, viewpoint, context):
if (
not hasattr(viewpoint.visualization_info.components, "visibility")
or not hasattr(viewpoint.visualization_info.components.visibility.exceptions, "component")
):
return
exception_global_ids = {v.ifc_guid for v in viewpoint.visualization_info.components.visibility.exceptions.component or []}
# print("default_visibility: {}".format(viewpoint.visualization_info.components.visibility.default_visibility))
if viewpoint.visualization_info.components.visibility.default_visibility:
# default_visibility is True: show all objs, hide the exceptions
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.hide_view_clear()
context.area.type = old
for global_id in exception_global_ids:
# print("{}: hide".format(global_id))
obj = IfcStore.get_element(global_id)
if obj and bpy.context.view_layer.objects.get(obj.name):
# print(" obj found")
obj.hide_set(True)
else:
# default_visibility is False: hide all objs, show the exceptions
objs = []
for global_id in exception_global_ids:
# print("{}: show".format(global_id))
obj = IfcStore.get_element(global_id)
if obj:
# print(" obj found")
objs.append(obj)
if objs:
old = context.area.type
context.area.type = "VIEW_3D"
bpy.ops.object.hide_view_clear()
context_override = {}
context_override["object"] = context_override["active_object"] = objs[0]
context_override["selected_objects"] = context_override["selected_editable_objects"] = objs
with context.temp_override(**context_override):
bpy.ops.object.hide_view_set(unselected=True)
bpy.data.objects["Viewpoint"].hide_set(False)
context.area.type = old
def set_view_setup_hints(self, viewpoint, context):
if viewpoint.visualization_info.components.view_setup_hints:
if not viewpoint.visualization_info.components.view_setup_hints.spaces_visible:
self.hide_spaces(context)
@@ -976,10 +1025,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
self.hide_spaces(context)
self.set_openings_visibility(False, context)
# set selection at the end not to conflict with .hide_spaces
self.set_selection(viewpoint)
self.set_colours(viewpoint)
def hide_spaces(self, context):
old = context.area.type
context.area.type = "VIEW_3D"
@@ -997,18 +1042,25 @@ class ActivateBcfViewpoint(bpy.types.Operator):
selected_global_ids = [s.ifc_guid for s in viewpoint.visualization_info.components.selection.component or []]
bpy.ops.object.select_all(action="DESELECT")
for global_id in selected_global_ids:
# print("{}: selected".format(global_id))
obj = IfcStore.get_element(global_id)
if obj:
# print(" obj found")
obj.select_set(True)
obj.hide_set(False)
def set_colours(self, viewpoint):
if not viewpoint.visualization_info.components or not viewpoint.visualization_info.components.coloring:
return
global_id_colours = {}
for coloring in viewpoint.visualization_info.components.coloring or []:
for component in coloring.components:
global_id_colours.setdefault(component.ifc_guid, coloring.color)
for acoloring in viewpoint.visualization_info.components.coloring.color:
for acomponent in acoloring.component:
global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color)
for global_id, color in global_id_colours.items():
# print("{}: color: {}: {}".format(global_id, color, self.hex_to_rgb(color)))
obj = IfcStore.get_element(global_id)
if obj:
# print(" obj found ")
obj.color = self.hex_to_rgb(color)
def draw_lines(self, viewpoint, context):
@@ -1092,8 +1144,14 @@ class ActivateBcfViewpoint(bpy.types.Operator):
def hex_to_rgb(self, value):
value = value.lstrip("#")
lv = len(value)
t = tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3))
return [t[0] / 255.0, t[1] / 255.0, t[2] / 255.0, 1]
# https://github.com/buildingSMART/BCF-XML/tree/release_3_0/Documentation#coloring
if lv == 8:
t = tuple(int(value[i : i + lv // 4], 16) for i in range(0, lv, lv // 4))
col = [t[1] / 255.0, t[2] / 255.0, t[3] / 255.0, t[0] / 255.0]
else:
t = tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3))
col = [t[0] / 255.0, t[1] / 255.0, t[2] / 255.0, 1]
return col
class OpenBcfReferenceLink(bpy.types.Operator):
@@ -20,6 +20,7 @@ import bpy
import bmesh
import logging
import shapely
import shapely.ops
import mathutils
import numpy as np
import multiprocessing
@@ -132,14 +133,11 @@ class Loader:
return mesh
def load_settings(self):
settings = ifcopenshell.geom.settings()
settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False)
settings.set(settings.USE_BREP_DATA, False)
return settings
return ifcopenshell.geom.settings()
def load_fallback_settings(self):
settings = ifcopenshell.geom.settings()
settings.set(settings.INCLUDE_CURVES, True)
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
return settings
def load_importer(self):
@@ -631,24 +629,18 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# TODO : refactor to be able to generate all boundaries for selected space automatically or with an option
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
self.layout.label(text="Please set an active container to detect space boundaries from.")
element = tool.Ifc.get_entity(objs[0])
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = objs[0]
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
if not (container := tool.Root.get_default_container()):
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
for subelement in ifcopenshell.util.element.get_decomposition(container):
if not (
subelement.is_a("IfcWall") or subelement.is_a("IfcSlab") or subelement.is_a("IfcVirtualElement")
):
@@ -691,8 +683,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
tree = ifcopenshell.geom.tree()
shapes = {}
settings = ifcopenshell.geom.settings()
settings.set(settings.STRICT_TOLERANCE, True)
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
settings.set("disable-opening-subtractions", True)
iterator = ifcopenshell.geom.iterator(settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=include)
if iterator.initialize():
while True:
@@ -830,17 +821,10 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# Create shape of opening as a dissolved BMesh
settings = ifcopenshell.geom.settings()
settings.set(settings.STRICT_TOLERANCE, True)
shape = ifcopenshell.geom.create_shape(settings, opening)
m = shape.transformation.matrix
mat = Matrix(
(
[m[0], m[3], m[6], m[9]],
[m[1], m[4], m[7], m[10]],
[m[2], m[5], m[8], m[11]],
[0, 0, 0, 1],
)
)
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
mat.translation = (0, 0, 0)
opening_bm = bmesh.new()
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
for vert in verts:
@@ -1031,12 +1015,10 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
settings = ifcopenshell.geom.settings()
if not element.is_a("IfcOpeningElement"):
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
settings.set(settings.STRICT_TOLERANCE, True)
settings.set("disable-opening-subtractions", True)
# geometry = ifcopenshell.geom.create_shape(settings, body)
shape = ifcopenshell.geom.create_shape(settings, element)
m = shape.transformation.matrix
mat = Matrix(([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]))
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
verts = [space_matrix_i @ mat @ Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
polygons = []
@@ -54,11 +54,11 @@ classes = (
def register():
bpy.types.Scene.BIMClassificationProperties = bpy.props.PointerProperty(type=prop.BIMClassificationProperties)
bpy.types.Object.BIMClassificationReferenceProperties = bpy.props.PointerProperty(
bpy.types.Scene.BIMClassificationReferenceProperties = bpy.props.PointerProperty(
type=prop.BIMClassificationReferenceProperties
)
def unregister():
del bpy.types.Scene.BIMClassificationProperties
del bpy.types.Object.BIMClassificationReferenceProperties
del bpy.types.Scene.BIMClassificationReferenceProperties
@@ -88,7 +88,7 @@ class ClassificationReferencesData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "OBJECT"
cls.data["object_type"] = "Object"
@classmethod
def references(cls):
@@ -112,17 +112,21 @@ class MaterialClassificationsData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "MATERIAL"
cls.data["object_type"] = "Material"
@classmethod
def references(cls):
results = []
element = tool.Ifc.get_entity(bpy.context.active_object.active_material)
if element:
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
props = bpy.context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
element = tool.Ifc.get().by_id(material.ifc_definition_id)
for reference in ifcopenshell.util.classification.get_references(element):
data = reference.get_info()
del data["ReferencedSource"]
results.append(data)
return results
@@ -136,7 +140,7 @@ class CostClassificationsData(ReferencesData):
cls.data["references"] = cls.references()
cls.data["active_classification_library"] = cls.active_classification_library()
cls.data["classifications"] = cls.classifications()
cls.data["object_type"] = "COST"
cls.data["object_type"] = "Cost"
@classmethod
def references(cls):
@@ -77,26 +77,37 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_manual_classification_reference"
bl_label = "Add Manual Classification Reference"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
def _execute(self, context):
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
if self.obj_type == "Object":
if context.selected_objects:
objects = [o.name for o in context.selected_objects]
else:
objects = [context.active_object.name]
else:
objects = [self.obj]
props = context.scene.BIMClassificationReferenceProperties
attributes = blenderbim.bim.helper.export_attributes(props.reference_attributes)
product = tool.Ifc.get_entity(obj)
# TODO: refactor and support material and cost item classifications
classification = tool.Ifc.get().by_id(int(props.classifications))
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=[product],
classification=classification,
identification="X",
name="Unnamed",
)
ifcopenshell.api.run(
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
)
products = [
tool.Ifc.get().by_id(ifc_definition_id)
for obj in objects
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
if products:
classification = tool.Ifc.get().by_id(int(props.classifications))
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
products=products,
classification=classification,
identification="X",
name="Unnamed",
)
ifcopenshell.api.run(
"classification.edit_reference", tool.Ifc.get(), reference=reference, attributes=attributes
)
props.is_adding = False
@@ -150,8 +161,7 @@ class EnableAddingManualClassificationReference(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
props = context.scene.BIMClassificationReferenceProperties
props.is_adding = True
props.reference_attributes.clear()
blenderbim.bim.helper.import_attributes2("IfcClassificationReference", props.reference_attributes)
@@ -164,8 +174,7 @@ class DisableAddingManualClassificationReference(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
props = obj.BIMClassificationReferenceProperties
props = context.scene.BIMClassificationReferenceProperties
props.is_adding = False
return {"FINISHED"}
@@ -209,6 +218,10 @@ class DisableEditingClassification(bpy.types.Operator):
class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_classification"
bl_label = "Remove Classification"
bl_description = (
"The classification and all of its relationships, children references, "
"and relationships between objects and child references will be completely removed from a project"
)
bl_options = {"REGISTER", "UNDO"}
classification: bpy.props.IntProperty()
@@ -253,8 +266,7 @@ class EnableEditingClassificationReference(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMClassificationReferenceProperties
props = context.scene.BIMClassificationReferenceProperties
props.reference_attributes.clear()
blenderbim.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.reference), props.reference_attributes)
props.active_reference_id = self.reference
@@ -268,8 +280,7 @@ class DisableEditingClassificationReference(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj.BIMClassificationReferenceProperties.active_reference_id = 0
context.scene.BIMClassificationReferenceProperties.active_reference_id = 0
return {"FINISHED"}
@@ -322,8 +333,7 @@ class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
obj: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = obj.BIMClassificationReferenceProperties
props = context.scene.BIMClassificationReferenceProperties
attributes = {}
for attribute in props.reference_attributes:
if attribute.is_null:
@@ -364,9 +374,8 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
classification = element
break
ifc_file = tool.Ifc.get()
products = [
ifc_file.by_id(ifc_definition_id)
tool.Ifc.get().by_id(ifc_definition_id)
for obj in objects
if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
]
@@ -118,10 +118,9 @@ class BIM_PT_classifications(Panel):
class ReferenceUI:
def draw_ui(self, context):
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.sprops = context.scene.BIMClassificationProperties
self.bprops = context.scene.BIMBSDDProperties
self.props = obj.BIMClassificationReferenceProperties
self.props = context.scene.BIMClassificationReferenceProperties
self.file = IfcStore.get_file()
self.draw_add_ui(context)
@@ -155,7 +154,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()
@@ -295,28 +294,31 @@ class BIM_PT_material_classifications(Panel, ReferenceUI):
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_context = "scene"
bl_parent_id = "BIM_PT_materials"
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
return False
try:
return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id)
except:
return False
props = context.scene.BIMMaterialProperties
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index]
if material.ifc_definition_id:
return True
return False
def draw(self, context):
if not MaterialClassificationsData.is_loaded:
MaterialClassificationsData.load()
self.data = MaterialClassificationsData
self.obj = context.active_object.active_material.name
self.obj = ""
self.obj_type = "Material"
self.draw_ui(context)
class BIM_PT_cost_classifications(Panel, ReferenceUI):
bl_label = "Cost Classifications"
bl_label = "Cost Item Classifications"
bl_idname = "BIM_PT_cost_classifications"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
@@ -53,6 +53,10 @@ class AddContext(bpy.types.Operator, Operator):
class RemoveContext(bpy.types.Operator, Operator):
bl_idname = "bim.remove_context"
bl_label = "Remove Context"
bl_description = (
"Remove representation context. Any representation geometry that is assigned to the context is also removed. "
"If a context is removed, then any subcontexts are also removed"
)
bl_options = {"REGISTER", "UNDO"}
context: bpy.props.IntProperty()
@@ -82,6 +82,7 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
ui.BIM_PT_cost_item_quantities,
@@ -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)
@@ -193,10 +198,16 @@ class CostSchedulesData:
# data["DerivedUnitSymbol"] = "?"
# print("Total Cost", data["DerivedTotalCostQuantity"], cost_item.Name)
# TODO: dead code?
@classmethod
def _get_object_quantities(cls, cost_item, element):
def _get_object_quantities(
cls, cost_item: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
) -> list[int]:
if not element.is_a("IfcObject"):
return []
cost_quantities = cost_item.CostQuantities
if not cost_quantities:
return []
results = []
for relationship in element.IsDefinedBy:
if not relationship.is_a("IfcRelDefinesByProperties"):
@@ -205,7 +216,7 @@ class CostSchedulesData:
if not qto.is_a("IfcElementQuantity"):
continue
for prop in qto.Quantities:
if prop in cost_item.CostQuantities or []:
if prop in cost_quantities:
results.append(prop.id())
return results
@@ -73,7 +73,7 @@ class RemoveCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
cost_schedule: bpy.props.IntProperty()
def _execute(self, context):
core.remove_cost_schedule(tool.Ifc, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
core.remove_cost_schedule(tool.Ifc, tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
class EnableEditingCostSchedule(bpy.types.Operator):
@@ -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"
@@ -527,6 +533,13 @@ class AddCostColumn(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
if not context.scene.BIMCostProperties.cost_column:
cls.poll_message_set("Cost column name is empty")
return False
return True
def execute(self, context):
core.add_cost_column(tool.Cost, self.name)
return {"FINISHED"}
@@ -538,7 +551,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 +669,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 +693,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):
@@ -175,6 +175,10 @@ def update_active_cost_item_resources(self, context):
bpy.ops.bim.load_cost_item_resource_quantities()
class ScheduleColumn(PropertyGroup):
schedule_id: IntProperty()
class BIMCostProperties(PropertyGroup):
cost_schedule_predefined_types: EnumProperty(
items=get_schedule_predefined_types, name="Predefined Type", default=None
@@ -210,9 +214,14 @@ 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)
columns: CollectionProperty(name="Active Schedule Columns", type=StrProperty)
columns_storage: CollectionProperty(name="Columns", type=ScheduleColumn)
active_column_index: IntProperty(name="Active Column Index")
cost_item_products: CollectionProperty(name="Cost Item Products", type=CostItemQuantity)
active_cost_item_product_index: IntProperty(name="Active Cost Item Product Index")
+22 -10
View File
@@ -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:
@@ -745,9 +755,11 @@ class BIM_UL_product_cost_items(UIList):
class BIM_PT_Costing_Tools(Panel):
bl_label = "5D Tools"
bl_idname = "BIM_PT_Costing_Tools"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "4D/5D Toolkit"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_cost"
def draw(self, context):
self.props = context.scene.BIMCostProperties
@@ -29,13 +29,9 @@ def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.CoveringTool, after={"bim.structural_tool"}, separator=False, group=False)
bpy.types.Scene.BIMCoveringProperties = bpy.props.PointerProperty(type=prop.BIMCoveringProperties)
# bpy.types.Object.BIMObjectSpatialProperties = bpy.props.PointerProperty(type=prop.BIMObjectSpatialProperties)
# bpy.types.Scene.BIMSpatialManagerProperties = bpy.props.PointerProperty(type=prop.BIMSpatialManagerProperties)
def unregister():
if not bpy.app.background:
bpy.utils.unregister_tool(workspace.CoveringTool)
del bpy.types.Scene.BIMCoveringProperties
# del bpy.types.Object.BIMObjectSpatialProperties
# del bpy.types.Scene.BIMSpatialManagerProperties
@@ -35,6 +35,7 @@ class Operator:
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class CoveringTool(WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
@@ -53,18 +54,19 @@ class CoveringTool(WorkSpaceTool):
def draw_settings(cls, context, layout, ws_tool):
CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey(layout, text, hotkey, description):
args = ["covering", layout, text, hotkey, description]
tool.Blender.add_layout_hotkey_operator(*args)
class CoveringToolUI:
@classmethod
def draw(cls, context, layout, ifc_element_type = None):
def draw(cls, context, layout, ifc_element_type=None):
cls.layout = layout
cls.props = context.scene.BIMModelProperties
cls.covering_props = context.scene.BIMCoveringProperties
row = cls.layout.row(align=True)
if not tool.Ifc.get():
row.label(text="No IFC Project", icon="ERROR")
@@ -82,8 +84,6 @@ class CoveringToolUI:
cls.draw_default_interface()
@classmethod
def draw_header_interface(cls):
cls.draw_type_selection_interface()
@@ -95,93 +95,35 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.prop(data=cls.covering_props, property="ceiling_height", text="Ceiling Height")
if AuthoringData.data["ifc_classes"]:
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
collection = bpy.context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_constr_type_instance", text="Add")
relating_type_id = int(cls.props.relating_type_id)
type_material_usage = ifcopenshell.util.element.get_material(tool.Ifc.get().by_id(relating_type_id)).is_a()
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_instance_flooring_covering_from_cursor")
# PLEASE KEEP COMMENTS AS A REMINDER
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_instance_ceiling_covering_from_cursor")
if (type_material_usage == "IfcMaterialLayerSet" and
not bpy.context.selected_objects):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
if tool.Ifc.get_entity(collection_obj):
if AuthoringData.data["predefined_type"] == "FLOORING":
op = row.operator("bim.add_instance_flooring_covering_from_cursor")
elif AuthoringData.data["predefined_type"] == "CEILING":
op = row.operator("bim.add_instance_ceiling_covering_from_cursor")
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_instance_flooring_coverings_from_walls")
else:
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
row.operator("bim.add_instance_ceiling_coverings_from_walls")
elif (AuthoringData.data["predefined_type"] == "FLOORING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_flooring_coverings_from_walls")
elif (AuthoringData.data["predefined_type"] == "CEILING" and
type_material_usage == "IfcMaterialLayerSet" and
element and
bpy.context.selected_objects and
element.is_a("IfcWall")):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
elif (element and
bpy.context.selected_objects and
element.is_a("IfcCovering") and
# AuthoringData.data["predefined_type"] == "FLOORING" and
AuthoringData.data["active_material_usage"] == "LAYER3"):
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
op = row.operator("bim.regen_selected_covering_object")
# elif AuthoringData.data["predefined_type"] == "CEILING":
# row = cls.layout.row(align=True)
# row.prop(data=cls.props, property="ceiling_height", text="ceiling height")
# if element and bpy.context.selected_objects and element.is_a("IfcWall"):
# op = row.operator("bim.add_instance_ceiling_coverings_from_walls")
# elif element and bpy.context.selected_objects and element.is_a("IfcSpace"):
# op. = row.operator("bim.add_istance_flooring_from_spaces"):
# else:
# op = row.operator("bim.add_instance_ceiling_from_cursor")
# op = row.operator("bim.add_constr_type_instance", text="Add")
# op.from_invoke = True
# if cls.props.relating_type_id.isnumeric():
# op.relating_type_id = int(cls.props.relating_type_id)
else:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_G")
row.operator("bim.regen_selected_covering_object")
@classmethod
def draw_type_selection_interface(cls):
@@ -227,7 +169,7 @@ class Hotkey(bpy.types.Operator, Operator):
return operator.description or ""
def _execute(self, context):
# self.props = context.scene.BIMCoveringProperties
# self.props = context.scene.BIMCoveringProperties
getattr(self, f"hotkey_{self.hotkey}")()
def invoke(self, context, event):
@@ -241,20 +183,19 @@ class Hotkey(bpy.types.Operator, Operator):
def hotkey_S_A(self):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
collection = bpy.context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
container = tool.Root.get_default_container()
if AuthoringData.data["predefined_type"] == "FLOORING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_flooring_coverings_from_walls()
elif tool.Ifc.get_entity(collection_obj):
elif container:
bpy.ops.bim.add_instance_flooring_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
elif AuthoringData.data["predefined_type"] == "CEILING":
if element and bpy.context.selected_objects and element.is_a("IfcWall"):
bpy.ops.bim.add_instance_ceiling_coverings_from_walls()
elif tool.Ifc.get_entity(collection_obj):
elif container:
bpy.ops.bim.add_instance_ceiling_covering_from_cursor()
else:
bpy.ops.bim.add_constr_type_instance()
@@ -264,9 +205,10 @@ class Hotkey(bpy.types.Operator, Operator):
def hotkey_S_G(self):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if (element and
bpy.context.selected_objects and
element.is_a("IfcCovering") and
AuthoringData.data["active_material_usage"] == "LAYER3"):
bpy.ops.bim.regen_selected_covering_object()
if (
element
and bpy.context.selected_objects
and element.is_a("IfcCovering")
and AuthoringData.data["active_material_usage"] == "LAYER3"
):
bpy.ops.bim.regen_selected_covering_object()
@@ -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
@@ -72,7 +73,7 @@ class ReorderCsvAttribute(bpy.types.Operator):
def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index]
new = context.scene.CsvProperties.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary"]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
for prop in props:
value = getattr(new, prop)
setattr(new, prop, getattr(old, prop))
@@ -246,6 +247,7 @@ class ExportIfcCsv(bpy.types.Operator):
if props.format != "csv" and props.should_generate_svg:
schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
self.report({"INFO"}, f"Data is exported to {props.format.upper()}.")
return {"FINISHED"}
@@ -285,6 +287,7 @@ class ImportIfcCsv(bpy.types.Operator):
if not props.should_load_from_memory:
ifc_file.write(props.csv_ifc_file)
refresh_ui_data()
self.report({"INFO"}, "Data is imported to IFC.")
return {"FINISHED"}
@@ -35,7 +35,6 @@ classes = (
operator.PrintUnusedElementStats,
operator.ProfileImportIFC,
operator.PurgeHdf5Cache,
operator.PurgeIfcLinks,
operator.PurgeUnusedElementsByClass,
operator.RewindInspector,
operator.SelectExpressFile,
@@ -24,9 +24,9 @@ import random
import logging
import platform
import subprocess
import addon_utils
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
@@ -34,38 +34,17 @@ import blenderbim.tool as tool
import blenderbim.core.debug as core
import blenderbim.bim.handler
import blenderbim.bim.import_ifc as import_ifc
import blenderbim.tool as tool
from blenderbim import get_debug_info, format_debug_info
from blenderbim.bim.ifc import IfcStore
class CopyDebugInformation(bpy.types.Operator):
bl_idname = "bim.copy_debug_information"
bl_label = "Copy Debug Information"
bl_description = "Copies debugging information to your clipboard for use in bugreports"
bl_description = "Copies debugging information to your clipboard for use in bug reports"
def execute(self, context):
version = ".".join(
[
str(x)
for x in [
addon.bl_info.get("version", (-1, -1, -1))
for addon in addon_utils.modules()
if addon.bl_info["name"] == "BlenderBIM"
][0]
]
)
info = {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"architecture": platform.architecture(),
"machine": platform.machine(),
"processor": platform.processor(),
"blender_version": bpy.app.version_string,
"blenderbim_version": version,
"ifc": False,
}
info = get_debug_info()
if tool.Ifc.get():
info.update(
{
@@ -76,17 +55,15 @@ class CopyDebugInformation(bpy.types.Operator):
}
)
# Format it in a readable way
text = "\n".join(f"{k}: {v}" for k, v in info.items())
print(text)
text = format_debug_info(info)
if platform.system() == "Windows":
command = "echo | set /p nul=" + text.strip()
elif platform.system() == "Darwin": # for MacOS
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy'
else: # Linux
command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
subprocess.run(command, shell=True, check=True)
text_with_backticks = f"```\n{text}\n```"
print("-" * 80)
print(text_with_backticks)
print("-" * 80)
context.window_manager.clipboard = text_with_backticks
return {"FINISHED"}
@@ -104,10 +81,11 @@ class PrintIfcFile(bpy.types.Operator):
return {"FINISHED"}
class PurgeIfcLinks(bpy.types.Operator):
bl_idname = "bim.purge_ifc_links"
bl_label = "Purge IFC Links"
bl_description = "Purge all definitions and references from the file.\nWarning : Cannot be undone."
class ConvertToBlender(bpy.types.Operator):
bl_idname = "bim.convert_to_blender"
bl_label = "Convert To Blender File"
bl_description = "Removes all IFC data and revert to basic Blender objects.\nWarning : Cannot be undone."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in bpy.data.objects:
@@ -116,7 +94,8 @@ class PurgeIfcLinks(bpy.types.Operator):
if obj.data:
obj.data.BIMMeshProperties.ifc_definition_id = 0
for material in bpy.data.materials:
material.BIMMaterialProperties.ifc_style_id = False
material.BIMObjectProperties.ifc_definition_id = 0
material.BIMMaterialProperties.ifc_style_id = 0
context.scene.BIMProperties.ifc_file = ""
context.scene.BIMDebugProperties.attributes.clear()
IfcStore.purge()
@@ -124,26 +103,6 @@ class PurgeIfcLinks(bpy.types.Operator):
return {"FINISHED"}
class ConvertToBlender(bpy.types.Operator):
bl_idname = "bim.convert_to_blender"
bl_label = "Convert To Blender File"
bl_description = "Removes all IFC data, and converts the file to a simple Blender file."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for o in bpy.data.objects:
if o.type in {"MESH", "EMPTY"}:
o.BIMObjectProperties.ifc_definition_id = 0
if o.data:
o.data.BIMMeshProperties.ifc_definition_id = 0
for m in bpy.data.materials:
m.BIMMaterialProperties.ifc_style_id = False
bpy.context.scene.BIMProperties.ifc_file = ""
IfcStore.purge()
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class ValidateIfcFile(bpy.types.Operator):
bl_idname = "bim.validate_ifc_file"
bl_label = "Validate IFC File"
@@ -214,7 +173,7 @@ class CreateAllShapes(bpy.types.Operator):
total = len(elements)
settings = ifcopenshell.geom.settings()
settings_2d = ifcopenshell.geom.settings()
settings_2d.set(settings_2d.INCLUDE_CURVES, True)
settings_2d.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
failures = []
excludes = () # For the developer to debug with
for i, element in enumerate(elements, 1):
@@ -267,7 +226,7 @@ class CreateShapeFromStepId(bpy.types.Operator):
element = self.file.by_id(self.step_id or int(context.scene.BIMDebugProperties.step_id))
settings = ifcopenshell.geom.settings()
if self.should_include_curves:
settings.set(settings.INCLUDE_CURVES, True)
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, element)
if shape:
ifc_importer = import_ifc.IfcImporter(self.ifc_import_settings)
@@ -60,9 +60,6 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.purge_hdf5_cache")
row = layout.row()
row.operator("bim.purge_ifc_links")
row = layout.row()
row.operator("bim.update_representation", text="Manually Save Representation")
@@ -159,9 +159,7 @@ class BaseDecorator:
objecttype = "NOTDEFINED"
def __init__(self):
self.font_id = blf.load(
os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf")
)
self.font_id = 0 # 0 is the default font
# POLYLINE_UNIFORM_COLOR is good for smoothed lines since `bgl.enable(GL_LINE_SMOOTH)` is deprecated
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
@@ -404,7 +402,6 @@ class BaseDecorator:
line_no += 1 if multiline_to_bottom else -1
return
# 0 is the default font, but we're fancier than that
font_id = self.font_id
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
@@ -418,12 +415,14 @@ class BaseDecorator:
factor = self.camera_zoom_to_factor(context.space_data.region_3d.view_camera_zoom)
camera_width_px = factor * context.region.width
mm_to_px = camera_width_px / self.get_camera_width_mm()
# 0.004118616 is a magic constant number I visually discovered to get the right number.
# magic_font_scale's default of (0.004118616) is a magic constant number I visually discovered to get the right number.
# In particular it works only for the OpenGOST font and produces a 2.5mm font size.
# It probably should be dynamically calculated using system.dpi or something.
# font_size = 16 <-- this is a good default
# TODO: need to synchronize it better with svg
font_size_px = int(0.004118616 * mm_to_px) * font_size_mm / 2.5
magic_font_scale = bpy.context.scene.DocProperties.magic_font_scale
font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5
pos = pos - line_no * font_size_px * rotation_matrix[1]
blf.size(font_id, font_size_px)
@@ -2014,6 +2013,13 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
if drawing_font := bpy.context.scene.DocProperties.drawing_font:
drawing_font_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", drawing_font)
if os.path.exists(drawing_font_path):
font_id = blf.load(drawing_font_path)
for decorator in self.decorators.values():
decorator.font_id = font_id
def get_objects_and_decorators(self, collection):
# TODO: do it in data instead of the handler for performance?
@@ -29,6 +29,8 @@ import subprocess
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.geom
import ifcopenshell.util.selector
import ifcopenshell.util.representation
@@ -219,11 +221,12 @@ class CreateDrawing(bpy.types.Operator):
def execute(self, context):
self.props = context.scene.DocProperties
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
if self.print_all:
original_drawing_id = self.props.active_drawing_id
original_drawing_id = active_drawing_id
drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing]
else:
drawings_to_print = [self.props.active_drawing_id]
drawings_to_print = [active_drawing_id]
for drawing_i, drawing_id in enumerate(drawings_to_print):
self.drawing_index = drawing_i
@@ -473,7 +476,7 @@ class CreateDrawing(bpy.types.Operator):
if not context or not drawing_elements:
continue
geom_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
DISABLE_TRIANGULATION=True, INCLUDE_CURVES=True
)
if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
@@ -527,31 +530,38 @@ class CreateDrawing(bpy.types.Operator):
files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()}
for link in context.scene.BIMProjectProperties.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
self.setup_serialiser(target_view)
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
for ifc_path, ifc in files.items():
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
# Get all representation contexts to see what we're dealing with.
# Drawings only draw bodies and annotations (and facetation, due to a Revit bug).
# A drawing prioritises a target view context first, followed by a model view context as a fallback.
# Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised.
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
contexts = self.get_linework_contexts(ifc, target_view)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element)
self.setup_serialiser(ifc, target_view)
tree = ifcopenshell.geom.tree()
tree.enable_face_styles(True)
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view)
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view)
if self.camera_element not in drawing_elements:
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
with profile("Camera element"):
# The camera must always be included, regardless of any include/exclude filters.
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True)
geom_settings = ifcopenshell.geom.settings(DISABLE_TRIANGULATION=True)
it = ifcopenshell.geom.iterator(geom_settings, ifc, include=[self.camera_element])
for elem in it:
self.serialiser.write(elem)
@@ -863,13 +873,12 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
def setup_serialiser(self, ifc, target_view):
def setup_serialiser(self, target_view):
self.svg_settings = ifcopenshell.geom.settings(
DISABLE_TRIANGULATION=True, STRICT_TOLERANCE=True, INCLUDE_CURVES=True
DISABLE_TRIANGULATION=True, INCLUDE_CURVES=True
)
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
self.serialiser.setFile(ifc)
self.serialiser.setWithoutStoreys(True)
self.serialiser.setPolygonal(True)
self.serialiser.setUseHlrPoly(True)
@@ -933,6 +942,18 @@ class CreateDrawing(bpy.types.Operator):
self.is_manifold_cache[obj.data.name] = True
return True
def get_element_by_guid(self, guid):
try:
return tool.Ifc.get().by_guid(guid)
except:
for link in bpy.context.scene.BIMProjectProperties.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
return IfcStore.session_files[link.name].by_guid(guid)
except:
continue
def merge_linework_and_add_metadata(self, root):
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
if join_criteria:
@@ -947,7 +968,7 @@ class CreateDrawing(bpy.types.Operator):
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
if "projection" in el.get("class", "").split():
classes = self.get_svg_classes(element)
@@ -960,6 +981,10 @@ class CreateDrawing(bpy.types.Operator):
el.set("class", " ".join(classes))
obj = tool.Ifc.get_object(element)
if not obj: # This is a linked model object. For now, do nothing.
continue
if not self.is_manifold(obj):
continue
@@ -1241,12 +1266,14 @@ class AddDrawingToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": drawing_reference.Location,
"Description": "DRAWING",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=drawing_reference.Location,
Description="DRAWING",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
@@ -1314,9 +1341,10 @@ class CreateSheets(bpy.types.Operator, Operator):
has_sheet_reference = False
for reference in tool.Drawing.get_document_references(sheet):
if reference.Description == "SHEET":
reference_description = tool.Drawing.get_reference_description(reference)
if reference_description == "SHEET":
has_sheet_reference = True
elif reference.Description == "RASTER":
elif reference_description == "RASTER":
if reference.Location in raster_references:
raster_references.remove(reference.Location)
else:
@@ -1327,7 +1355,9 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes={"Location": tool.Ifc.get_relative_uri(svg), "Description": "SHEET"},
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET"
),
)
for raster_reference in raster_references:
@@ -1335,7 +1365,9 @@ class CreateSheets(bpy.types.Operator, Operator):
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes={"Location": tool.Ifc.get_relative_uri(raster_reference), "Description": "RASTER"},
attributes=tool.Drawing.generate_reference_attributes(
reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER"
),
)
svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command
@@ -1444,13 +1476,19 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall()
# save current visibility statuses for Views and Types collections
visibility_status: dict[bpy.types.Object, bool] = {}
for col in bpy.data.collections["Views"].children:
for obj in col.objects:
visibility_status[obj] = obj.hide_get()
for obj in bpy.data.collections["Types"].objects:
visibility_status[obj] = obj.hide_get()
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.object.hide_view_clear()
bpy.ops.bim.activate_status_filters()
subcontext = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
@@ -1468,6 +1506,11 @@ class ActivateModel(bpy.types.Operator):
is_global=True,
should_sync_changes_first=True,
)
# restore visibility after hide_view_clear()
for obj, hide_status in visibility_status.items():
obj.hide_set(hide_status)
tool.Blender.update_viewport()
return {"FINISHED"}
@@ -1495,7 +1538,7 @@ class ActivateDrawing(bpy.types.Operator):
if not self.camera_view_point:
viewport_position = tool.Blender.get_viewport_position()
core.activate_drawing_view(tool.Ifc, tool.Drawing, drawing=drawing)
core.activate_drawing_view(tool.Ifc, tool.Blender, tool.Drawing, drawing=drawing)
if not self.camera_view_point:
tool.Blender.set_viewport_position(viewport_position)
@@ -1589,7 +1632,7 @@ class ReloadDrawingStyles(bpy.types.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
camera_props = context.active_object.data.BIMCameraProperties
camera_props = context.scene.camera.data.BIMCameraProperties
# added this part as a temporary fallback
# TODO: should remove it a bit later when projects get more accommodated
@@ -1841,10 +1884,10 @@ class ActivateDrawingStyle(bpy.types.Operator, Operator):
except:
continue
if self.drawing_style.include_query:
results = ifcopenshell.util.selector.Selector.parse(ifc, self.drawing_style.include_query)
results = ifcopenshell.util.selector.filter_elements(ifc, self.drawing_style.include_query)
self.include_global_ids.extend([e.GlobalId for e in results])
if self.drawing_style.exclude_query:
results = ifcopenshell.util.selector.Selector.parse(ifc, self.drawing_style.exclude_query)
results = ifcopenshell.util.selector.filter_elements(ifc, self.drawing_style.exclude_query)
self.exclude_global_ids.extend([e.GlobalId for e in results])
if self.drawing_style.include_query:
self.parse_filter_query("INCLUDE", context)
@@ -1988,12 +2031,14 @@ class AddScheduleToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "SCHEDULE")]) + 1),
"Location": schedule_location,
"Description": "SCHEDULE",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1
),
Location=schedule_location,
Description="SCHEDULE",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2042,12 +2087,15 @@ class AddReferenceToSheet(bpy.types.Operator, Operator):
return
reference = tool.Ifc.run("document.add_reference", information=sheet)
id_attr = "ItemReference" if tool.Ifc.get_schema() == "IFC2X3" else "Identification"
attributes = {
id_attr: str(len([r for r in references if r.Description in ("DRAWING", "REFERENCE")]) + 1),
"Location": extref_location,
"Description": "REFERENCE",
}
attributes = tool.Drawing.generate_reference_attributes(
reference,
Identification=str(
len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "REFERENCE")])
+ 1
),
Location=extref_location,
Description="REFERENCE",
)
tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
sheet_builder = sheeter.SheetBuilder()
@@ -2392,8 +2440,8 @@ class EditSheet(bpy.types.Operator, Operator):
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
self.name = sheet.Name
self.identification = sheet.Identification
elif sheet.is_a("IfcDocumentReference") and sheet.Description == "TITLEBLOCK":
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
self.document_type = "TITLEBLOCK"
else:
self.document_type = "EMBEDDED"
@@ -2419,7 +2467,7 @@ class EditSheet(bpy.types.Operator, Operator):
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
elif self.document_type == "EMBEDDED":
core.rename_reference(tool.Ifc, reference=sheet, identification=self.identification)
core.rename_reference(tool.Ifc, tool.Drawing, reference=sheet, identification=self.identification)
elif self.document_type == "TITLEBLOCK":
titleblock = self.props.titleblock
reference = sheet
@@ -315,9 +315,7 @@ class RasterStyleProperty(enum.Enum):
SPACE_SHADING = "space.shading"
RASTER_STYLE_PROPERTIES_EXCLUDE = (
"scene.render.filepath",
)
RASTER_STYLE_PROPERTIES_EXCLUDE = ("scene.render.filepath",)
class DocProperties(PropertyGroup):
@@ -371,6 +369,8 @@ 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")
magic_font_scale: bpy.props.FloatProperty(default=0.004118616, name="Font Scale Factor")
class BIMCameraProperties(PropertyGroup):
@@ -16,22 +16,21 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
import os
import re
import bpy
import string
import svgwrite
import openpyxl
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
from odf.opendocument import load as load_ods
from odf.table import Table, TableRow, TableColumn, TableCell
from odf.text import P
from odf.style import Style
from textwrap import wrap
from pathlib import Path
import string
FONT_SIZE = 4.13
FONT_WIDTH = lambda size: size * 0.45
FONT_SIZE_PT = 12
FONT_FAMILY = "OpenGost Type B TT"
DEBUG = False
@@ -63,11 +62,29 @@ class Scheduler:
)
self.padding = 1
self.margin = 1
self.parse_css(infile)
if infile.endswith("ods"):
self.schedule_ods(infile, outfile)
elif infile.endswith("xlsx"):
self.schedule_xlsx(infile, outfile)
def parse_css(self, infile):
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
stylesheet_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "assets", "schedule.css")
with open(stylesheet_path, "r") as stylesheet:
css = stylesheet.read()
matches = re.search("--font-size-pt:\s*([0-9.]+);", css)
self.font_size_pt = float(matches.groups()[0]) if matches else 12 # Default to 12pt
matches = re.search("--font-size-px:\s*([0-9.]+);", css)
self.font_size_px = float(matches.groups()[0]) if matches else 4.13 # Magic number 4.13px ~= 12pt
matches = re.search("--font-width:\s*([0-9.]+);", css)
self.font_width = float(matches.groups()[0]) if matches else 0.45 # A magic number for OpenGost
self.svg.defs.add(self.svg.style(css))
def schedule_xlsx(self, infile, outfile):
workbook = openpyxl.open(infile, data_only=True)
sheet = workbook.active
@@ -144,8 +161,8 @@ class Scheduler:
x += unmerged_width
continue
font_size = cell.font.size or 11 # 11pt default
font_size = font_size / FONT_SIZE_PT * FONT_SIZE # Magic?
font_size = cell.font.size or self.font_size_pt # 12pt default
font_size = font_size / self.font_size_pt * self.font_size_px # Magic?
text_position = [0.0, 0.0]
if cell.alignment.horizontal == "left":
@@ -424,9 +441,9 @@ class Scheduler:
italic_text = final_cell_style.get("font-style", None) == "italic"
# NOTE: very naive since we're scaling text proportionally
font_size = (
float(final_cell_style.get("font-size", f"{FONT_SIZE_PT}pt")[:-2])
/ FONT_SIZE_PT
* FONT_SIZE
float(final_cell_style.get("font-size", f"{self.font_size_pt}pt")[:-2])
/ self.font_size_pt
* self.font_size_px
)
if p_tags:
@@ -523,10 +540,7 @@ class Scheduler:
"""
text_lines = [str(p) for p in p_tags]
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
text_params = {
"font-size": font_size,
"font-family": FONT_FAMILY,
}
text_params = {"font-size": font_size}
if bold:
text_params["font-weight"] = "bold"
if italic:
@@ -546,13 +560,13 @@ class Scheduler:
text_tag = self.svg.text("", **(text_params | {"font-size": "0"} | box_alignment_params))
# TODO: should be done in less naive way
# without using magic number for FONT_WIDTH
# currently it might not work for all fonts and font sizes
# TODO: Should be done without using magic number for self.font_width
if wrap_text:
wrapped_lines = []
for line in text_lines:
wrapped_line = wrap(line, width=int(cell_width // FONT_WIDTH(font_size)), break_long_words=False)
wrapped_line = wrap(
line, width=int(cell_width // (font_size * self.font_width)), break_long_words=False
)
wrapped_lines.extend(wrapped_line)
else:
wrapped_lines = text_lines
@@ -41,7 +41,7 @@ class SheetBuilder:
self.data_dir = None
self.scale = "NTS"
def create(self, layout_path, titleblock_name):
def create(self, layout_path: str, titleblock_name: str) -> None:
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
@@ -76,7 +76,12 @@ class SheetBuilder:
with open(layout_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
def add_drawing(self, reference, drawing, sheet):
def add_drawing(
self,
reference: ifcopenshell.entity_instance,
drawing: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
filename = drawing.Name
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
layout_dir = os.path.dirname(layout_path)
@@ -131,7 +136,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def update_sheet_drawing_sizes(self, sheet):
def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -171,7 +176,7 @@ class SheetBuilder:
layout_tree.write(layout_path)
def remove_drawing(self, reference, sheet):
def remove_drawing(self, reference: ifcopenshell.entity_instance, sheet: ifcopenshell.entity_instance) -> None:
ET.register_namespace("", "http://www.w3.org/2000/svg")
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -187,7 +192,12 @@ class SheetBuilder:
layout_tree.write(layout_path)
def add_document(self, reference, document, sheet):
def add_document(
self,
reference: ifcopenshell.entity_instance,
document: ifcopenshell.entity_instance,
sheet: ifcopenshell.entity_instance,
) -> None:
view_path = tool.Drawing.get_path_with_ext(tool.Drawing.get_document_uri(document), "svg")
if not os.path.exists(view_path):
tool.Drawing.create_svg_document(document)
@@ -224,7 +234,7 @@ class SheetBuilder:
)
layout_tree.write(layout_path)
def add_view_title(self, x, y, parent, layout_dir):
def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None:
title_path = os.path.join(layout_dir, "assets", "view-title.svg")
os.makedirs(os.path.dirname(title_path), exist_ok=True)
if not os.path.exists(title_path):
@@ -241,7 +251,7 @@ class SheetBuilder:
title.attrib["width"] = str(self.convert_to_mm(title_root.attrib.get("width")))
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet):
def build(self, sheet: ifcopenshell.entity_instance) -> dict:
self.references = {"SHEET": None, "RASTER": []}
layout_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -272,7 +282,7 @@ class SheetBuilder:
return self.references
def build_titleblock(self, root, sheet):
def build_titleblock(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
titleblock = root.findall('{http://www.w3.org/2000/svg}g[@data-type="titleblock"]')[0]
image = titleblock.findall("{http://www.w3.org/2000/svg}image")[0]
g = self.parse_embedded_svg(image, sheet.get_info())
@@ -285,7 +295,7 @@ class SheetBuilder:
titleblock.append(g)
titleblock.remove(image)
def ensure_drawing_unique_styles(self, svg, drawing_id):
def ensure_drawing_unique_styles(self, svg: ET.Element, drawing_id: int) -> ET.Element:
"""ensures all drawing's classes and ids will be unique for the whole sheet
by adding `drawing_id` based prefix
"""
@@ -313,7 +323,7 @@ class SheetBuilder:
brackets_level -= 1
text += l
def replace_urls(text):
def replace_urls(text: str) -> str:
"""replace urls `url(#marker)` with `url(#prefix-marker)`
since `url(#marker.prefix)` doesn't seem to work
"""
@@ -343,7 +353,7 @@ class SheetBuilder:
return svg
def build_drawings(self, root, sheet):
def build_drawings(self, root: ET.Element, sheet: ifcopenshell.entity_instance):
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
drawing_id = int(view.attrib["data-id"])
try:
@@ -390,7 +400,7 @@ class SheetBuilder:
for image in images:
view.remove(image)
def build_documents(self, root, sheet):
def build_documents(self, root: ET.Element, sheet: ifcopenshell.entity_instance) -> None:
schedules = root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]')
references = root.findall('{http://www.w3.org/2000/svg}g[@data-type="reference"]')
documents = schedules + references
@@ -427,10 +437,10 @@ class SheetBuilder:
for image in images:
view.remove(image)
def get_href(self, element):
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace('\\','/')
def get_href(self, element: ET.Element) -> str:
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href")).replace("\\", "/")
def parse_embedded_svg(self, image, data):
def parse_embedded_svg(self, image: ET.Element, data: dict) -> ET.Element:
group = ET.Element("g")
group.attrib["transform"] = "translate({},{})".format(
self.convert_to_mm(image.attrib.get("x")), self.convert_to_mm(image.attrib.get("y"))
@@ -466,7 +476,7 @@ class SheetBuilder:
group.append(child)
return group
def change_titleblock(self, sheet, titleblock_name):
def change_titleblock(self, sheet: ifcopenshell.entity_instance, titleblock_name: str) -> None:
ootb_titleblock_path = os.path.join(self.data_dir, "templates", "titleblocks", titleblock_name + ".svg")
titleblock_path = tool.Drawing.get_default_titleblock_path(titleblock_name)
sheet_path = tool.Drawing.get_document_uri(sheet, "LAYOUT")
@@ -499,7 +509,7 @@ class SheetBuilder:
sheet_tree.write(sheet_path)
def convert_to_mm(self, value):
def convert_to_mm(self, value: str) -> float:
# CSS is what defines these possibilities
# https://www.w3.org/TR/SVG/refs.html#ref-css-values-3
# https://www.w3.org/TR/css-values-3/#absolute-lengths
@@ -520,5 +530,5 @@ class SheetBuilder:
return float(value[0:-2]) * (1 / 96) * 2.54 * 10
return float(value)
def mm_to_px(self, value):
def mm_to_px(self, value: float) -> float:
return (value / 25.4) * 96
@@ -22,7 +22,6 @@ import bpy
import math
import bmesh
import shutil
import pystache
import mathutils
import xml.etree.ElementTree as ET
import svgwrite
@@ -719,14 +718,15 @@ class SvgWriter:
self.svg.text(sheet_id, insert=(text_position[0], text_position[1] + 2.5), class_="ELEVATION", **text_style)
)
def get_reference_and_sheet_id_from_annotation(self, element):
def get_reference_and_sheet_id_from_annotation(self, element: ifcopenshell.entity_instance) -> tuple[str, str]:
reference_id = "-"
sheet_id = "-"
drawing = tool.Drawing.get_annotation_element(element)
reference = tool.Drawing.get_drawing_reference(drawing)
if reference:
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
if sheet_reference.Description != "DRAWING" or sheet_reference.Location != reference.Location:
reference_description = tool.Drawing.get_reference_description(sheet_reference)
if reference_description != "DRAWING" or sheet_reference.Location != reference.Location:
continue
sheet = tool.Drawing.get_reference_document(sheet_reference)
if sheet:
@@ -22,13 +22,20 @@ from . import ui, prop, operator
classes = (
operator.AddRepresentation,
operator.CopyRepresentation,
operator.DisableEditingRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemStyle,
operator.DisableEditingRepresentationItems,
operator.DuplicateLinkedAggregateTo3dCursor,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
operator.EditObjectPlacement,
operator.EditRepresentationItemShapeAspect,
operator.EditRepresentationItemStyle,
operator.EnableEditingRepresentationItemShapeAspect,
operator.EnableEditingRepresentationItemStyle,
operator.EnableEditingRepresentationItems,
operator.FlipObject,
operator.GetRepresentationIfcParameters,
operator.DuplicateMoveLinkedAggregate,
operator.DuplicateMoveLinkedAggregateMacro,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
@@ -45,19 +52,13 @@ classes = (
operator.RefreshLinkedAggregate,
operator.RemoveConnection,
operator.RemoveRepresentation,
operator.RemoveRepresentationItem,
operator.RemoveRepresentationItemFromShapeAspect,
operator.SelectConnection,
operator.SwitchRepresentation,
operator.UnassignRepresentationItemStyle,
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
operator.RemoveRepresentationItem,
operator.EnableEditingRepresentationItemStyle,
operator.EditRepresentationItemStyle,
operator.DisableEditingRepresentationItemStyle,
operator.UnassignRepresentationItemStyle,
operator.EnableEditingRepresentationItemShapeAspect,
operator.EditRepresentationItemShapeAspect,
operator.DisableEditingRepresentationItemShapeAspect,
operator.RemoveRepresentationItemFromShapeAspect,
prop.RepresentationItem,
prop.ShapeAspect,
prop.BIMObjectGeometryProperties,
@@ -119,6 +120,7 @@ def register():
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
kmi.properties.should_save = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("wm.call_menu", "P", "PRESS")
kmi.properties.name = ui.BIM_MT_hotkey_separate.bl_idname
@@ -17,6 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell.util.element
import blenderbim.tool as tool
import ifcopenshell.util.placement
from mathutils import Vector
@@ -222,7 +223,7 @@ class ConnectionsData:
@classmethod
def is_connection_realization(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
connections = element.IsConnectionRealization
connections = getattr(element, "IsConnectionRealization", None)
if not connections:
return
@@ -327,12 +328,17 @@ class PlacementData:
@classmethod
def load(cls):
cls.data = {
"has_placement": cls.has_placement(),
"original_x": cls.original_x(),
"original_y": cls.original_y(),
"original_z": cls.original_z(),
}
cls.data = {"has_placement": cls.has_placement()}
props = bpy.context.scene.BIMGeoreferenceProperties
obj = bpy.context.active_object
if obj and props.has_blender_offset:
xyz = cls.original_xyz(obj)
cls.data.update({
"original_x": str(xyz[0]),
"original_y": str(xyz[1]),
"original_z": str(xyz[2]),
})
cls.is_loaded = True
@classmethod
@@ -343,40 +349,18 @@ class PlacementData:
return False
@classmethod
def original_x(cls):
def original_xyz(cls, obj):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props = bpy.context.scene.BIMGeoreferenceProperties
obj = bpy.context.active_object
if not obj or not props.has_blender_offset:
return
return str(round(cls.original_xyz(obj.location)[0], 3))
@classmethod
def original_y(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
obj = bpy.context.active_object
if not obj or not props.has_blender_offset:
return
return str(round(cls.original_xyz(obj.location)[1], 3))
@classmethod
def original_z(cls):
props = bpy.context.scene.BIMGeoreferenceProperties
obj = bpy.context.active_object
if not obj or not props.has_blender_offset:
return
return str(round(cls.original_xyz(obj.location)[2], 3))
@classmethod
def original_xyz(cls, location):
props = bpy.context.scene.BIMGeoreferenceProperties
return ifcopenshell.util.geolocation.xyz2enh(
location[0],
location[1],
location[2],
float(props.blender_eastings),
float(props.blender_northings),
float(props.blender_orthogonal_height),
xyz = ifcopenshell.util.geolocation.xyz2enh(
obj.matrix_world[0][3],
obj.matrix_world[1][3],
obj.matrix_world[2][3],
float(props.blender_eastings) * unit_scale,
float(props.blender_northings) * unit_scale,
float(props.blender_orthogonal_height) / unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
1.0,
)
return [round(o, 3) / unit_scale for o in xyz] # To nearest mm of precision
@@ -538,7 +538,7 @@ class Helper:
)
position = None
if self.file.schema == "IFC2X3":
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0, 0.0]))
position = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint([0.0, 0.0]))
curve = self.file.createIfcRectangleProfileDef("AREA", None, position, xdim, ydim)
return {"curve_ucs": curve_ucs, "curve": curve}
@@ -28,6 +28,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.placement
import ifcopenshell.api
import blenderbim.core.geometry
import blenderbim.core.geometry as core
import blenderbim.core.aggregate
import blenderbim.core.style
@@ -39,6 +40,7 @@ from mathutils import Vector, Matrix
from time import time
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.util.shape_builder import ShapeBuilder
from typing import Any
class Operator:
@@ -136,19 +138,23 @@ class AddRepresentation(bpy.types.Operator, Operator):
"for Profile - 2D bounding box by local XZ axes.\n"
"For other contexts - bounding box is 3d.",
),
("PROJECT", "Full Representation", ""),
("OBJECT", "From Object", "Copies geometry from another object"),
("PROJECT", "Full Representation", "Reuses the current representation"),
],
name="Representation Conversion Method",
)
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
ifc_context = int(props.contexts or "0") or None
props = context.scene.BIMGeometryProperties
oprops = obj.BIMGeometryProperties
ifc_context = int(oprops.contexts or "0") or None
if not ifc_context:
return
ifc_context = tool.Ifc.get().by_id(ifc_context)
original_data = obj.data
if self.representation_conversion_method == "OUTLINE":
if ifc_context.ContextType == "Plan":
data = tool.Geometry.generate_outline_mesh(obj, axis="+Z")
@@ -165,17 +171,31 @@ class AddRepresentation(bpy.types.Operator, Operator):
else:
data = tool.Geometry.generate_3d_box_mesh(obj)
tool.Geometry.change_object_data(obj, data, is_global=True)
elif (
self.representation_conversion_method == "OBJECT"
and props.representation_from_object
and props.representation_from_object.data
):
data = tool.Geometry.duplicate_object_data(props.representation_from_object)
tool.Geometry.change_object_data(obj, data, is_global=True)
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
try:
core.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=ifc_context,
ifc_representation_class=None,
profile_set_usage=None,
)
except core.IncompatibleRepresentationError:
if obj.data != original_data:
tool.Geometry.change_object_data(obj, original_data, is_global=True)
bpy.data.meshes.remove(data)
self.report({"ERROR"}, "No compatible representation for the context could be created.")
return {"CANCELLED"}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -183,6 +203,9 @@ class AddRepresentation(bpy.types.Operator, Operator):
def draw(self, context):
row = self.layout.row()
row.prop(self, "representation_conversion_method", text="")
if self.representation_conversion_method == "OBJECT":
row = self.layout.row()
row.prop(context.scene.BIMGeometryProperties, "representation_from_object", text="")
class SelectConnection(bpy.types.Operator, Operator):
@@ -215,6 +238,13 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
disable_opening_subtractions: bpy.props.BoolProperty()
should_switch_all_meshes: bpy.props.BoolProperty()
@classmethod
def poll(cls, context):
if context.active_object.mode == "OBJECT":
return True
cls.poll_message_set("Only available in OBJECT mode - Press TAB in the viewport")
return False
def _execute(self, context):
target_representation = tool.Ifc.get().by_id(self.ifc_definition_id)
target = target_representation.ContextOfItems
@@ -223,6 +253,8 @@ class SwitchRepresentation(bpy.types.Operator, Operator):
element = tool.Ifc.get_entity(obj)
if not element:
continue
if not obj.mode == "OBJECT":
continue
if obj == context.active_object:
representation = target_representation
else:
@@ -342,14 +374,12 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
gprop = context.scene.BIMGeoreferenceProperties
coordinate_offset = None
if gprop.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT":
coordinate_offset = Vector(
(
float(gprop.blender_eastings),
float(gprop.blender_northings),
float(gprop.blender_orthogonal_height),
)
)
if (
gprop.has_blender_offset
and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT"
and obj.BIMObjectProperties.cartesian_point_offset
):
coordinate_offset = Vector(map(float, obj.BIMObjectProperties.cartesian_point_offset.split(",")))
representation_data = {
"context": context_of_items,
@@ -370,7 +400,11 @@ class UpdateRepresentation(bpy.types.Operator, Operator):
representation_data["profile_set_usage"] = tool.Geometry.get_profile_set_usage(product)
representation_data["text_literal"] = tool.Geometry.get_text_literal(old_representation)
# TODO: replace with core.add_representation?
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
if new_representation is None:
self.report({"ERROR"}, "Error creating representation for Blender object.")
return {"CANCELLED"}
if tool.Geometry.is_body_representation(new_representation):
[
@@ -538,6 +572,8 @@ class OverrideDelete(bpy.types.Operator):
row.prop(self, "is_batch", text="Enable Faster Deletion")
def _execute(self, context):
start_time = time()
if self.is_batch:
ifcopenshell.util.element.batch_remove_deep2(tool.Ifc.get())
@@ -562,6 +598,11 @@ class OverrideDelete(bpy.types.Operator):
IfcStore.add_transaction_operation(self)
# Required otherwise gizmos are still visible
context.view_layer.objects.active = None
operator_time = time() - start_time
if operator_time > 10:
self.report({"INFO"}, "IFC Delete was finished in {:.2f} seconds".format(operator_time))
return {"FINISHED"}
def rollback(self, data):
@@ -672,6 +713,10 @@ class OverrideOutlinerDelete(bpy.types.Operator):
else:
bpy.data.objects.remove(obj)
for collection in collections_to_delete:
# Removing an aggregate object would also remove it's collection
# making the collection data-block invalid.
if not tool.Blender.is_valid_data_block(collection):
continue
bpy.data.collections.remove(collection)
if self.is_batch:
old_file = tool.Ifc.get()
@@ -683,7 +728,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
IfcStore.add_transaction_operation(self)
return {"FINISHED"}
def get_collection_objects_and_children(self, collection):
def get_collection_objects_and_children(self, collection: bpy.types.Collection) -> dict[str, Any]:
objects = set()
children = set()
queue = [collection]
@@ -843,7 +888,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Recreate decompositions
tool.Root.recreate_decompositions(decomposition_relationships, old_to_new)
OverrideDuplicateMove.handle_linked_aggregates(old_to_new)
OverrideDuplicateMove.remove_linked_aggregate_data(old_to_new)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@@ -896,25 +941,21 @@ class OverrideDuplicateMove(bpy.types.Operator):
if entity in old_to_new.keys():
core.remove_connection(tool.Geometry, connection=connection)
@staticmethod
def handle_linked_aggregates(old_to_new):
def remove_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
old_aggregate = ifcopenshell.util.element.get_aggregate(old)
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if old_aggregate == new_aggregate:
parts = ifcopenshell.util.element.get_parts(new_aggregate)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index += 1
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Index": index},
)
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0], pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(new[0], "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], products=[new[0]])
class OverrideDuplicateMoveLinkedMacro(bpy.types.Macro):
@@ -940,14 +981,15 @@ class OverrideDuplicateMoveLinked(bpy.types.Operator):
class DuplicateMoveLinkedAggregateMacro(bpy.types.Macro):
bl_description = "Create and move a new linked aggregate"
bl_idname = "bim.object_duplicate_move_linked_aggregate_macro"
bl_label = "IFC Duplicate Linked Aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
class DuplicateMoveLinkedAggregate(bpy.types.Operator):
bl_idname = "bim.object_duplicate_move_linked_aggregate"
bl_label = "IFC Duplicate Linked Aggregate"
bl_label = "IFC Duplicate and Move Linked Aggregate"
bl_options = {"REGISTER", "UNDO"}
is_interactive: bpy.props.BoolProperty(name="Is Interactive", default=True)
@@ -962,7 +1004,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
@staticmethod
def execute_ifc_duplicate_linked_aggregate_operator(self, context):
def execute_ifc_duplicate_linked_aggregate_operator(self, context, location_from_3d_cursor=False):
self.new_active_obj = None
self.group_name = "BBIM_Linked_Aggregate"
self.pset_name = "BBIM_Linked_Aggregate"
@@ -974,15 +1016,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
obj.select_set(True)
parts = ifcopenshell.util.element.get_parts(element)
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index = get_max_index(parts)
add_linked_aggregate_pset(element, index)
index += 1
for part in parts:
if part.is_a("IfcElementAssembly"):
select_objects_and_add_data(part)
else:
add_linked_aggregate_pset(part, index)
index += 1
index = add_linked_aggregate_pset(part, index)
# index += 1
obj = tool.Ifc.get_object(part)
obj.select_set(True)
@@ -999,6 +1041,8 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
pset=pset,
properties={"Index": index},
)
index += 1
else:
pass
@@ -1014,10 +1058,8 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if self.group_name in product_groups_name:
return
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name)
ifcopenshell.api.run(
"group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group
)
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.group_name)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
for new in old_to_new.values():
@@ -1042,6 +1084,49 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
split_name = new_obj.name.split(".")
new_obj.name = split_name[0] + "_" + number
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
def copy_linked_aggregate_data(old_to_new):
for old, new in old_to_new.items():
pset = ifcopenshell.util.element.get_pset(old, "BBIM_Linked_Aggregate")
if pset:
new_pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=new[0], name=self.pset_name
)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=new_pset,
properties={"Index": pset["Index"]},
)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(old, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.assign_group", group=linked_aggregate_group[0], products=new)
def get_location_from_3d_cursor(old_to_new, aggregate):
base_obj = tool.Ifc.get_object(aggregate)
base_obj_location = base_obj.location.copy()
for new in old_to_new.values():
new_obj = tool.Ifc.get_object(new[0])
location_diff = new_obj.location - base_obj_location
new_obj.location = context.scene.cursor.location + location_diff
if len(context.selected_objects) != 1:
return {"FINISHED"}
@@ -1062,26 +1147,36 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
tool.Root.recreate_aggregate(old_to_new)
copy_linked_aggregate_data(old_to_new)
custom_incremental_naming_for_element_assembly(old_to_new)
# Recreate aggregate relationship
for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"):
tool.Root.recreate_aggregate(old_to_new)
if location_from_3d_cursor:
get_location_from_3d_cursor(old_to_new, selected_element)
blenderbim.bim.handler.refresh_ui_data()
return old_to_new
@staticmethod
def get_max_index(parts):
psets = [ifcopenshell.util.element.get_pset(p, "BBIM_Linked_Aggregate") for p in parts]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
class DuplicateLinkedAggregateTo3dCursor(bpy.types.Operator):
bl_idname = "bim.duplicate_linked_aggregate_to_3d_cursor"
bl_label = "IFC Duplicate Linked Aggregate to 3d Cursor"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 0
def execute(self, context):
return OverrideDuplicateMove.execute_duplicate_operator(self, context, linked=False)
def _execute(self, context):
return DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(
self, context, location_from_3d_cursor=True
)
class RefreshLinkedAggregate(bpy.types.Operator):
@@ -1216,6 +1311,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
return list(set(linked_aggregate_groups)), selected_parents
def get_original_matrix(element, base_instance):
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
return selected_matrix, duplicate_matrix
def set_new_matrix(selected_matrix, duplicate_matrix, old_to_new):
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
active_element = tool.Ifc.get_entity(context.active_object)
if not active_element:
self.report({"INFO"}, "Object has no Ifc metadata.")
@@ -1255,10 +1366,7 @@ class RefreshLinkedAggregate(bpy.types.Operator):
element_aggregate = ifcopenshell.util.element.get_aggregate(element)
selected_obj = tool.Ifc.get_object(base_instance)
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
selected_matrix, duplicate_matrix = get_original_matrix(element, base_instance)
original_names = get_original_names(element)
@@ -1269,15 +1377,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
tool.Ifc.get_object(base_instance).select_set(True)
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(
self, context
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
new_base_matrix = Matrix.LocRotScale(*duplicate_matrix)
matrix_diff = Matrix.inverted(selected_matrix) @ new_obj.matrix_world
new_obj_matrix = new_base_matrix @ matrix_diff
new_obj.matrix_world = new_obj_matrix
old_to_new = DuplicateMoveLinkedAggregate.execute_ifc_duplicate_linked_aggregate_operator(self, context)
set_new_matrix(selected_matrix, duplicate_matrix, old_to_new)
for old, new in old_to_new.items():
if element_aggregate and new[0].is_a("IfcElementAssembly"):
@@ -1409,14 +1511,13 @@ class OverridePasteBuffer(bpy.types.Operator):
def execute(self, context):
bpy.ops.view3d.pastebuffer()
if IfcStore.get_file():
for obj in context.selected_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
for obj in context.selected_objects:
# Pasted objects may come from another Blender session, or even
# from the same session where the original object has since
# been deleted. As the source element may not exist, paste will
# always unlink the element. If you want to duplicate an
# element, use the duplicate commands.
tool.Root.unlink_object(obj)
return {"FINISHED"}
@@ -1589,20 +1690,11 @@ class OverrideModeSetObject(bpy.types.Operator):
apply_openings=True,
)
def draw(self, context):
if self.is_valid:
row = self.layout.row()
row.prop(self, "should_save")
else:
row = self.layout.row()
row.label(text="No Geometry Found: Object will revert to previous state.")
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
def _invoke(self, context, event):
self.is_valid = True
self.should_save = True
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
@@ -1664,8 +1756,6 @@ class OverrideModeSetObject(bpy.types.Operator):
else:
tool.Ifc.finish_edit(obj)
if self.edited_objs:
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
@@ -138,3 +138,4 @@ class BIMGeometryProperties(PropertyGroup):
name="IFC Interaction Mode",
update=update_mode,
)
representation_from_object: PointerProperty(type=bpy.types.Object)
@@ -34,6 +34,8 @@ from blenderbim.bim.module.layer.data import LayersData
def mode_menu(self, context):
row = self.layout.row(align=True)
if context.scene.BIMGeometryProperties.mode == "EDIT":
row.operator("bim.override_mode_set_object", icon="CANCEL", text="Discard Changes").should_save = False
row.prop(context.scene.BIMGeometryProperties, "mode", text="", icon_value=blenderbim.bim.icons["IFC"].icon_id)
@@ -31,7 +31,6 @@ classes = (
operator.SelectGroupProducts,
operator.ToggleGroup,
operator.UnassignGroup,
operator.UpdateGroup,
operator.SelectGroupElements,
prop.ExpandedGroups,
prop.Group,
@@ -22,7 +22,6 @@ import ifcopenshell.api
import blenderbim.bim.helper
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.util.selector import Selector
import json
@@ -48,7 +47,6 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
new = self.props.groups.add()
new.ifc_definition_id = group.id()
new.name = group.Name or "Unnamed"
new.selection_query = ""
new.tree_depth = tree_depth
new.has_children = False
new.is_expanded = group.id() in self.expanded_groups
@@ -113,7 +111,6 @@ class EditGroup(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_group"
bl_label = "Edit Group"
bl_options = {"REGISTER", "UNDO"}
copy_from_selector: bpy.props.BoolProperty(name="Copy from Selector", default=False)
def _execute(self, context):
props = context.scene.BIMGroupProperties
@@ -124,9 +121,6 @@ class EditGroup(bpy.types.Operator, tool.Ifc.Operator):
else:
attributes[attribute.name] = attribute.string_value
if self.copy_from_selector:
attributes["Description"] = context.scene.IFCSelector.selection_query
self.file = IfcStore.get_file()
ifcopenshell.api.run(
"group.edit_group", self.file, **{"group": self.file.by_id(props.active_group_id), "attributes": attributes}
@@ -242,31 +236,6 @@ class SelectGroupProducts(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
class UpdateGroup(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_group"
bl_label = "Update Group"
bl_options = {"REGISTER", "UNDO"}
query: bpy.props.StringProperty()
group_id: bpy.props.IntProperty()
def _execute(self, context):
self.file = IfcStore.get_file()
group = self.file.by_id(self.group_id)
query = self.query
new_products = Selector.parse(self.file, query)
ifcopenshell.api.run(
"group.update_group_products",
self.file,
**{
"group": group,
"products": new_products,
}
)
bpy.ops.bim.load_groups()
return {"FINISHED"}
class SelectGroupElements(bpy.types.Operator):
bl_idname = "bim.select_group_elements"
bl_label = "Select Group elements"
@@ -44,7 +44,6 @@ class ExpandedGroups(StrProperty):
class Group(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
selection_query: StringProperty(name="Selection Query")
is_expanded: BoolProperty(name="Is Expanded", default=False)
has_children: BoolProperty(name="Has Children", default=False)
tree_depth: IntProperty(name="Tree Depth")
@@ -131,7 +131,7 @@ class BIM_UL_groups(UIList):
else:
row.label(text="", icon="BLANK1")
row.label(text=f"*{item.name}") if item.selection_query != "" else row.label(text=item.name)
row.label(text=item.name)
group_id = item.ifc_definition_id
if context.scene.BIMGroupProperties.active_group_id == group_id:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
@@ -145,10 +145,6 @@ class BIM_UL_groups(UIList):
op.group = group_id
op = row.operator("bim.remove_group", text="", icon="X")
op.group = group_id
if item.selection_query != "":
op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
op.group_id = item.ifc_definition_id
op.query = item.selection_query
else:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
op.group = group_id
@@ -158,10 +154,6 @@ class BIM_UL_groups(UIList):
op.group = group_id
op = row.operator("bim.remove_group", text="", icon="X")
op.group = group_id
if item.selection_query != "":
op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
op.group_id = item.ifc_definition_id
op.query = item.selection_query
class BIM_UL_object_groups(UIList):
@@ -247,9 +247,11 @@ class IFCGIT_PT_revision_inspector(bpy.types.Panel):
bl_idname = "IFCGIT_PT_revision_inspector"
bl_label = "Git History"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "IFCGIT_PT_panel"
def draw(self, context):
@@ -24,12 +24,12 @@ classes = (
operator.AddLayer,
operator.AddListItem,
operator.AddMaterial,
operator.DuplicateMaterial,
operator.AddMaterialSet,
operator.AddProfile,
operator.AssignMaterial,
operator.AssignParameterizedProfile,
operator.ContractMaterialCategory,
operator.CopyMaterial,
operator.DisableEditingAssignedMaterial,
operator.DisableEditingMaterial,
operator.DisableEditingMaterialSetItem,
@@ -63,7 +63,6 @@ classes = (
prop.BIMMaterialProperties,
prop.BIMObjectMaterialProperties,
ui.BIM_PT_materials,
ui.BIM_PT_material,
ui.BIM_PT_object_material,
ui.BIM_UL_materials,
)
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.doc
import ifcopenshell.util.schema
import blenderbim.tool as tool
@@ -35,31 +36,17 @@ class MaterialsData:
@classmethod
def load(cls):
cls.data = {
"total_materials": cls.total_materials(),
"material_types": cls.material_types(),
"profiles": cls.profiles(),
"styles": cls.styles(),
"contexts": cls.contexts(),
"active_styles": cls.active_styles(),
}
cls.is_loaded = True
cls.data["material_types"] = cls.material_types()
cls.data["total_materials"] = cls.total_materials()
cls.data["profiles"] = cls.profiles()
cls.data["styles"] = cls.styles()
cls.data["contexts"] = cls.contexts()
cls.data["active_styles"] = cls.active_styles()
@classmethod
def total_materials(cls):
if tool.Ifc.get_schema() == "IFC2X3":
return (
len(tool.Ifc.get().by_type("IfcMaterial"))
+ len(tool.Ifc.get().by_type("IfcMaterialLayerSet"))
+ len(tool.Ifc.get().by_type("IfcMaterialList"))
)
return (
len(tool.Ifc.get().by_type("IfcMaterial"))
+ len(tool.Ifc.get().by_type("IfcMaterialConstituentSet"))
+ len(tool.Ifc.get().by_type("IfcMaterialLayerSet"))
+ len(tool.Ifc.get().by_type("IfcMaterialProfileSet"))
+ len(tool.Ifc.get().by_type("IfcMaterialList"))
)
return len(tool.Ifc.get().by_type(bpy.context.scene.BIMMaterialProperties.material_type))
@classmethod
def material_types(cls):
@@ -166,6 +153,9 @@ class ObjectMaterialData:
cls.data["type_material"] = cls.type_material()
cls.data["material_type"] = cls.material_type()
cls.data["active_material_constituents"] = cls.active_material_constituents()
# after material_name and type_material
cls.data["is_type_material_overridden"] = cls.is_type_material_overridden()
cls.is_loaded = True
@classmethod
@@ -294,8 +284,7 @@ class ObjectMaterialData:
@classmethod
def material_name(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
material = ifcopenshell.util.element.get_material(element)
material = cls.material
if material:
return getattr(material, "Name", None) or "Unnamed"
@@ -339,3 +328,18 @@ class ObjectMaterialData:
if not cls.material or not material.is_a("IfcMaterialConstituentSet"):
return []
return [m.Name for m in material.MaterialConstituents if m.Name]
@classmethod
def is_type_material_overridden(cls) -> bool:
if not cls.data["type_material"]:
return False
# try to avoid accessing ifc
if cls.data["material_name"] != cls.data["type_material"]:
return True
# in theory material can be overridden by the same material
# so we check occurrence material explicitly
element = tool.Ifc.get_entity(bpy.context.active_object)
occurrence_material = ifcopenshell.util.element.get_material(element, should_inherit=False)
return bool(occurrence_material)
@@ -120,6 +120,8 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(name="Material Name")
name: bpy.props.StringProperty(default="Default")
category: bpy.props.StringProperty(default="")
description: bpy.props.StringProperty(default="")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -127,13 +129,41 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
def draw(self, context):
row = self.layout
row.prop(self, "name", text="Name")
row = self.layout
row.prop(self, "description", text="Description")
row = self.layout
row.prop(self, "category", text="Category")
def _execute(self, context):
obj = bpy.data.materials.get(self.obj) if self.obj else None
core.add_material(tool.Ifc, tool.Material, tool.Style, obj=obj, name=self.name)
core.add_material(tool.Ifc, tool.Material, tool.Style, obj=obj, name=self.name, category=self.category, description=self.description)
material_prop_purge()
class DuplicateMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_material"
bl_label = "Diplicate Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material ID")
def _execute(self, context):
material = tool.Ifc.get().by_id(self.material)
new = tool.Ifc.run("material.copy_material", material=material)
blender_material = tool.Ifc.get_object(material)
new_blender = blender_material.copy()
new_blender.use_fake_user = True
tool.Ifc.link(new, new_blender)
if not new.is_a("IfcMaterialList"):
name = new[0] + " Copy"
new[0] = name
new_blender.name = name
material_prop_purge()
bpy.ops.bim.load_materials()
class AddMaterialSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material_set"
bl_label = "Add Material Set"
@@ -172,9 +202,10 @@ class UnlinkMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unlink_material"
bl_label = "Unlink Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty(name="Material ID")
def _execute(self, context):
core.unlink_material(tool.Ifc, obj=context.active_object.active_material)
core.unlink_material(tool.Ifc, obj=tool.Ifc.get_object(tool.Ifc.get().by_id(self.material)))
class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator):
@@ -531,6 +562,7 @@ class EnableEditingMaterialSetItemProfile(bpy.types.Operator):
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.props = obj.BIMObjectMaterialProperties
self.props.active_material_set_item_id = self.material_set_item
self.props.material_set_item_profile_attributes.clear()
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
blenderbim.bim.helper.import_attributes2(profile, self.props.material_set_item_profile_attributes)
@@ -546,6 +578,7 @@ class DisableEditingMaterialSetItemProfile(bpy.types.Operator):
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.props = obj.BIMObjectMaterialProperties
self.props.active_material_set_item_id = 0
self.props.material_set_item_profile_attributes.clear()
return {"FINISHED"}
@@ -563,6 +596,7 @@ class EditMaterialSetItemProfile(bpy.types.Operator, tool.Ifc.Operator):
attributes = blenderbim.bim.helper.export_attributes(self.props.material_set_item_profile_attributes)
profile = tool.Ifc.get().by_id(self.material_set_item).Profile
ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes)
self.props.active_material_set_item_id = 0
self.props.material_set_item_profile_attributes.clear()
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile)
@@ -666,45 +700,31 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.disable_editing_material_set_item(obj=obj.name)
class CopyMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.copy_material"
bl_label = "Copy Material"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
blender_material = context.active_object.active_material
material = tool.Ifc.get_entity(blender_material)
if tool.Ifc.has_changed_shading(blender_material):
blenderbim.core.style.update_style_colours(tool.Ifc, tool.Style, obj=blender_material)
copied_material = ifcopenshell.api.run("material.copy_material", tool.Ifc.get(), material=material)
copied_blender_material = blender_material.copy()
copied_style = self.get_style(copied_material)
tool.Ifc.link(copied_material, copied_blender_material)
if copied_style:
tool.Ifc.link(copied_style, copied_blender_material)
context.active_object.active_material = copied_blender_material
def get_style(self, material):
for material_representation in material.HasRepresentation:
for representation in material_representation.Representations:
for item in representation.Items:
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
return style
class ExpandMaterialCategory(bpy.types.Operator):
bl_idname = "bim.expand_material_category"
bl_label = "Expand Material Category"
bl_description = "SHIFT+CLICK to expand all material categories"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Expanding all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck.
if event.type == "LEFTMOUSE" and event.shift:
self.expand_all = True
return self.execute(context)
def execute(self, context):
props = context.scene.BIMMaterialProperties
for category in [c for c in props.materials if c.is_category and c.name == self.category]:
for index, category in [
(i, c)
for i, c in enumerate(props.materials)
if c.is_category and (self.expand_all or c.name == self.category)
]:
category.is_expanded = True
if category.name == self.category:
props.active_material_index = index
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
@@ -712,13 +732,28 @@ class ExpandMaterialCategory(bpy.types.Operator):
class ContractMaterialCategory(bpy.types.Operator):
bl_idname = "bim.contract_material_category"
bl_label = "Contract Material Category"
bl_description = "SHIFT+CLICK to contract all material categories"
bl_options = {"REGISTER", "UNDO"}
category: bpy.props.StringProperty()
contract_all: bpy.props.BoolProperty(name="Contract All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# Contracting all categories on shift+click.
# Make sure to use SKIP_SAVE on property, otherwise it might get stuck.
if event.type == "LEFTMOUSE" and event.shift:
self.contract_all = True
return self.execute(context)
def execute(self, context):
props = context.scene.BIMMaterialProperties
for category in [c for c in props.materials if c.is_category and c.name == self.category]:
for index, category in [
(i, c)
for i, c in enumerate(props.materials)
if c.is_category and (self.contract_all or c.name == self.category)
]:
category.is_expanded = False
if category.name == self.category:
props.active_material_index = index
core.load_materials(tool.Material, props.material_type)
return {"FINISHED"}
@@ -97,6 +97,10 @@ def get_material_types(self, context):
return MaterialsData.data["material_types"]
def update_material_type(self, context):
MaterialsData.data["total_materials"] = MaterialsData.total_materials()
def get_profiles(self, context):
if not MaterialsData.is_loaded:
MaterialsData.load()
@@ -119,14 +123,14 @@ class Material(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
is_category: BoolProperty(name="Is Category", default=False)
is_expanded: BoolProperty(name="Is Expanded", default=True)
is_expanded: BoolProperty(name="Is Expanded", default=False)
has_style: BoolProperty(name="Has Style", default=True)
total_elements: IntProperty(name="Total Elements")
class BIMMaterialProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
material_type: EnumProperty(items=get_material_types, name="Material Type")
material_type: EnumProperty(items=get_material_types, update=update_material_type, name="Material Type")
materials: CollectionProperty(name="Materials", type=Material)
active_material_index: IntProperty(name="Active Material Index")
profiles: EnumProperty(items=get_profiles, name="Profiles")
@@ -45,7 +45,7 @@ class BIM_PT_materials(Panel):
self.props = context.scene.BIMMaterialProperties
row = self.layout.row(align=True)
row.label(text="{} Materials".format(MaterialsData.data["total_materials"]), icon="NODE_MATERIAL")
row.label(text=f"{MaterialsData.data['total_materials']} Materials", icon="NODE_MATERIAL")
if self.props.is_editing:
row.operator("bim.disable_editing_materials", text="", icon="CANCEL")
else:
@@ -61,12 +61,16 @@ class BIM_PT_materials(Panel):
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
if material.ifc_definition_id:
op = row.operator("bim.duplicate_material", text="", icon="DUPLICATE")
op.material = material.ifc_definition_id
op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
op.material = material.ifc_definition_id
op = row.operator("bim.enable_editing_material", text="", icon="GREASEPENCIL")
op.material = material.ifc_definition_id
op = row.operator("bim.enable_editing_material_style", text="", icon="SHADING_RENDERED")
op.material = material.ifc_definition_id
op = row.operator("bim.unlink_material", icon="UNLINKED", text="")
op.material = material.ifc_definition_id
row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
self.draw_editing_ui()
@@ -106,42 +110,20 @@ class BIM_PT_materials(Panel):
elif self.props.editing_material_type == "STYLE":
row = self.layout.row(align=True)
row.prop(self.props, "contexts", text="")
row.prop(self.props, "styles", text="")
prop_with_search(row, self.props, "styles", text="")
row = self.layout.row(align=True)
row.operator("bim.edit_material_style", text="Assign Style", icon="CHECKMARK")
row.operator("bim.disable_editing_material", text="", icon="CANCEL")
class BIM_PT_material(Panel):
bl_label = "Material"
bl_idname = "BIM_PT_material"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
@classmethod
def poll(cls, context):
return IfcStore.get_file() and context.active_object and context.active_object.active_material
def draw(self, context):
row = self.layout.row(align=True)
material_id = context.active_object.active_material.BIMObjectProperties.ifc_definition_id
if bool(material_id):
row.operator("bim.remove_material", icon="X", text="Remove IFC Material").material = material_id
row.operator("bim.copy_material", icon="DUPLICATE", text="")
row.operator("bim.unlink_material", icon="UNLINKED", text="")
else:
op = row.operator("bim.add_material", icon="ADD", text="Create IFC Material")
op.obj = context.active_object.active_material.name
class BIM_PT_object_material(Panel):
bl_label = "Object Material"
bl_idname = "BIM_PT_object_material"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_materials"
bl_parent_id = "BIM_PT_tab_object_materials"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
@@ -175,7 +157,13 @@ class BIM_PT_object_material(Panel):
if ObjectMaterialData.data["type_material"]:
row = self.layout.row(align=True)
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF")
if ObjectMaterialData.data["is_type_material_overridden"]:
row.label(
text=f"Inherited Material Is Occurrence Overridden",
icon="CON_CHILDOF",
)
else:
row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF")
if ObjectMaterialData.data["material_class"]:
return self.draw_material_ui()
@@ -240,7 +228,7 @@ class BIM_PT_object_material(Panel):
total_items = len(ObjectMaterialData.data["set_items"])
for index, set_item in enumerate(ObjectMaterialData.data["set_items"]):
if len(self.props.material_set_item_profile_attributes):
if len(self.props.material_set_item_profile_attributes) and self.props.active_material_set_item_id == set_item["id"]:
self.draw_editable_set_item_profile_ui(set_item)
elif self.props.active_material_set_item_id == set_item["id"]:
self.draw_editable_set_item_ui(set_item)
@@ -355,7 +343,7 @@ class BIM_UL_materials(UIList):
row.operator(
"bim.expand_material_category", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
).category = item.name
row.label(text=item.name or "Uncategorised")
row.label(text=item.name)
else:
row.label(text="", icon="BLANK1")
row.label(text=item.name, icon="MATERIAL")
@@ -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"
@@ -22,10 +22,11 @@ import bpy
class BIM_PT_misc_utilities(bpy.types.Panel):
bl_idname = "BIM_PT_misc_utilities"
bl_label = "Miscellaneous"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "output"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "BlenderBIM"
bl_parent_id = "BIM_PT_tab_sandbox"
def draw(self, context):
layout = self.layout
@@ -83,8 +83,17 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props = obj.BIMArrayProperties
relating_obj = props.relating_array_object
if relating_obj:
element = tool.Ifc.get_entity(relating_obj)
parent_globalid = ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Parent")
parent_element = tool.Ifc.get().by_guid(parent_globalid)
data = json.loads(ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array", "Data"))[self.item]
else:
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props.count = data["count"]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props.x = data["x"] * si_conversion
@@ -93,7 +102,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
props.use_local_space = data.get("use_local_space", False)
props.sync_children = data.get("sync_children", False)
props.method = data.get("method", "OFFSET")
props.is_editing = self.item
return {"FINISHED"}
@@ -137,6 +148,10 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
#clears the relating_array_object so it doesn't show again next time
props.relating_array_object = None
return {"FINISHED"}
@@ -193,7 +208,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))
@@ -22,6 +22,7 @@ import ifcopenshell
import blenderbim.tool as tool
import blenderbim.core.covering as core
class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_flooring_covering_from_cursor"
bl_label = "Add Flooring From Cursor"
@@ -30,26 +31,16 @@ class AddInstanceFlooringCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operato
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return relating_type == "FLOORING"
def _execute(self, context):
try:
core.add_instance_flooring_covering_from_cursor(tool.Ifc, tool.Root, tool.Spatial)
except core.NoDefaultContainer:
return self.report({"ERROR"}, "Please set a default container to create the covering in.")
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.add_instance_flooring_covering_from_cursor(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry)
class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_ceiling_covering_from_cursor"
@@ -59,26 +50,16 @@ class AddInstanceCeilingCoveringFromCursor(bpy.types.Operator, tool.Ifc.Operator
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
return tool.Ifc.get_entity(collection_obj)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
return relating_type == "CEILING"
def _execute(self, context):
try:
core.add_instance_ceiling_covering_from_cursor(tool.Ifc, tool.Root, tool.Covering, tool.Spatial)
except core.NoDefaultContainer:
return self.report({"ERROR"}, "Please set a default container to create the covering in.")
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.add_instance_ceiling_covering_from_cursor(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry, tool.Covering)
class RegenSelectedCoveringObject(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.regen_selected_covering_object"
@@ -88,37 +69,31 @@ class RegenSelectedCoveringObject(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
element = tool.Ifc.get_entity(bpy.context.active_object)
return element and element.is_a("IfcCovering")
def _execute(self, context):
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if not element.is_a("IfcCovering"):
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.regen_selected_covering_object(tool.Ifc, tool.Spatial, tool.Model, tool.Type, tool.Geometry)
try:
core.regen_selected_covering_object(tool.Root, tool.Spatial)
except core.NoDefaultContainer:
return self.report({"ERROR"}, "Please set a default container to create the covering in.")
class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_flooring_coverings_from_walls"
bl_label = "Add Flooring From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add instance flooring coverings from selected walls. The active object must be a wall and layered vertically"
bl_description = (
"Add instance flooring coverings from selected walls. The active object must be a wall and layered vertically"
)
@classmethod
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects
element = tool.Ifc.get_entity(bpy.context.active_object)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element and element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects and relating_type == "FLOORING"
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -126,38 +101,27 @@ class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operato
# In order to run, the active object must be a wall and
# there must be selected walls
active_obj = bpy.context.active_object
if not active_obj:
self.report({"ERROR"}, "No active object. Please select a wall")
return
try:
core.add_instance_flooring_coverings_from_walls(tool.Root, tool.Spatial)
except core.NoDefaultContainer:
return self.report({"ERROR"}, "Please set a default container to create the covering in.")
element = tool.Ifc.get_entity(active_obj)
if element and not element.is_a("IfcWall"):
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
container = ifcopenshell.util.element.get_container(element)
if not container:
self.report({"ERROR"}, "The wall is not contained.")
if not bpy.context.selected_objects:
self.report({"ERROR"}, "No selected objects found. Please select walls.")
return
core.add_instance_flooring_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry)
class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_instance_ceiling_coverings_from_walls"
bl_label = "Add Ceilings From Walls"
bl_label = "Add Ceiling From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add instance ceiling coverings from selected walls. The active object must be a wall and layered vertically"
bl_description = (
"Add instance ceiling coverings from selected walls. The active object must be a wall and layered vertically"
)
@classmethod
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
if element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects
element = tool.Ifc.get_entity(bpy.context.active_object)
relating_type_id = int(bpy.data.scenes["Scene"].BIMModelProperties.relating_type_id)
relating_type = ifcopenshell.util.element.get_predefined_type(tool.Ifc.get().by_id(relating_type_id))
if element and element.is_a("IfcWall") and tool.Model.get_usage_type(element) == "LAYER2":
return context.selected_objects and relating_type == "CEILING"
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -165,22 +129,7 @@ class AddInstanceCeilingCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator
# In order to run, the active object must be a wall and
# there must be selected walls
active_obj = bpy.context.active_object
if not active_obj:
self.report({"ERROR"}, "No active object. Please select a wall")
return
element = tool.Ifc.get_entity(active_obj)
if element and not element.is_a("IfcWall"):
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
container = ifcopenshell.util.element.get_container(element)
if not container:
self.report({"ERROR"}, "The wall is not contained.")
if not bpy.context.selected_objects:
self.report({"ERROR"}, "No selected objects found. Please select walls.")
return
core.add_instance_ceiling_coverings_from_walls(tool.Ifc, tool.Spatial, tool.Collector, tool.Geometry, tool.Covering)
try:
core.add_instance_ceiling_coverings_from_walls(tool.Root, tool.Spatial, tool.Covering)
except core.NoDefaultContainer:
return self.report({"ERROR"}, "Please set a default container to create the covering in.")
@@ -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"):
@@ -22,20 +22,24 @@ import bmesh
from bmesh.types import BMVert, BMFace
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.representation
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.tool as tool
import blenderbim.core.geometry
import blenderbim.core.geometry as core
from blenderbim.bim.module.model.window import create_bm_window, create_bm_box, update_simple_openings
import blenderbim.core.root
from blenderbim.bim.module.model.window import create_bm_window, create_bm_box
from mathutils import Vector, Matrix
from pprint import pprint
import json
import collections
def update_door_modifier_representation(context, obj):
def update_door_modifier_representation(context: bpy.types.Context, obj: bpy.types.Object) -> None:
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
@@ -159,7 +163,7 @@ def update_door_modifier_representation(context, obj):
occurrence.OverallWidth = props.overall_width / si_conversion
occurrence.OverallHeight = props.overall_height / si_conversion
update_simple_openings(element, props.overall_width / si_conversion, props.overall_height / si_conversion)
tool.Model.update_simple_openings(element)
# TODO: move it out to tools
@@ -504,8 +508,6 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcDoor")
obj = bpy.data.objects.new("IfcDoor", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcDoor", should_add_representation=False
@@ -644,6 +646,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"}
@@ -28,23 +28,9 @@ def add_object(self, context):
obj = bpy.data.objects.new("Grid", None)
obj.name = "Grid"
collection = context.view_layer.active_layer_collection.collection
site = tool.Ifc.get().by_type("IfcSite")
if site:
site = site[0]
site_obj = tool.Ifc.get_object(site)
if site_obj:
collection = site_obj.BIMObjectProperties.collection or collection
collection.objects.link(obj)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcGrid")
grid = tool.Ifc.get_entity(obj)
collection = obj.BIMObjectProperties.collection or collection
axes_collection = bpy.data.collections.new("UAxes")
collection.children.link(axes_collection)
for i in range(0, self.total_u):
verts = [
Vector((-2, i * self.u_spacing, 0)),
@@ -57,19 +43,13 @@ def add_object(self, context):
tag = chr(ord("A") + i)
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
axes_collection.objects.link(obj)
result = ifcopenshell.api.run(
"grid.create_grid_axis",
tool.Ifc.get(),
**{"axis_tag": tag, "uvw_axes": "UAxes", "grid": grid},
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="UAxes", grid=grid
)
tool.Ifc.link(result, obj)
ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), **{"axis_curve": obj, "grid_axis": result})
obj.BIMObjectProperties.ifc_definition_id = result.id()
ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), axis_curve=obj, grid_axis=result)
tool.Collector.assign(obj)
axes_collection = bpy.data.collections.new("VAxes")
collection.children.link(axes_collection)
for i in range(0, self.total_v):
verts = [
Vector((i * self.v_spacing, -2, 0)),
@@ -82,16 +62,12 @@ def add_object(self, context):
tag = str(i + 1).zfill(2)
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
axes_collection.objects.link(obj)
result = ifcopenshell.api.run(
"grid.create_grid_axis",
tool.Ifc.get(),
**{"axis_tag": tag, "uvw_axes": "VAxes", "grid": grid},
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="VAxes", grid=grid
)
tool.Ifc.link(result, obj)
ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), **{"axis_curve": obj, "grid_axis": result})
obj.BIMObjectProperties.ifc_definition_id = result.id()
ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), axis_curve=obj, grid_axis=result)
tool.Collector.assign(obj)
class BIM_OT_add_object(Operator, tool.Ifc.Operator):
@@ -243,7 +243,6 @@ class MEPGenerator:
def setup_ports(self, obj):
self.file = tool.Ifc.get()
self.collection = bpy.context.view_layer.active_layer_collection.collection
segment = tool.Ifc.get_entity(obj)
representation = ifcopenshell.util.representation.get_representation(segment, "Model", "Body", "MODEL_VIEW")
@@ -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)
@@ -122,44 +130,37 @@ class FilledOpeningGenerator:
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
if existing_opening_occurrence:
opening = ifcopenshell.api.run(
"root.create_entity",
tool.Ifc.get(),
ifc_class="IfcOpeningElement",
predefined_type="OPENING",
name="Opening",
)
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=filling_obj.matrix_world
)
opening = ifcopenshell.api.run(
"root.create_entity",
tool.Ifc.get(),
ifc_class="IfcOpeningElement",
predefined_type="OPENING",
name="Opening",
)
ifcopenshell.api.run(
"geometry.edit_object_placement",
tool.Ifc.get(),
product=opening,
matrix=np.array(filling_obj.matrix_world),
is_si=True,
)
if existing_opening_occurrence:
representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
representation = ifcopenshell.util.representation.resolve_representation(representation)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
else:
representation = self.generate_opening_from_filling(filling, filling_obj)
opening = ifcopenshell.api.run(
"root.create_entity", tool.Ifc.get(), ifc_class="IfcOpeningElement", predefined_type="OPENING"
representation = self.generate_opening_from_filling(
filling, filling_obj, opening_thickness_si=opening_thickness_si
)
matrix = np.array(filling_obj.matrix_world)
ifcopenshell.api.run(
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=matrix, is_si=True
)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
mapped_representation = ifcopenshell.api.run(
"geometry.map_representation", tool.Ifc.get(), representation=representation
)
ifcopenshell.api.run(
"geometry.assign_representation", tool.Ifc.get(), product=opening, representation=mapped_representation
)
ifcopenshell.api.run("void.add_opening", tool.Ifc.get(), opening=opening, element=element)
ifcopenshell.api.run("void.add_filling", tool.Ifc.get(), opening=opening, element=filling)
@@ -187,7 +188,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 +253,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())
@@ -285,7 +291,7 @@ class FilledOpeningGenerator:
return tool.Ifc.get().createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
settings = ifcopenshell.geom.settings()
settings.set(settings.INCLUDE_CURVES, True)
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
geometry = ifcopenshell.geom.create_shape(settings, profile)
verts = ifcopenshell.util.shape.get_vertices(geometry)
# [0, 2] represents X and Z ordinates
@@ -346,7 +352,9 @@ class FilledOpeningGenerator:
return True
return False
def get_existing_opening_occurrence_if_any(self, filling):
def get_existing_opening_occurrence_if_any(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
filling_type = ifcopenshell.util.element.get_type(filling)
if filling_type:
filling_occurrences = ifcopenshell.util.element.get_types(filling_type)
@@ -39,6 +39,7 @@ from mathutils import Vector, Matrix
from bpy_extras.object_utils import AddObjectHelper
from . import prop
import json
from typing import Any, Union
class EnableAddType(bpy.types.Operator, tool.Ifc.Operator):
@@ -137,7 +138,10 @@ class AddConstrTypeInstance(bpy.types.Operator):
if not relating_type_id:
return {"FINISHED"}
if self.from_invoke:
# Check relating_type_id enum_items since it's possible
# that we're adding e.g. IfcRoofType being in a Slab Tool
# and roof type id won't be present in the relating_type_id enum.
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
props.relating_type_id = str(self.relating_type_id)
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
@@ -188,9 +192,9 @@ class AddConstrTypeInstance(bpy.types.Operator):
obj.location = context.scene.cursor.location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
collection_obj = collection.BIMCollectionProperties.obj
self.container_obj = None
if container := tool.Root.get_default_container():
self.container_obj = tool.Ifc.get_object(container)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
tool.Blender.remove_data_block(mesh) # Remove "Instance" mesh
@@ -244,11 +248,11 @@ class AddConstrTypeInstance(bpy.types.Operator):
# TODO For now we are hardcoding windows and doors as a prototype
bpy.ops.bim.add_filled_opening(voided_obj=building_obj.name, filling_obj=obj.name)
else:
if collection_obj and tool.Ifc.get_entity(collection_obj):
if self.container_obj:
if props.rl_mode == "BOTTOM":
obj.location.z = collection_obj.location.z - tool.Blender.get_object_bounding_box(obj)["min_z"]
obj.location.z = self.container_obj.location.z - tool.Blender.get_object_bounding_box(obj)["min_z"]
elif props.rl_mode == "CONTAINER":
obj.location.z = collection_obj.location.z
obj.location.z = self.container_obj.location.z
elif props.rl_mode == "CURSOR":
pass
@@ -270,11 +274,8 @@ class AddConstrTypeInstance(bpy.types.Operator):
return {"FINISHED"}
def set_flow_segment_rl(self, obj):
collection = bpy.context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if collection_obj and tool.Ifc.get_entity(collection_obj):
obj.location[2] = collection_obj.location[2] + bpy.context.scene.BIMModelProperties.rl2
if self.container_obj:
obj.location[2] = self.container_obj.location[2] + bpy.context.scene.BIMModelProperties.rl2
@staticmethod
def generate_layered_element(ifc_class: str, relating_type: ifcopenshell.entity_instance) -> bool:
@@ -511,7 +512,7 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
)
def ensure_material_assigned(usecase_path, ifc_file, settings):
def ensure_material_assigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
if usecase_path == "material.assign_material":
if not settings.get("material", None):
return
@@ -524,53 +525,76 @@ def ensure_material_assigned(usecase_path, ifc_file, settings):
]:
elements.extend(rel.RelatedObjects)
for element in elements:
obj = IfcStore.get_element(element.GlobalId)
if not obj or not obj.data:
continue
element_material = ifcopenshell.util.element.get_material(element)
material = [m for m in ifc_file.traverse(element_material) if m.is_a("IfcMaterial")]
object_material_ids = [
om.BIMObjectProperties.ifc_definition_id
for om in obj.data.materials
if om is not None and om.BIMObjectProperties.ifc_definition_id
]
if material and material[0].id() in object_material_ids:
continue
if len(obj.data.materials) == 1:
obj.data.materials.clear()
if not material:
continue
obj.data.materials.append(IfcStore.get_element(material[0].id()))
update_blender_ifc_materials(elements)
def ensure_material_unassigned(usecase_path, ifc_file, settings):
def ensure_material_unassigned(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
elements = settings["products"]
if elements[0].is_a("IfcElementType"):
elements.extend(ifcopenshell.util.element.get_types(elements[0]))
update_blender_ifc_materials(elements)
def update_blender_ifc_materials(elements: list[ifcopenshell.entity_instance]) -> None:
"""update mesh blender materials that have ifc material connected to them
by replacing them with `blender_material`"""
# since different elements can share meshes (e.g. occurrecnes without openings)
# we need to make sure not to affect them accidentally
meshes_users: dict[bpy.types.Mesh, set[bpy.types.Object]] = dict()
for obj in bpy.data.objects:
if not obj.data:
continue
meshes_users.setdefault(obj.data, set()).add(obj)
objects: set[bpy.types.Object] = set()
for element in elements:
obj = tool.Ifc.get_object(element)
obj: bpy.types.Object = tool.Ifc.get_object(element)
if not obj or not obj.data:
continue
element_material = ifcopenshell.util.element.get_material(element)
if element_material:
objects.add(obj)
meshes: set[bpy.types.Mesh] = {obj.data for obj in objects}
for mesh in meshes:
mesh_users = meshes_users[mesh]
if not mesh_users.issubset(objects):
continue
to_remove = []
for i, slot in enumerate(obj.material_slots):
if not slot.material:
# NOTE: we need `obj` as removing materials and appending them to `mesh.materials`
# will mess up mesh faces material indices
# NOTE: we make an assumption here that all mesh users
# have the same material - they either inherit it from the type
# or type doesn't have a material.
#
# If we add option to UI to add materials overriding type materials
# then this assumption won't be safe anymore
obj = next(iter(mesh_users))
element = tool.Ifc.get_entity(obj)
current_material = ifcopenshell.util.element.get_material(element)
if current_material:
current_material = tool.Ifc.get_object(current_material)
material_replaced = False
for material_slot in obj.material_slots:
material = material_slot.material
if material is None:
continue
material = tool.Ifc.get_entity(slot.material)
if material:
to_remove.append(i)
total_removed = 0
for i in to_remove:
obj.active_material_index = i - total_removed
with bpy.context.temp_override(object=obj):
bpy.ops.object.material_slot_remove()
total_removed += 1
ifc_material = tool.Ifc.get_entity(material)
# it's blender material for style, so ignore it
if not ifc_material:
continue
if ifc_material == current_material:
continue
material_slot.material = current_material
material_replaced = True
if not material_replaced and current_material:
mesh.materials.append(current_material)
# clear empty slots
for i, material in reversed(list(enumerate(mesh.materials[:]))):
if material is None:
mesh.materials.pop(index=i)
@@ -57,8 +57,10 @@ class DumbProfileGenerator:
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = self.collection.BIMCollectionProperties.obj
self.container = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.depth = props.extrusion_depth
self.rotation = 0
self.location = Vector((0, 0, 0))
@@ -83,9 +85,8 @@ class DumbProfileGenerator:
):
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
matrix_world.translation = self.location
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world.translation.z = self.collection_obj.location.z
self.collection.objects.link(obj)
if self.container_obj:
matrix_world.translation.z = self.container_obj.location.z
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -94,7 +95,6 @@ class DumbProfileGenerator:
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
@@ -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
@@ -87,6 +87,21 @@ def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
def update_relating_array_from_object(self, context):
bpy.ops.bim.enable_editing_array(item=self.is_editing)
return
def is_object_array_applicable(self, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return False
return ifcopenshell.util.element.get_pset(element, "BBIM_Array")
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
relating_type_id: bpy.props.EnumProperty(
@@ -203,6 +218,14 @@ class BIMArrayProperties(PropertyGroup):
description="Regenerate all children based on the parent object",
default=False,
)
relating_array_object: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Copy Array Properties",
update=update_relating_array_from_object,
poll=is_object_array_applicable,
)
class BIMStairProperties(PropertyGroup):
@@ -20,8 +20,12 @@
import bpy
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.representation
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import V
import blenderbim
import blenderbim.core.root
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.door import bm_sort_out_geom
@@ -295,8 +299,6 @@ class BIM_OT_add_railing(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcRailing")
obj = bpy.data.objects.new("IfcRailing", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
blenderbim.core.root.assign_class(
@@ -535,5 +537,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"}
@@ -20,7 +20,11 @@ import bpy
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import blenderbim
import blenderbim.core.root
import blenderbim.tool as tool
from blenderbim.bim.helper import convert_property_group_from_si
from blenderbim.bim.module.model.door import bm_sort_out_geom
@@ -511,8 +515,6 @@ class BIM_OT_add_roof(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcRoof")
obj = bpy.data.objects.new("IfcRoof", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
blenderbim.core.root.assign_class(
@@ -757,7 +759,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"}
@@ -31,7 +31,6 @@ import blenderbim.core.type
import blenderbim.core.geometry
import blenderbim.core.root
import blenderbim.tool as tool
from math import radians
from mathutils import Vector, Matrix
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.decorator import ProfileDecorator
@@ -137,8 +136,10 @@ class DumbSlabGenerator:
)
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = self.collection.BIMCollectionProperties.obj
self.container = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.depth = sum(thicknesses) * unit_scale
self.width = 3
self.length = 3
@@ -161,13 +162,12 @@ class DumbSlabGenerator:
matrix_world = Matrix()
matrix_world.translation = self.location
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world.translation.z = self.collection_obj.location.z - self.depth
if self.container_obj:
matrix_world.translation.z = self.container_obj.location.z - self.depth
else:
matrix_world.translation.z -= self.depth
obj.matrix_world = Matrix.Rotation(self.x_angle, 4, "X") @ matrix_world
bpy.context.view_layer.update()
self.collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -176,7 +176,6 @@ class DumbSlabGenerator:
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
@@ -18,16 +18,13 @@
import bpy
import ifcopenshell
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.core.spatial as core
import blenderbim.core.type
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_label = "Generate Space from Cursor"
bl_options = {"REGISTER"}
bl_description = (
"Create a space from the cursor position. "
@@ -35,33 +32,15 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
"select the right space collection and run the operator"
)
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
return tool.Ifc.get_entity(collection_obj) and not element.is_a("IfcWall")
def _execute(self, context):
# This works as a 2.5 extruded polygon based on a cutting plane. Note
# that rooms exclude walls (i.e. not to wall midpoint or exterior /
# exterior edge.
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
collection = context.view_layer.active_layer_collection.collection
collection_obj = collection.BIMCollectionProperties.obj
if not collection_obj:
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
core.generate_space(tool.Ifc, tool.Spatial, tool.Model, tool.Type)
try:
core.generate_space(tool.Ifc, tool.Model, tool.Root, tool.Spatial, tool.Type)
except core.NoDefaultContainer:
return self.report({"ERROR"}, "Please set a default container to create the space in.")
class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
@@ -70,13 +49,6 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Generate spaces from selected walls. The active object must be a wall"
@classmethod
def poll(cls, context):
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
if element:
return context.selected_objects and element.is_a("IfcWall")
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
# walls (i.e. prismatic) in the active object storey.
@@ -87,19 +59,30 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(active_obj)
container = tool.Spatial.get_container(element)
def msg_no_active_object(self, context):
self.layout.label(text="No active object. Please select a wall")
def msg_no_active_wall(self, context):
self.layout.label(text="The active object is not a wall. Please select a wall.")
def msg_no_container(self, context):
self.layout.label(text="The wall is not contained. Please the selected wall in a building container")
def msg_no_selected_objects(self, context):
self.layout.label(text="No selected objects found. Please select walls.")
if not active_obj:
self.report({"ERROR"}, "No active object. Please select a wall")
context.window_manager.popup_menu(msg_no_active_object, title="Error", icon="ERROR")
return
element = tool.Ifc.get_entity(active_obj)
if element and not element.is_a("IfcWall"):
return self.report({"ERROR"}, "The active object is not a wall. Please select a wall.")
context.window_manager.popup_menu(msg_no_active_wall, title="Error", icon="ERROR")
return
if not container:
self.report({"ERROR"}, "The wall is not contained.")
context.window_manager.popup_menu(msg_no_container, title="Error", icon="ERROR")
return
if not context.selected_objects:
self.report({"ERROR"}, "No selected objects found. Please select walls.")
context.window_manager.popup_menu(msg_no_selected_objects, title="Error", icon="ERROR")
return
core.generate_spaces_from_walls(tool.Ifc, tool.Spatial, tool.Collector)
@@ -20,7 +20,11 @@ import bpy
import json
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import blenderbim
import blenderbim.core.root
import blenderbim.tool as tool
from mathutils import Vector
from bmesh.types import BMVert
@@ -214,10 +218,7 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcStairFlight")
obj = bpy.data.objects.new("StairFlight", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
body_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
element = blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
@@ -225,7 +226,6 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator):
obj=obj,
ifc_class="IfcStairFlight",
should_add_representation=False,
context=body_context,
)
if tool.Ifc.get_schema() != "IFC2X3":
element.PredefinedType = "STRAIGHT"
@@ -337,6 +337,6 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMStairProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -56,8 +56,11 @@ class LaunchTypeManager(bpy.types.Operator):
ifc_class = props.ifc_class or AuthoringData.data["ifc_element_type"]
else:
ifc_class = AuthoringData.data["ifc_element_type"]
props.type_class = ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
# will be None if project has no types
if ifc_class is not None:
props.type_class = ifc_class
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9)
return context.window_manager.invoke_popup(self, width=550)
def draw(self, context):
@@ -220,6 +223,8 @@ class BIM_PT_array(bpy.types.Panel):
row = col.row(align=True)
row.prop(props, "z")
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "relating_array_object", icon="COPYDOWN")
else:
row = box.row(align=True)
name = f"{array['count']} Items ({array.get('method', 'OFFSET').capitalize()})"
@@ -34,7 +34,7 @@ import blenderbim.core.root
import blenderbim.core.geometry
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from math import pi, sin, cos, degrees, radians
from math import pi, sin, cos, degrees
from mathutils import Vector, Matrix
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
from typing import Optional
@@ -449,8 +449,10 @@ class DumbWallGenerator:
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = self.collection.BIMCollectionProperties.obj
self.container = None
if container := tool.Root.get_default_container():
self.container = container
self.container_obj = tool.Ifc.get_object(container)
self.width = self.layers["thickness"]
self.height = props.extrusion_depth
self.length = props.length
@@ -540,11 +542,12 @@ class DumbWallGenerator:
def derive_from_cursor(self):
RAYCAST_PRECISION = 0.01
self.location = bpy.context.scene.cursor.location
if self.collection:
for sibling_obj in self.collection.objects:
if not isinstance(sibling_obj.data, bpy.types.Mesh):
if self.container:
for subelement in ifcopenshell.util.element.get_decomposition(self.container):
if not subelement.is_a("IfcWall"):
continue
if "IfcWall" not in sibling_obj.name:
sibling_obj = tool.Ifc.get_object(subelement)
if not sibling_obj or not isinstance(sibling_obj.data, bpy.types.Mesh):
continue
inv_obj_matrix = sibling_obj.matrix_world.inverted()
local_location = inv_obj_matrix @ self.location
@@ -594,11 +597,10 @@ class DumbWallGenerator:
matrix_world = Matrix.Rotation(self.rotation, 4, "Z")
matrix_world.translation = self.location
if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id:
matrix_world.translation.z = self.collection_obj.location.z + props.rl1
if self.container_obj:
matrix_world.translation.z = self.container_obj.location.z + props.rl1
obj.matrix_world = matrix_world
bpy.context.view_layer.update()
self.collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc,
@@ -607,7 +609,6 @@ class DumbWallGenerator:
obj=obj,
ifc_class=ifc_class,
should_add_representation=False,
context=self.body_context,
)
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
if self.axis_context:
@@ -26,66 +26,16 @@ import blenderbim.tool as tool
import blenderbim.core.root
import blenderbim.core.geometry
from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import V
from bmesh.types import BMVert
from mathutils import Vector
# TODO: move to some utils helpers/tool module
def update_simple_openings(element, opening_width, opening_height):
ifc_file = tool.Ifc.get()
fillings = tool.Ifc.get_all_element_occurrences(element)
voided_objs = set()
has_replaced_opening_representation = False
for filling in fillings:
if not filling.FillsVoids:
continue
opening = filling.FillsVoids[0].RelatingOpeningElement
voided_obj = tool.Ifc.get_object(opening.VoidsElements[0].RelatingBuildingElement)
voided_objs.add(voided_obj)
# we assume that the same element type (e.g. window)
# will be used only for voiding objects of the same thickness (by y-dimension)
# (e.g. all walls window's attached to will share the same thickness)
# If that's not the case it will some linings will be too thick or too thin
if has_replaced_opening_representation:
continue
old_representation = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
old_representation = tool.Geometry.resolve_mapped_representation(old_representation)
ifcopenshell.api.run(
"geometry.unassign_representation", ifc_file, product=opening, representation=old_representation
)
thickness = voided_obj.dimensions[1] + 0.1 + 0.1
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
shape_builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
extrusion = shape_builder.extrude(
shape_builder.rectangle(size=Vector([opening_width, 0.0, opening_height]).xz),
magnitude=thickness / unit_scale,
position=Vector([0.0, -0.1 / unit_scale, 0.0]),
**shape_builder.extrude_kwargs("Y")
)
new_representation = shape_builder.get_representation(context, extrusion)
for inverse in ifc_file.get_inverse(old_representation):
ifcopenshell.util.element.replace_attribute(inverse, old_representation, new_representation)
ifcopenshell.api.run("geometry.remove_representation", ifc_file, representation=old_representation)
has_replaced_opening_representation = True
tool.Model.reload_body_representation(voided_objs)
if fillings:
with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]):
bpy.ops.bim.recalculate_fill()
def update_window_modifier_representation(context, obj):
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
@@ -175,7 +125,7 @@ def update_window_modifier_representation(context, obj):
occurrence.OverallWidth = props.overall_width / si_conversion
occurrence.OverallHeight = props.overall_height / si_conversion
update_simple_openings(element, props.overall_width / si_conversion, props.overall_height / si_conversion)
tool.Model.update_simple_openings(element)
def create_bm_window_frame(bm, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze()):
@@ -452,8 +402,6 @@ class BIM_OT_add_window(bpy.types.Operator, tool.Ifc.Operator):
mesh = bpy.data.meshes.new("IfcWindow")
obj = bpy.data.objects.new("IfcWindow", mesh)
obj.location = spawn_location
collection = context.view_layer.active_layer_collection.collection
collection.objects.link(obj)
element = blenderbim.core.root.assign_class(
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcWindow", should_add_representation=False
@@ -590,6 +538,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"}
@@ -22,7 +22,7 @@ import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
from blenderbim.bim.helper import prop_with_search, close_operator_panel
from blenderbim.bim.helper import prop_with_search
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData
from blenderbim.bim.module.drawing.data import DecoratorData
@@ -378,6 +378,7 @@ class BimToolUI:
op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
@@ -385,8 +386,8 @@ class BimToolUI:
"IfcDuctSegment",
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_F", "")
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "")
if context.region.type != "TOOL_HEADER":
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
@@ -394,7 +395,6 @@ class BimToolUI:
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
@@ -485,7 +485,7 @@ class BimToolUI:
cls.layout.separator()
add_layout_hotkey_operator(
cls.layout, "Calculate All Quantities", "S_Q", bpy.ops.bim.calculate_all_quantities.__doc__
cls.layout, "Perform Quantity Take-off", "S_Q", bpy.ops.bim.perform_quantity_take_off.__doc__
)
@classmethod
@@ -614,7 +614,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects:
return
bpy.ops.bim.calculate_all_quantities()
bpy.ops.bim.perform_quantity_take_off()
def hotkey_C_P(self):
if not bpy.context.selected_objects:
@@ -719,10 +719,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.flip_wall()
elif self.active_class in ("IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase"):
bpy.ops.bim.flip_fill()
elif self.active_class in ("IfcBeam", "IfcColumn"):
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.flip_object(flip_local_axes="XZ")
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
def hotkey_S_G(self):
obj = bpy.context.active_object
@@ -808,9 +807,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return
if self.active_material_usage == "LAYER2":
bpy.ops.bim.join_wall(join_type="V")
elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"):
bpy.ops.bim.fit_flow_segments()
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.extend_profile(join_type="V")
def hotkey_S_B(self):
bpy.ops.bim.add_boundary()
@@ -121,12 +121,16 @@ class UpdateIfcPatchArguments(bpy.types.Operator):
for arg_name in inputs:
arg_info = inputs[arg_name]
new_attr = patch_args.add()
data_type = arg_info.get("type", "str")
if isinstance(data_type, list):
data_type = [dt for dt in data_type if dt != "NoneType"][0]
new_attr.data_type = {
"Literal": "string",
"str": "string",
"float": "float",
"int": "integer",
"bool": "boolean",
}[arg_info.get("type", "str")]
}[data_type]
new_attr.name = arg_name
new_attr.set_value(arg_info.get("default", new_attr.get_value_default()))
return {"FINISHED"}
@@ -21,6 +21,7 @@ from . import ui, prop, operator, data
classes = (
operator.AddProfileDef,
operator.DuplicateProfileDef,
operator.DisableEditingArbitraryProfile,
operator.DisableEditingProfile,
operator.DisableProfileEditingUI,
@@ -66,9 +66,15 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator):
profile: bpy.props.IntProperty()
def _execute(self, context):
props = context.scene.BIMProfileProperties
current_index = props.active_profile_index
ifcopenshell.api.run("profile.remove_profile", tool.Ifc.get(), profile=tool.Ifc.get().by_id(self.profile))
bpy.ops.bim.load_profiles()
# preserve selected index if possible
if props.profiles:
props.active_profile_index = min(current_index, len(props.profiles) - 1)
class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_profile"
@@ -126,6 +132,27 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.load_profiles()
class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.duplicate_profile_def"
bl_label = "Duplicate Profile"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
props = context.scene.BIMProfileProperties
if len(props.profiles) > props.active_profile_index:
return True
cls.poll_message_set("No profile selected to duplicate.")
return False
def _execute(self, context):
props = context.scene.BIMProfileProperties
ifc_file = tool.Ifc.get()
profile = ifc_file.by_id(props.profiles[props.active_profile_index].ifc_definition_id)
tool.Profile.duplicate_profile(profile)
bpy.ops.bim.load_profiles()
class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_arbitrary_profile"
bl_label = "Enable Editing Arbitrary Profile"
@@ -148,7 +175,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
def disable_editing_arbitrary_profile(context):
obj = context.active_object
if obj and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
if obj and obj.type == "MESH" and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
profile_mesh = obj.data
@@ -70,6 +70,7 @@ class BIM_PT_profiles(Panel):
row = self.layout.row(align=True)
row.prop(self.props, "profile_classes", text="")
row.operator("bim.add_profile_def", text="", icon="ADD")
row.operator("bim.duplicate_profile_def", icon="DUPLICATE", text="")
self.layout.template_list(
"BIM_UL_profiles",
@@ -36,8 +36,8 @@ classes = (
operator.EnableCulling,
operator.EnableEditingHeader,
operator.ExportIFC,
operator.ExportIFCDeprecated,
operator.FlipClippingPlane,
operator.ImportIFC,
operator.LinkIfc,
operator.LoadLink,
operator.LoadLinkedProject,
@@ -76,6 +76,11 @@ classes = (
gizmo.ClippingPlane,
)
if bpy.app.version >= (4, 1, 0):
classes += (
operator.IFCFileHandlerOperator,
operator.BIM_FH_import_ifc,
)
addon_keymaps = []
@@ -97,7 +102,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))

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