Compare commits

...

112 Commits

Author SHA1 Message Date
Dion Moult 04cf9c870b Fix bug when linking in models with no chunks 2024-04-03 21:24:03 +11:00
Andrej730 bf9bddc867 ifcopenshell.validate to check unique guids (IfcRoot.UR1) 2024-04-02 17:17:23 +05:00
Andrej730 20a2fda066 typing 2024-04-02 15:45:42 +05:00
Andrej730 f03afb20f3 validate.py - black format 2024-04-02 15:45:42 +05:00
Gorgious56 e0df22ffec The button to remove an inherited pset is no longer enabled on instances
Previously it would fail silently if clicked
2024-04-02 09:52:43 +02:00
Andrej730 96302a5c07 Return of the missing button for regenerating global id
Was about to implement to find that we already have "bim.generate_global_id" and it just was lost at some point. Returned it to the attributes UI. It also allows Updating guids for all selected elements on Shift+Click.

This also can be useful to solve MergeProject and other guids conflicts, see example issue in https://community.osarch.org/discussion/2062/bbim-mergeproject-patch-not-separating-elements-in-different-buildings

Example - https://imgur.com/a/mvRtyER
2024-04-02 12:44:06 +05:00
Andrej730 38afce1628 RegenerateGlobalIds - tests 2024-04-02 12:32:36 +05:00
Andrej730 cd5afacc97 Fix test after cf9a94a79 2024-04-01 17:22:49 +05:00
Andrej730 c2623e5dce prevent reloading reference annotations on loading .ifc #4478
1) if reference annotation was changed, it would reload discarding the changes
2) even if reference annotation wasn't changed it would still reload them producing noise in .ifc changes
2024-04-01 17:06:20 +05:00
Andrej730 42ecd7e127 typing 2024-04-01 17:04:00 +05:00
Andrej730 c80a7cfcc9 fix typo 2024-04-01 16:00:18 +05:00
Andrej730 6d8f1a3980 experimental svg to dxf conversion for drawings
Example - https://imgur.com/a/AkvLlbZ
2024-04-01 15:50:43 +05:00
Andrej730 5c4c61bb15 fix issue with prohibited facets in 0065c5f, add a test 2024-04-01 13:49:42 +05:00
Andrej730 d8af8b58bb ifctester.Specification.set_usage convenience method
sets minOccurs and maxOccurs according to the agreement mentioned in the latest ids.xsd
https://github.com/buildingSMART/IDS/blob/6d71cdf3547a0383c6cfbbead81a7cef7521ac3a/Development/ids.xsd#L67-L72
2024-04-01 12:26:07 +05:00
Andrej730 3c0405249b typing 2024-04-01 12:26:07 +05:00
Dion Moult 7f57649d10 Whoops. 2024-04-01 17:49:44 +11:00
Dion Moult fbde23870e Fix #4321. The PurgeData recipe now also works on IFC2X3. 2024-04-01 17:00:47 +11:00
Dion Moult 7b968030ca Fix #4060. The colour by property panel now shows the number of results in the legend. 2024-04-01 15:45:34 +11:00
Dion Moult b5d672ba42 Fix #4069. IfcClash Blender interface now uses the new filter UI. 2024-04-01 15:28:34 +11:00
Dion Moult efa8da83ba Bump IOS. 2024-04-01 10:19:40 +11:00
Dion Moult 64cf7237db IfcDiff now uses the new selector syntax with the new UI 2024-04-01 10:16:30 +11:00
Dion Moult cf9a94a799 Fix #4306. Remove superseded selector UI. 2024-04-01 09:44:50 +11:00
Ryan Schultz 46c1a8c8ff added search for styles - in BIM_PT_representation_items 2024-03-31 11:57:55 -05:00
Thomas Krijnen ac7f2aeb62 #4450 todo note 2024-03-31 15:16:30 +02:00
Dion Moult 9378a763f3 Fix #4328. Fix incorrect documentation about how to use the imperial_length formatting function. 2024-03-31 22:32:07 +11:00
Dion Moult 0bc83fedae Use indices instead of WorkTimeDatesInterface
Checking start/end dates are done very, very often in date calculations (often looping through every day in a date range). So I'd prefer more opaque code but less overhead.
2024-03-31 22:11:17 +11:00
Thomas Krijnen f7c03db752 #4450 don't fail on empty bbox in prefiltering 2024-03-31 11:25:56 +02:00
Dion Moult b6b8c21741 See #4484. Editing predefined type attributes now enforces valid combinations of userdefined and object/element type 2024-03-31 15:50:31 +11:00
Dion Moult c8af51cfd2 Fix #4323. Bug where running a filter could fail if you tried to compare values which had incompatible types. 2024-03-30 23:07:06 +11:00
Dion Moult 77de3820e7 More minor doc changes and polish 2024-03-30 23:06:07 +11:00
Dion Moult adc5bea4e9 Fix #4265. Bug where when loading a saved filter it showed an unnecessary = comparison (which is by default). 2024-03-30 23:05:37 +11:00
Dion Moult 1eddcb9ee0 Minor doc fixes 2024-03-30 17:37:50 +11:00
Dion Moult eae542f90f Fix #4461. Write documentation on how to build and edit documentation. 2024-03-30 17:37:14 +11:00
Dion Moult fab0d05279 Fix #4483. 2024-03-30 17:14:13 +11:00
Dion Moult d2e495a69f Fix #4318. Theme docs to match website branding and add edit button, footer links, and icons. 2024-03-30 16:47:56 +11:00
Dion Moult 02058efa4d Fix #4286. IfcCSV summaries are now formatted the same as the rows. 2024-03-30 15:40:02 +11:00
Dion Moult ed79cbb1cc Fix bug where loading drawings didn't correctly link shared meshes (e.g. from a type of an annotation) 2024-03-30 15:05:49 +11:00
Dion Moult 0065c5fba8 Fix incorrect handling of prohibited specifications 2024-03-30 00:13:02 +11:00
Andrej730 1ab24e8362 import referenced drawings activating the current drawing #4478 2024-03-29 15:13:40 +05:00
Andrej730 232b4ec34d typing 2024-03-29 15:11:47 +05:00
Thomas Krijnen 75e6147dbc #4474 Commented out code perhaps for future use 2024-03-29 10:46:25 +01:00
Dion Moult 8d7b293381 Fix #4305. Bug where importing spreadsheets into IFC didn't intelligently cast attribute data types 2024-03-29 20:37:34 +11:00
Andrej730 19c8afb652 Fix #4477 2024-03-29 11:51:14 +05:00
Dion Moult 84dcee6b36 Fix #4368. Update to latest Brickschema alpha to cut dep size 2024-03-29 15:48:52 +11:00
Dion Moult dadcf03f55 See #4473. Cull entourage library.
We haven't confirmed the legality of the other assets.
2024-03-29 15:33:47 +11:00
Dion Moult 413886ac6b Fix #4411. Fallback to absolute paths if relative paths fail. 2024-03-29 15:29:19 +11:00
Dion Moult 377f68c020 Run black on IfcTester 2024-03-29 15:28:33 +11:00
Dion Moult b718a37db2 Fix #4427. Failures are now stored in single dictionary.
There was a missing clear() call to failed_reasons which could lead to incorrect reporting. It also makes no sense to store them separately.
2024-03-29 15:06:59 +11:00
Dion Moult e21043b038 Fix #4465. Update IfcTester to IDS 0.9.7 2024-03-28 22:47:48 +11:00
Andrej730 395598d8e3 Fix invalid profiles after subdividing edges near arcs #4471 2024-03-28 14:02:03 +05:00
Andrej730 813518bcb5 use blender units subtype for cad props 2024-03-28 14:02:03 +05:00
Andrej730 4ce7a898c9 typing, format 2024-03-28 14:02:02 +05:00
Thomas Krijnen 78f235ea7c #4273 ignore degenerate triangles in mesher output 2024-03-28 09:06:41 +01:00
Vukas Pajic bda2f50f9d sync with latest 2024-03-28 17:15:58 +11:00
Vukas Pajic 5e5ba5b3d4 Additional metadata as property set #4425
https://github.com/IfcOpenShell/IfcOpenShell/issues/4425
2024-03-28 17:15:58 +11:00
Dion Moult 22b0e9dcc5 Minor doc fixes to refer to Mac intel and silicon 2024-03-28 17:13:50 +11:00
Dion Moult 68420d43ea Clash results now are sorted and show text overlays of clash type and distance 2024-03-28 17:11:46 +11:00
Dion Moult 924e337c90 Don't consider borderline points to be inside a shape for more robust protrusion results 2024-03-28 16:57:54 +11:00
Dion Moult 04599a10d2 Change precision check of "is point on line" to be 0.01mm
Precision::Confusion() is 1e-7 which is too small apparently when doing this math. There are plenty of results which are things like 5e-7.
2024-03-28 16:57:54 +11:00
Dion Moult 84c931bb5f Fix bug where hidden elements weren't selected sometimes 2024-03-28 16:52:30 +11:00
Dion Moult 572aedb3eb Fix #4269. Include dateutil for ifcopenshell on pypi 2024-03-28 10:48:19 +11:00
Dion Moult 1db86c24e3 Bump IfcOpenShell and remove py37 py38 from installation docs 2024-03-28 10:47:44 +11:00
Andrej730 74adde3030 ExtractElemenets - add some tests 2024-03-27 17:52:18 +05:00
Andrej730 f68e8f0e00 ExtractElements - fix bug where IfcElementAssembly elements wasn't added to the spatial tree
Example issue - https://community.osarch.org/discussion/2054/bbim-extractelements-patch-and-missing-elements-in-ifcelementassembly
2024-03-27 16:59:10 +05:00
Andrej730 98386984f3 Fix tests involving changes in Blender 4.1 (will fail on Blender <4.1)
Basically in Blender 4.1 they've changed a way renaming IDs works if name already existed before and those tests will stop working on Blender <4.1, see https://projects.blender.org/blender/blender/issues/119955
2024-03-27 15:23:46 +05:00
Andrej730 7b35ed97ca tests - reduce precision to our usual Cad tool precision to fix some tests 2024-03-27 15:07:49 +05:00
Andrej730 b3302ed679 Fix test in 1c1035e
tag @brunoperdigao for information
2024-03-27 15:07:49 +05:00
Andrej730 e12f060af8 black format 2024-03-27 15:07:49 +05:00
Dion Moult 6d4f47a2ca New entourage library including people and cars 2024-03-27 20:23:44 +11:00
Thomas Krijnen e49ab7e0cc #4070 don't increment aggregate length on invalid members 2024-03-27 09:36:05 +01:00
Dion Moult ccfe7ab80a Optimise property / quantity checks for IDS and other purposes 2024-03-27 16:09:13 +11:00
Dion Moult 7b95358975 Forgot to commit clash tool 2024-03-27 16:06:47 +11:00
Dion Moult bcc50aaadc Switch over clash to latest add_element signature 2024-03-27 16:06:15 +11:00
Dion Moult 30f63b7d67 Minor note about which OCCT version is currently supported 2024-03-27 15:59:25 +11:00
Dion Moult ead13e23bc Write docs about new geometry tree clash functions 2024-03-27 15:59:25 +11:00
Thomas Krijnen 24257d2a85 checkout v0.7.0 -- src/ifcgeom_schema_agnostic/IfcGeomRepresentation.cpp 2024-03-26 15:13:36 +01:00
Dion Moult 5c4dbb924d Reinclude missing max_protrusions_ 2024-03-26 15:13:36 +01:00
Thomas Krijnen 88e1b3d2f9 Fix is_manifold(), sorry :( 2024-03-26 15:13:36 +01:00
Thomas Krijnen f67b80c6a6 Integrate @Moult's add_element(TriangulationElement*) 2024-03-26 15:13:36 +01:00
Dion Moult c7fd42f2cf Clash detection features 2024-03-26 15:13:36 +01:00
Thomas Krijnen bed03bf83c #4307 2024-03-26 12:27:05 +01:00
Andrej730 1e49400e93 fix styles tests 2024-03-26 16:12:13 +05:00
Andrej730 608c0dc05d Fix tests, clean up file creating empty project
To fix tests changed bim.unlink_object not to unlink original blender materials as they might be used elsewhere.
2024-03-26 15:59:23 +05:00
Andrej730 921dbe9dae restore accidentally unregistered operator in 9be7d95 (caused issue in tests) 2024-03-26 15:54:46 +05:00
Andrej730 a7e0837f96 small optimization
not to calculate each quantity twice
2024-03-26 15:54:46 +05:00
Andrej730 071c80f845 fix tests 2024-03-26 15:54:46 +05:00
Andrej730 06d823b9d8 typing 2024-03-26 15:54:45 +05:00
Andrej730 b15e33e5e9 fix test after 7634e71 2024-03-26 14:32:16 +05:00
Thomas Krijnen 55885f7a24 #4329 Don't accept non-finite floats 2024-03-26 10:23:14 +01:00
Ian e5fd6d9152 Update build-deps.cmd 2024-03-26 19:32:56 +11:00
Gorgious56 4dda7e84a0 You can now select only the aggregate or the aggregate and all its part using the interface 2024-03-26 08:33:18 +01:00
Dion Moult bb6be4d52d Blender IfcClash interface now supports new IfcClash modes
The dumb clash manager has been merged in with the main clash sets window. Loading and saving clashes now also bring in clash results, so the dumb clash manager's load/save is being deprecated. New decorator highlights clash result visually.
2024-03-26 15:24:47 +11:00
Dion Moult 01d487f585 Upgrade IfcClash to use new clash capabilities (currently unmerged from feature-clash2) 2024-03-26 15:22:56 +11:00
Andrej730 67a17ddd1b Fix hiding annotation objects without repr on drawing activation 2024-03-25 14:44:51 +05:00
Andrej730 f916edce1e typing 2024-03-25 14:43:53 +05:00
Andrej730 b0d810199b fix duplicated function names in test_feature 2024-03-25 14:18:35 +05:00
Andrej730 515a72cec9 fix linked ifc selectability toggle 2024-03-25 14:01:08 +05:00
Andrej730 7464f7519f fix load/unload linked ifc tests 2024-03-25 14:01:08 +05:00
Andrej730 f0dde4f825 fix linked ifc visibility toggle tests 2024-03-25 14:01:08 +05:00
Andrej730 54fefb3684 fix linked ifc wireframe mode toggle 2024-03-25 14:01:08 +05:00
tim 628fcb2999 Remove object text from subheadings (#4445)
https://github.com/IfcOpenShell/IfcOpenShell/issues/4312
2024-03-25 13:37:11 +05:00
Thomas Krijnen 22f830b053 #4130 setOnlyValid() option that skips and logs invalid topology 2024-03-24 16:45:31 +01:00
Bruno Postle 579c72fc69 Drawings panel: fix select_all_drawings operator
Previously it always deselected all drawings, now works as advertised
2024-03-24 10:31:47 +00:00
Andrej730 642a0f7084 black format 2024-03-22 17:46:21 +05:00
Andrej730 7cd5aab1c5 Fix ifc linking tests 2024-03-22 17:44:38 +05:00
Andrej730 4b15535c9c Fix issue removing material set
1) core.remove_material_set were removing material set and calling material_tool.get_active_material_type()
2) material_tool.get_active_material_type() called MaterialsData which failed with the traceback below since props.materias[current_index] had ifc id that were just removed

Traceback (most recent call last):
  File "\prop.py", line 96, in get_material_types
    MaterialsData.load()
  File "\data.py", line 43, in load
    "active_styles": cls.active_styles(),
                     ^^^^^^^^^^^^^^^^^^^
  File "\data.py", line 119, in active_styles
    material = tool.Ifc.get().by_id(material)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \blenderbim\libs\site\packages\ifcopenshell\file.py", line 411, in by_id
    return self[id]
           ~~~~^^^^
  File \blenderbim\libs\site\packages\ifcopenshell\file.py", line 396, in __getitem__
    return entity_instance(self.wrapped_data.by_id(key), self)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File \blenderbim\libs\site\packages\ifcopenshell\ifcopenshell_wrapper.py", line 4619, in by_id
    return _ifcopenshell_wrapper.file_by_id(self, id)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Instance # 66 not found
File "\prop.py", line 94, in get_material_types
WARN (bpy.rna): C:\Users\blender\git\blender-v410\blender.git\source\blender\python\intern\bpy_rna.cc:1366 pyrna_enum_to_py: current value '2' matches no enum in 'BIMMaterialProperties', '', 'material_type'
2024-03-22 16:54:42 +05:00
Andrej730 b978da24d9 fix ui error getting active styles for non IfcMaterial materials
Error was:

```
Traceback (most recent call last):
  File "\blenderbim\bim\module\material\ui.py", line 43, in draw
    MaterialsData.load()
  File "\blenderbim\bim\module\material\data.py", line 43, in load
    "active_styles": cls.active_styles(),
                     ^^^^^^^^^^^^^^^^^^^
  File "\blenderbim\bim\module\material\data.py", line 117, in active_styles
    return results

  File "\blenderbim\libs\site\packages\ifcopenshell\entity_instance.py", line 194, in __getattr__
    raise AttributeError(
AttributeError: entity instance of type 'IFC4.IfcMaterialLayerSet' has no attribute 'HasRepresentation'
```
2024-03-22 16:54:42 +05:00
Andrej730 7132c4e9bf typing and small refactor 2024-03-22 16:54:42 +05:00
Andrej730 5aa6123806 Blender 4.1 - fix errors due to renamed type manager icon 2024-03-22 15:58:45 +05:00
FabioPiccinini e48299cb3f Update sample.css (#4459)
Changed unescaped ; with , because it may cause problems if the style is included in svg
2024-03-22 11:51:57 +01:00
FabioPiccinini c65088a7a7 Unescaped ; was causing problems in Inkscape (#4456)
* Update default.css

An unescaped ; was causing problems when opening svgs that included this style with Inkscape

* Update default.css
2024-03-22 10:06:57 +01:00
Thomas Krijnen 4f53d8c5f1 #4452 release gil in ifcopenshell.open() and read() 2024-03-21 14:11:34 +01:00
147 changed files with 7435 additions and 2241 deletions
+3
View File
@@ -81,6 +81,9 @@ src/blenderbim/blenderbim/translations.py
# blenderbim test temp files
src/blenderbim/test/files/temp
src/blenderbim/test/files/basic.ifc.cache.blend
src/blenderbim/test/files/basic.ifc.cache.sqlite
src/blenderbim/drawings
src/blenderbim/layouts
+4 -2
View File
@@ -137,7 +137,7 @@ 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-e38eafd-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -362,7 +362,9 @@ endif
# Provides express rule validation for ifcopenshell.validate
cd dist/working && . env/bin/activate && $(PIP) install pytest --target=./site-packages
# Provides Brickschema functionality
cd dist/working && . env/bin/activate && $(PIP) install brickschema[persistence] --target=./site-packages
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
cp -r dist/working/site-packages/* dist/blenderbim/libs/site/packages/
rm -rf dist/working
@@ -10,7 +10,7 @@
* (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
* 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.
*
@@ -10,7 +10,7 @@
* (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
* 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.
*
File diff suppressed because one or more lines are too long
+7 -38
View File
@@ -75,6 +75,10 @@ def draw_attribute(attribute, layout, copy_operator=None):
op.data_path = attribute.path_from_id("string_value")
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if attribute.name == "GlobalId":
layout.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
if copy_operator:
op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN")
op.name = attribute.name
@@ -216,39 +220,6 @@ def get_enum_items(data, prop_name, context=None):
return items
def get_obj_ifc_definition_id(context, obj, obj_type):
if obj_type == "Object":
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Material":
return bpy.data.materials.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "MaterialSet":
return ifcopenshell.util.element.get_material(
tool.Ifc.get_entity(bpy.data.objects.get(obj)), should_skip_usage=True
).id()
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task":
return context.scene.BIMTaskTreeProperties.tasks[
context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id
elif obj_type == "Cost":
return context.scene.BIMCostProperties.cost_items[
context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
elif obj_type == "Resource":
return context.scene.BIMResourceTreeProperties.resources[
context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
elif obj_type == "Profile":
return context.scene.BIMProfileProperties.profiles[
context.scene.BIMProfileProperties.active_profile_index
].ifc_definition_id
elif obj_type == "WorkSchedule":
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
elif obj_type == "Group":
prop = context.scene.BIMGroupProperties
return prop.groups[prop.active_group_index].ifc_definition_id
# hack to close popup
# https://blender.stackexchange.com/a/202576/130742
def close_operator_panel(event):
@@ -304,6 +275,9 @@ def draw_filter(layout, filter_groups, data, module):
op.type = sprops.facet
op.index = i
op.module = module
op = row.operator("bim.remove_filter_group", text="", icon="X")
op.index = i
op.module = module
for j, ifc_filter in enumerate(filter_group.filters):
if ifc_filter.type == "entity":
@@ -350,11 +324,6 @@ def draw_filter(layout, filter_groups, data, module):
op.index = j
op.module = module
row = box.row()
op = row.operator("bim.remove_filter_group", icon="X")
op.index = i
op.module = module
# TODO this should move into ifcopenshell.util
@@ -1164,6 +1164,9 @@ class IfcImporter:
return obj
def load_existing_meshes(self) -> None:
self.meshes.update({m.name: m for m in bpy.data.meshes})
def get_representation_item_material_name(self, item):
if not item.StyledByItem:
return
@@ -94,9 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
relating_obj=tool.Ifc.get_object(aggregate),
related_obj=tool.Ifc.get_object(element),
)
# Removes Pset related to Linked Aggregates
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
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)
@@ -206,15 +206,29 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
bl_label = "Select Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
select_parts: bpy.props.BoolProperty(default=False)
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select Aggregate and Parts"
else:
return "Select Aggregate"
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.data.objects.get(self.obj) or context.active_object
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj))
aggregate_obj = tool.Ifc.get_object(aggregate)
if self.select_parts:
bpy.ops.bim.select_parts(obj=aggregate_obj.name)
if aggregate_obj in context.selectable_objects:
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
tool.Blender.set_objects_selection(
context,
aggregate_obj,
[aggregate_obj],
clear_previous_selection=not self.select_parts,
)
return {"FINISHED"}
@@ -242,6 +256,7 @@ class BIM_OT_add_part_to_object(bpy.types.Operator, Operator):
part_name=self.part_name,
)
class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.break_link_to_other_aggregates"
bl_label = "Break link to other aggregates"
@@ -256,12 +271,12 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
return []
parts = ifcopenshell.util.element.get_parts(aggregate)
for part in parts:
pset = ifcopenshell.util.element.get_pset(part, 'BBIM_Linked_Aggregate')
pset = ifcopenshell.util.element.get_pset(part, "BBIM_Linked_Aggregate")
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
@@ -269,9 +284,10 @@ class BIM_OT_break_link_to_other_aggregates(bpy.types.Operator, Operator):
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
tool.Ifc.run("group.unassign_group", group=linked_aggregate_group[0], product=aggregate)
return {"FINISHED"}
class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
bl_idname = "bim.select_linked_aggregates"
bl_label = "Select linked aggregates"
@@ -284,17 +300,17 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator):
return []
if not element:
return []
linked_aggregate_group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
]
for obj in context.selected_objects:
obj.select_set(False)
for rel in linked_aggregate_group[0].IsGroupedBy or []:
for element in rel.RelatedObjects:
obj = tool.Ifc.get_object(element)
@@ -62,8 +62,12 @@ class BIM_PT_aggregate(Panel):
row = layout.row(align=True)
if AggregateData.data["has_relating_object"]:
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
op.obj = context.active_object.name
op.select_parts = False
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
op.select_parts = True
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
op = row.operator("bim.aggregate_unassign_object", icon="X", text="")
@@ -120,20 +120,46 @@ class EditAttributes(bpy.types.Operator, Operator):
return {"FINISHED"}
class GenerateGlobalId(bpy.types.Operator):
class GenerateGlobalId(bpy.types.Operator, Operator):
bl_idname = "bim.generate_global_id"
bl_label = "Regenerate GlobalId"
bl_description = "Regenerate GlobalId\n\nSHIFT+CLICK to regenerate GlobalIds for all selected objects"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId")
if index >= 0:
global_id = context.active_object.BIMAttributeProperties.attributes[index]
use_selected: bpy.props.BoolProperty(name="Use All Selected Objects", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# using all selected objects on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.use_selected = True
return self.execute(context)
def _execute(self, context):
if self.use_selected:
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcRoot"):
continue
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
# guid value was already regenerated, just update the ui prop
guid_value = element.GlobalId
else:
global_id = context.active_object.BIMAttributeProperties.attributes.add()
global_id.name = "GlobalId"
global_id.data_type = "string"
global_id.string_value = ifcopenshell.guid.new()
guid_value = ifcopenshell.guid.new()
props.attributes["GlobalId"].string_value = guid_value
return {"FINISHED"}
@@ -24,10 +24,10 @@ from math import pi
class BIMCadProperties(PropertyGroup):
resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1)
distance: bpy.props.FloatProperty(name="Distance", default=0.1)
x: bpy.props.FloatProperty(name="X", default=0.2)
y: bpy.props.FloatProperty(name="Y", default=0.1)
radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE")
distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE")
x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE")
y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE")
gable_roof_edge_angle: bpy.props.FloatProperty(
name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE"
)
@@ -20,6 +20,7 @@ import os
import bpy
import blenderbim.tool as tool
import blenderbim.bim.module.type.prop as type_prop
import ifcopenshell.util.unit
from bpy.types import WorkSpaceTool
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData
@@ -239,8 +240,9 @@ class CadHotkey(bpy.types.Operator):
row.prop(props, "resolution")
def hotkey_S_C(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifccircle(radius=self.props.radius)
bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_arc_from_2_points()
@@ -248,13 +250,15 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_trim_extend()
def hotkey_S_F(self):
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if self.is_profile():
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius)
bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion)
else:
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius)
bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion)
def hotkey_S_O(self):
bpy.ops.bim.cad_offset(distance=self.props.distance)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
def hotkey_S_Q(self):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -270,7 +274,8 @@ class CadHotkey(bpy.types.Operator):
def hotkey_S_R(self):
if self.is_profile():
bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion)
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
@@ -25,15 +25,12 @@ classes = (
operator.ExecuteIfcClash,
operator.ExportClashSets,
operator.ImportClashSets,
operator.LoadIfcClashes,
operator.SaveIfcClashes,
operator.LoadSmartGroupsForActiveClashSet,
operator.RemoveClashSet,
operator.RemoveClashSource,
operator.SelectClash,
operator.SelectClashResults,
operator.SelectClashSource,
operator.SelectIfcClashResults,
operator.SelectSmartGroup,
operator.SelectSmartGroupedClashesPath,
operator.SmartClashGroup,
@@ -44,7 +41,6 @@ classes = (
prop.BIMClashProperties,
ui.BIM_PT_ifcclash,
ui.BIM_PT_clash_manager,
ui.BIM_PT_dumb_clash_manager,
ui.BIM_PT_smart_clash_manager,
ui.BIM_UL_clashes,
ui.BIM_UL_clash_sets,
@@ -0,0 +1,52 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 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/>.
import bpy
import json
import ifcopenshell.util.element
import blenderbim.tool as tool
def refresh():
ClashData.is_loaded = False
class ClashData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {}
cls.data["saved_searches"] = cls.saved_searches()
@classmethod
def saved_searches(cls):
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
for group in groups:
try:
data = json.loads(group.Description)
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@@ -0,0 +1,102 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 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/>.
import blf
import gpu
import bmesh
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
class ClashDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
text = context.scene.BIMClashProperties.active_clash_text
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5)
font_id = 0
blf.size(font_id, 12)
coords_2d = location_3d_to_region_2d(context.region, context.region_data, p)
color = self.addon_prefs.decorations_colour
blf.color(font_id, *color)
if coords_2d:
w, h = blf.dimensions(font_id, text)
coords_2d -= Vector((w * .5, 0))
blf.position(font_id, coords_2d[0], coords_2d[1], 0)
blf.draw(font_id, text) # Set your text here
def draw_geometry(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2]
selected_edges = []
if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]]
self.draw_batch("POINTS", selected_vertices, special_elements_color)
if selected_edges:
self.draw_batch("LINES", selected_vertices, special_elements_color, selected_edges)
@@ -23,10 +23,11 @@ import bmesh
import logging
import numpy as np
import ifcopenshell
from mathutils import Matrix, Vector
from math import radians
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
from math import radians
from mathutils import Matrix, Vector
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.clash.decorator import ClashDecorator
class ExportClashSets(bpy.types.Operator):
@@ -45,18 +46,7 @@ class ExportClashSets(bpy.types.Operator):
def execute(self, context):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets = []
for clash_set in context.scene.BIMClashProperties.clash_sets:
self.a = []
self.b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
if data.selector:
clash_source["selector"] = data.selector
clash_source["mode"] = data.mode
getattr(self, ab).append(clash_source)
clash_sets.append({"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b})
clash_sets = tool.Clash.export_clash_sets()
with open(self.filepath, "w") as destination:
destination.write(json.dumps(clash_sets, indent=4))
return {"FINISHED"}
@@ -78,25 +68,34 @@ class ImportClashSets(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
with open(self.filepath) as f:
clash_sets = json.load(f)
for clash_set in clash_sets:
tool.Clash.load_clash_sets(self.filepath)
context.scene.BIMClashProperties.clash_sets.clear()
for clash_set in tool.Clash.get_clash_sets():
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = clash_set["name"]
new.tolerance = clash_set["tolerance"]
new.mode = clash_set["mode"]
if new.mode == "intersection":
new.tolerance = clash_set["tolerance"]
new.check_all = clash_set["check_all"]
elif new.mode == "collision":
new.allow_touching = clash_set["allow_touching"]
elif new.mode == "clearance":
new.clearance = clash_set["clearance"]
new.check_all = clash_set["check_all"]
for clash_source in clash_set["a"]:
new_source = new.a.add()
new_source.name = clash_source["file"]
if "selector" in clash_source:
new_source.selector = clash_source["selector"]
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
new_source.mode = clash_source["mode"]
if clash_set["b"]:
if "b" in clash_set and clash_set["b"]:
for clash_source in clash_set["b"]:
new_source = new.b.add()
new_source.name = clash_source["file"]
if "selector" in clash_source:
new_source.selector = clash_source["selector"]
tool.Search.import_filter_query(clash_source["selector"], new_source.filter_groups)
new_source.mode = clash_source["mode"]
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -109,7 +108,6 @@ class AddClashSet(bpy.types.Operator):
def execute(self, context):
new = context.scene.BIMClashProperties.clash_sets.add()
new.name = "New Clash Set"
new.tolerance = 0.01
return {"FINISHED"}
@@ -211,8 +209,11 @@ class ExecuteIfcClash(bpy.types.Operator):
def invoke(self, context, event):
_, extension = os.path.splitext(self.filepath)
if extension != ".json":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
# TODO Temporarily until BCF support comes back
# if extension != ".json":
# self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -220,7 +221,12 @@ class ExecuteIfcClash(bpy.types.Operator):
def execute(self, context):
from ifcclash import ifcclash
self.props = context.scene.BIMClashProperties
_, extension = os.path.splitext(self.filepath)
if extension != ".bcf":
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
# TODO Temporarily until BCF support comes back
if extension != ".json":
self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf")
@@ -230,7 +236,7 @@ class ExecuteIfcClash(bpy.types.Operator):
settings.logger.setLevel(logging.DEBUG)
clasher = ifcclash.Clasher(settings)
if context.scene.BIMClashProperties.should_create_clash_snapshots:
if self.props.should_create_clash_snapshots:
def get_viewpoint_snapshot(viewpoint):
camera = bpy.data.objects.get("IFC Clash Camera")
@@ -271,23 +277,13 @@ class ExecuteIfcClash(bpy.types.Operator):
clasher.get_viewpoint_snapshot = get_viewpoint_snapshot
clasher.clash_sets = []
for clash_set in context.scene.BIMClashProperties.clash_sets:
self.a = []
self.b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
if data.selector:
clash_source["selector"] = data.selector
clash_source["mode"] = data.mode
getattr(self, ab).append(clash_source)
clash_set_data = {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a}
if self.b:
clash_set_data["b"] = self.b
clasher.clash_sets.append(clash_set_data)
clasher.clash_sets = tool.Clash.export_clash_sets()
clasher.clash()
clasher.export()
if extension == ".json":
tool.Clash.load_clash_sets(self.filepath)
tool.Clash.import_active_clashes()
return {"FINISHED"}
@@ -306,6 +302,7 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
# TODO refactor into new clash results system
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
@@ -352,83 +349,6 @@ class SelectIfcClashResults(bpy.types.Operator):
return {"FINISHED"}
class LoadIfcClashes(bpy.types.Operator):
bl_idname = "bim.load_ifc_clashes"
bl_label = "Load IFC Clashes"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets_json = json.load(f)
active_clash_set = context.scene.BIMClashProperties.active_clash_set
for clash_set in clash_sets_json:
if not "clashes" in clash_set.keys():
self.report({"WARNING"}, "No clashes found for the selected Clash Set.")
return {"CANCELLED"}
active_clash_set.clashes.clear()
for clash in clash_set["clashes"].values():
blender_clash = active_clash_set.clashes.add()
blender_clash.a_global_id = clash["a_global_id"]
blender_clash.b_global_id = clash["b_global_id"]
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
return {"FINISHED"}
class SaveIfcClashes(bpy.types.Operator):
bl_idname = "bim.save_ifc_clashes"
bl_label = "Save IFC Clashes"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Save the clashing IFC geometry stored in a file"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
WindowManager = context.window_manager
WindowManager.fileselect_add(self)
return {"RUNNING_MODAL"}
def execute(self, context):
self.file = IfcStore.get_file()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
clash_sets_json = self.load_json()
active_clash_set = context.scene.BIMClashProperties.active_clash_set
self.update_clash_sets(clash_sets_json, active_clash_set)
self.save_json(clash_sets_json)
return {"FINISHED"}
def load_json(self):
with open(self.filepath) as f:
return json.load(f)
def update_clash_sets(self, clash_sets_json, active_clash_set):
for clash_set in clash_sets_json:
if clash_set["name"] != active_clash_set.name or "clashes" not in clash_set.keys():
continue
for clash in active_clash_set.clashes:
clash_key = clash.a_global_id + "-" + clash.b_global_id
if clash_set.get("clashes", {}).get(clash_key, None) is not None:
clash_set["clashes"][clash_key]["status"] = clash.status
def save_json(self, clash_sets_json):
with open(self.filepath, "w") as destination:
json.dump(clash_sets_json, destination, indent=4)
class SelectClash(bpy.types.Operator):
bl_idname = "bim.select_clash"
bl_label = "Select Clash"
@@ -437,31 +357,29 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
clash = context.scene.BIMClashProperties.active_clash
self.props = context.scene.BIMClashProperties
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id)
if not clash:
return {"FINISHED"}
products = []
linked_objects = []
try:
products.append(tool.Ifc.get().by_guid(clash.a_global_id))
except:
linked_objects.append(clash.a_name)
try:
products.append(tool.Ifc.get().by_guid(clash.b_global_id))
except:
linked_objects.append(clash.b_name)
for global_id in (clash["a_global_id"], clash["b_global_id"]):
try:
products.append(tool.Ifc.get().by_guid(global_id))
except:
pass
tool.Spatial.select_products(products, unhide=True)
print(linked_objects)
for obj_name in linked_objects:
obj = bpy.data.objects.get(obj_name)
if obj:
obj.select_set(True)
else:
print("Object not found:", obj_name)
if context.scene.BIMClashProperties.sould_focus_on_clash:
context_override = tool.Blender.get_viewport_context()
with bpy.context.temp_override(**context_override):
bpy.ops.view3d.view_selected()
ClashDecorator.install(bpy.context)
target = Vector(clash["p1"])
tool.Clash.look_at(target, target + Vector((5, 5, 5)))
self.props.p1 = clash["p1"]
self.props.p2 = clash["p2"]
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
return {"FINISHED"}
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.prop import StrProperty, Attribute, BIMFilterGroup
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -33,11 +33,12 @@ from bpy.props import (
class ClashSource(PropertyGroup):
name: StringProperty(name="File")
selector: StringProperty(name="Selector")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
mode: EnumProperty(
items=[
("i", "Include", "Only the selected objects are included for clashing"),
("e", "Exclude", "All objects except the selected objects are included for clashing"),
("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"),
("e", "Exclude", "All elements except the selected elements are included for clashing"),
],
name="Mode",
)
@@ -53,7 +54,24 @@ class Clash(PropertyGroup):
class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
tolerance: FloatProperty(name="Tolerance")
mode: EnumProperty(
items=[
(
"intersection",
"Intersection",
"Detect objects that protrude or pierce another object",
"PIVOT_MEDIAN",
1,
),
("collision", "Collision", "Detect touching objects with any surface collision", "PIVOT_INDIVIDUAL", 2),
("clearance", "Clearance", "Detect objects within a proximity threshold", "PIVOT_ACTIVE", 3),
],
name="Mode",
)
tolerance: FloatProperty(name="Tolerance", default=0.002)
clearance: FloatProperty(name="Clearance", default=0.01)
allow_touching: BoolProperty(name="Allow Touching", default=False)
check_all: BoolProperty(name="Check All", default=False)
a: CollectionProperty(name="Group A", type=ClashSource)
b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash)
@@ -78,22 +96,21 @@ class BIMClashProperties(PropertyGroup):
smart_clash_grouping_max_distance: IntProperty(
name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10
)
sould_focus_on_clash: BoolProperty(name="Show Focus Clash", default=False)
p1: FloatVectorProperty(name="P1", default=(0.0, 0.0, 0.0), subtype="XYZ")
p2: FloatVectorProperty(name="P2", default=(0.0, 0.0, 0.0), subtype="XYZ")
active_clash_text: StringProperty(name="Active Clash Text")
@property
def active_clash_set(self):
if not self.clash_sets:
return None
return self.clash_sets[self.active_clash_set_index]
if self.active_clash_set_index < len(self.clash_sets):
return self.clash_sets[self.active_clash_set_index]
@property
def active_smart_group(self):
if not self.smart_clash_groups:
return None
return self.smart_clash_groups[self.active_smart_group_index]
if self.active_smart_group_index < len(self.smart_clash_groups):
return self.smart_clash_groups[self.active_smart_group_index]
@property
def active_clash(self):
if not self.active_clash_set:
return None
return self.active_clash_set.clashes[self.active_clash_index]
if self.active_clash_index < len(self.active_clash_set.clashes):
return self.active_clash_set.clashes[self.active_clash_index]
@@ -17,7 +17,9 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import blenderbim.bim.helper
from bpy.types import Panel
from blenderbim.bim.module.clash.data import ClashData
class BIM_PT_ifcclash(Panel):
@@ -30,6 +32,9 @@ class BIM_PT_ifcclash(Panel):
bl_parent_id = "BIM_PT_tab_clash_detection"
def draw(self, context):
if not ClashData.is_loaded:
ClashData.load()
layout = self.layout
scene = context.scene
@@ -45,57 +50,83 @@ class BIM_PT_ifcclash(Panel):
layout.template_list("BIM_UL_clash_sets", "", props, "clash_sets", props, "active_clash_set_index")
if props.active_clash_set_index < len(props.clash_sets):
clash_set = props.active_clash_set
if not props.active_clash_set:
return
row = layout.row(align=True)
row.prop(clash_set, "name")
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
clash_set = props.active_clash_set
row = layout.row(align=True)
row = layout.row(align=True)
row.prop(clash_set, "name")
row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index
row = layout.row()
row.prop(clash_set, "mode")
if clash_set.mode == "intersection":
row = layout.row()
row.prop(clash_set, "tolerance")
layout.label(text="Group A:")
row = layout.row()
row.operator("bim.add_clash_source").group = "a"
for index, source in enumerate(clash_set.a):
row = layout.row(align=True)
row.prop(source, "name", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
row = layout.row(align=True)
row.prop(source, "mode", text="")
row.prop(source, "selector", text="")
layout.label(text="Group B:")
row.prop(clash_set, "check_all")
elif clash_set.mode == "collision":
row = layout.row()
row.operator("bim.add_clash_source").group = "b"
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
row = layout.row(align=True)
row.prop(source, "mode", text="")
row.prop(source, "selector", text="")
row.prop(clash_set, "allow_touching")
elif clash_set.mode == "clearance":
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
row.prop(clash_set, "clearance")
row = layout.row()
row.prop(clash_set, "check_all")
row = layout.row(align=True)
row.label(text="Group A:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "a"
for index, source in enumerate(clash_set.a):
row = layout.row(align=True)
row.operator("bim.execute_ifc_clash")
row.operator("bim.select_ifc_clash_results")
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "a"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "a"
if source.mode != "a":
blenderbim.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_a_{index}"
)
row = layout.row(align=True)
row.label(text="Group B:", icon="OUTLINER_OB_POINTCLOUD")
row.operator("bim.add_clash_source", icon="ADD", text="").group = "b"
for index, source in enumerate(clash_set.b):
row = layout.row(align=True)
row.prop(source, "name", text="")
row.prop(source, "mode", text="")
op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="")
op.index = index
op.group = "b"
op = row.operator("bim.remove_clash_source", icon="X", text="")
op.index = index
op.group = "b"
if source.mode != "a":
blenderbim.bim.helper.draw_filter(
layout, source.filter_groups, ClashData, f"clash_{props.active_clash_set_index}_b_{index}"
)
row = layout.row()
row.prop(props, "should_create_clash_snapshots")
row = layout.row()
row.operator("bim.execute_ifc_clash")
row = layout.row()
row.label(text=f"{len(clash_set.clashes)} Clashes Found", icon="PIVOT_CURSOR")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
row = layout.row()
row.operator("bim.select_clash")
class BIM_PT_clash_manager(Panel):
@@ -111,29 +142,6 @@ class BIM_PT_clash_manager(Panel):
pass
class BIM_PT_dumb_clash_manager(Panel):
bl_idname = "BIM_PT_dumb_clash_manager"
bl_label = "Dumb Manager"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_clash_manager"
def draw(self, context):
layout = self.layout
props = context.scene.BIMClashProperties
row = layout.row(align=True)
row.operator("bim.load_ifc_clashes", text="LOAD IFC CLASHES", icon="IMPORT")
row.operator("bim.save_ifc_clashes", text="SAVE CLASH RESULTS", icon="EXPORT")
layout.template_list("BIM_UL_clashes", "", props.active_clash_set, "clashes", props, "active_clash_index")
# bim.select_clash
row = layout.row(align=True)
row.operator("bim.select_clash", text="SELECT CLASH", icon="IMPORT")
row.prop(props, "sould_focus_on_clash", icon="RESTRICT_VIEW_OFF")
class BIM_PT_smart_clash_manager(Panel):
bl_idname = "BIM_PT_smart_clash_manager"
bl_label = "Smart Clash Manager"
@@ -293,7 +293,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
identification = active_reference[1]
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
references = ifcopenshell.util.classification.get_references(element, should_inherit=False)
for reference in references:
@@ -358,7 +358,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
break
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
if not ifc_definition_id:
continue
ifcopenshell.api.run(
@@ -406,7 +406,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
classification.Location = bsdd_classification.domain_namespace_uri
for obj in objects:
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
if not ifc_definition_id:
continue
element = tool.Ifc.get().by_id(ifc_definition_id)
@@ -191,11 +191,11 @@ class CoveringToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
else:
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
row = cls.layout.row()
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
@@ -75,6 +75,9 @@ class BIM_PT_debug(Panel):
row = layout.row()
row.operator("bim.profile_import_ifc")
row = layout.row()
row.operator("bim.generate_global_id")
row = layout.split(factor=0.5, align=True)
row.operator("bim.create_shape_from_step_id").should_include_curves = False
row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True
@@ -38,6 +38,7 @@ class DiffData:
cls.data["total_deleted"] = cls.total_deleted()
cls.data["total_changed"] = cls.total_changed()
cls.data["changes"] = cls.changes()
cls.data["saved_searches"] = cls.saved_searches()
cls.is_loaded = True
@classmethod
@@ -84,3 +85,18 @@ class DiffData:
elif element.GlobalId in diff["deleted"]:
changes["Deleted"] = True
return changes
@classmethod
def saved_searches(cls):
if not tool.Ifc.get():
return []
groups = tool.Ifc.get().by_type("IfcGroup")
results = []
for group in groups:
try:
data = json.loads(group.Description)
if isinstance(data, dict) and data.get("type", None) == "BBIM_Search" and data.get("query", None):
results.append(group)
except:
pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")]
@@ -135,6 +135,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
bl_label = "Execute IFC Diff"
filename_ext = ".json"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def invoke(self, context, event):
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json")
@@ -162,7 +163,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
new = ifcopenshell.open(self.props.new_file)
relationships = [r.relationship for r in self.props.diff_relationships]
query = self.props.diff_filter_elements
query = tool.Search.export_filter_query(self.props.filter_groups) or None
ifc_diff = ifcdiff.IfcDiff(old, new, relationships=relationships, filter_elements=query)
ifc_diff.diff()
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.prop import StrProperty
from blenderbim.bim.prop import StrProperty, BIMFilterGroup
from blenderbim.bim.module.diff.data import DiffData
from bpy.types import PropertyGroup
from bpy.props import (
@@ -48,7 +48,7 @@ class DiffProperties(PropertyGroup):
old_file: StringProperty(default="", name="Old IFC File")
new_file: StringProperty(default="", name="New IFC File")
diff_relationships: CollectionProperty(type=Relationships, name="Relationships")
diff_filter_elements: StringProperty(default="", name="Filter")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
should_load_changed_elements: BoolProperty(name="Load Changed Elements", default=True)
active_file: EnumProperty(
items=[
@@ -19,8 +19,8 @@
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.diff.data import DiffData
import blenderbim.bim.helper
import blenderbim.tool as tool
import json
class BIM_PT_diff(Panel):
@@ -93,9 +93,7 @@ class BIM_PT_diff(Panel):
remove.collection = "diff_relationships"
remove.index = index
row = layout.row(align=True)
row.prop(props, "diff_filter_elements")
row.operator("bim.ifc_selector", icon="FILTER", text="")
blenderbim.bim.helper.draw_filter(self.layout, props.filter_groups, DiffData, "diff")
row = layout.row()
row.operator("bim.execute_ifc_diff")
@@ -41,6 +41,7 @@ classes = (
operator.CleanWireframes,
operator.ContractSheet,
operator.ContractTargetView,
operator.ConvertSVGToDXF,
operator.CreateDrawing,
operator.CreateSheets,
operator.DisableAddAnnotationType,
@@ -188,7 +188,6 @@ def format_distance(
feet += 1
inches = 0
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
@@ -299,7 +298,9 @@ def parse_diagram_scale(camera):
return float(numerator) / float(denominator)
def ortho_view_frame(camera, margin=0.015):
def ortho_view_frame(
camera: bpy.types.Camera, margin: float = 0.015
) -> tuple[float, float, float, float, float, float]:
"""Calculates 2d bounding box of camera view area.
Similar to `bpy.types.Camera.view_frame`
@@ -44,7 +44,7 @@ import blenderbim.bim.module.drawing.helper as helper
import blenderbim.bim.export_ifc
from blenderbim.bim.module.drawing.decoration import CutDecorator
from blenderbim.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List
from typing import NamedTuple, List, Union
from lxml import etree
from mathutils import Vector, Color, Matrix
from timeit import default_timer as timer
@@ -1345,7 +1345,7 @@ class SelectAllDrawings(bpy.types.Operator):
bl_label = "Select All Drawings"
view: bpy.props.StringProperty()
bl_description = "Select all drawings in the drawing list.\n\n" + "SHIFT+CLICK to deselect all drawings"
select_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"})
select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"})
def invoke(self, context, event):
# deselect all drawings on shift+click
@@ -1451,10 +1451,6 @@ class ActivateModel(bpy.types.Operator):
class ActivateDrawing(bpy.types.Operator):
"""
Activates the selected drawing view
"""
bl_idname = "bim.activate_drawing"
bl_label = "Activate Drawing"
bl_options = {"REGISTER", "UNDO"}
@@ -2689,3 +2685,50 @@ class AddReferenceImage(bpy.types.Operator, Operator):
)
tool.Style.reload_material_from_ifc(material)
tool.Geometry.record_object_materials(obj)
class ConvertSVGToDXF(bpy.types.Operator):
bl_idname = "bim.convert_svg_to_dxf"
bl_label = "Convert SVG to DXF"
bl_options = {"REGISTER", "UNDO"}
view: bpy.props.StringProperty()
bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings"
convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
# convert all drawings on shift+click
# make sure to use SKIP_SAVE on property, otherwise it might get stuck
if event.type == "LEFTMOUSE" and event.shift:
self.open_all = True
return self.execute(context)
def execute(self, context):
if self.convert_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected
]
else:
drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
drawing_uris: list[Path] = []
drawings_not_found: list[str] = []
for drawing in drawings:
drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(drawing))
if drawing_uri is None or not os.path.exists(drawing_uri):
drawings_not_found.append(drawing.Name)
else:
drawing_uris.append(Path(drawing_uri))
if drawings_not_found:
msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format(
"\n".join(drawings_not_found)
)
self.report({"ERROR"}, msg)
return {"CANCELLED"}
for drawing_uri in drawing_uris:
tool.Drawing.convert_svg_to_dxf(drawing_uri, drawing_uri.with_suffix(".dxf"))
self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.")
return {"FINISHED"}
@@ -268,6 +268,11 @@ class BIM_PT_drawings(Panel):
col = row.column()
col.alignment = "RIGHT"
convert_to_dxf = row.row(align=True)
op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA")
op.view = active_drawing.name
convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0
op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="")
open_drawing_button = row.row(align=True)
@@ -250,7 +250,7 @@ class AnnotationToolUI:
row = cls.layout.row(align=True)
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.launch_annotation_type_manager", icon="LIGHTPROBE_GRID", text="")
row.operator("bim.launch_annotation_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
@@ -21,12 +21,14 @@ import bmesh
import mathutils
import ifcopenshell
import ifcopenshell.util.unit
import blenderbim.tool as tool
from math import pi, pow
from mathutils import Vector, Matrix, geometry
from typing import Union
class Helper:
def __init__(self, file):
def __init__(self, file: ifcopenshell.file):
self.file = file
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -34,7 +36,7 @@ class Helper:
# edge that shares a single vertex only with that face to find the extrusion
# edge. A face with the normal facing down is prioritised. A limited
# dissolve ensure that faces are quads and not tris.
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh):
def auto_detect_rectangle_profile_extruded_area_solid(self, mesh: bpy.types.Mesh) -> dict:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
@@ -115,7 +117,9 @@ class Helper:
return {"profile": profile, "extrusion": extrusion}
def auto_detect_arbitrary_profile_with_voids(self, obj, mesh):
def auto_detect_arbitrary_profile_with_voids(
self, obj: bpy.types.Object, mesh: bpy.types.Mesh
) -> Union[tuple, dict]:
groups = {"IFCARCINDEX": [], "IFCCIRCLE": []}
for i, group in enumerate(obj.vertex_groups):
if "IFCARCINDEX" in group.name:
@@ -138,28 +142,31 @@ class Helper:
total_groups = 0
is_circle = False
for group_type, group_indices in groups.items():
for group_index in group_indices:
if group_index in vert[deform_layer]:
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if not group_indices:
continue
is_special, group_index = tool.Blender.bmesh_check_vertex_in_groups(vert, deform_layer, group_indices)
if not is_special:
continue
if group_type == "IFCCIRCLE":
is_circle = True
group_verts[group_type].setdefault(group_index, 0)
group_verts[group_type][group_index] += 1
total_groups += 0
if total_groups > 1: # A vert can only belong to one group
return (False, "AMBIGUOUS_SPECIAL_VERTEX")
elif is_circle:
pass # Circles are allowed to be unclosed
pass # Circles are allowed to be unclosed
elif total_groups == 0 and len(vert.link_edges) != 2: # Unclosed loop or forked loop
return (False, "UNCLOSED_LOOP")
for group_type, group_counts in group_verts.items():
if group_type == "IFCARCINDEX":
for group_count in group_counts.values():
if group_count != 3: # Each arc needs 3 verts
if group_count != 3: # Each arc needs 3 verts
return (False, "3POINT_ARC")
elif group_type == "IFCCIRCLE":
for group_count in group_counts.values():
if group_count != 2: # Each circle needs 2 verts
if group_count != 2: # Each circle needs 2 verts
return (False, "CIRCLE")
loop_edges = set(bm.edges)
@@ -269,7 +276,7 @@ class Helper:
inner_loops.remove(outer_loop)
# Copy vectors to prevent random data mangling after bmesh is freed.
points = [Vector(list(v.co)) for v in bm.verts]
points = [v.co.copy() for v in bm.verts]
bm.to_mesh(mesh)
mesh.update()
@@ -908,7 +908,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
index += 1
pset = tool.Ifc.get().by_id(pset['id'])
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
@@ -976,26 +976,23 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if parts:
index = DuplicateMoveLinkedAggregate.get_max_index(parts)
add_linked_aggregate_pset(element, index)
index +=1
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
obj = tool.Ifc.get_object(part)
obj.select_set(True)
def add_linked_aggregate_pset(part, index):
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
if not pset:
pset = ifcopenshell.api.run(
"pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name
)
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=part, name=self.pset_name)
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
@@ -1029,21 +1026,20 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if r.is_a("IfcRelAssignsToGroup")
if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name
][0]
number = len(group_elements) - 1
number = f"{number:02d}"
new_obj = tool.Ifc.get_object(new[0])
pattern1 = r'_\d'
pattern1 = r"_\d"
if re.findall(pattern1, new_obj.name):
split_name = new_obj.name.split("_")
new_obj.name = split_name[0] + "_" + number
continue
pattern2 = r'\.\d{3}'
pattern2 = r"\.\d{3}"
if re.findall(pattern2, new_obj.name):
split_name = new_obj.name.split(".")
new_obj.name = split_name[0] + "_" + number
if len(context.selected_objects) != 1:
return {"FINISHED"}
@@ -1065,7 +1061,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
old_to_new = OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context, linked=True)
custom_incremental_naming_for_element_assembly(old_to_new)
# Recreate aggregate relationship
for old in old_to_new.keys():
if old.is_a("IfcElementAssembly"):
@@ -1075,17 +1071,15 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
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]
index = [i["Index"] for i in psets if i]
if len(index) > 0:
index = max(index)
return index
else:
return 0
class RefreshLinkedAggregate(bpy.types.Operator):
@@ -1134,9 +1128,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
original_names[group] = {}
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset['Index']
index = pset["Index"]
original_names[group][index] = tool.Ifc.get_object(element).name
parts = ifcopenshell.util.element.get_parts(element)
if parts:
for part in parts:
@@ -1144,20 +1138,22 @@ class RefreshLinkedAggregate(bpy.types.Operator):
original_names | get_original_names(part)
else:
try:
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
pset = ifcopenshell.util.element.get_pset(part, self.pset_name)
except:
continue
index = pset['Index']
continue
index = pset["Index"]
original_names[group][index] = tool.Ifc.get_object(part).name
return original_names
def set_original_name(obj, original_names):
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if ifcopenshell.util.element.get_parts(element): # if element has parts it means it is the base of and aggregate or sub-aggregate
if ifcopenshell.util.element.get_parts(
element
): # if element has parts it means it is the base of and aggregate or sub-aggregate
aggregate = element
group = [
r.RelatingGroup
for r in getattr(aggregate, "HasAssignments", []) or []
@@ -1166,18 +1162,16 @@ class RefreshLinkedAggregate(bpy.types.Operator):
]
if not group:
return
group = group[0].id()
pset = ifcopenshell.util.element.get_pset(element, self.pset_name)
index = pset['Index']
index = pset["Index"]
try:
obj.name = original_names[group][index]
except:
return
def get_element_assembly(element):
if element.is_a("IfcElementAssembly"):
@@ -1263,9 +1257,9 @@ class RefreshLinkedAggregate(bpy.types.Operator):
selected_matrix = selected_obj.matrix_world
object_duplicate = tool.Ifc.get_object(element)
duplicate_matrix = object_duplicate.matrix_world.decompose()
original_names = get_original_names(element)
delete_objects(element)
for obj in context.selected_objects:
@@ -1280,24 +1274,24 @@ class RefreshLinkedAggregate(bpy.types.Operator):
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
for old, new in old_to_new.items():
if element_aggregate and new[0].is_a("IfcElementAssembly"):
new_aggregate = ifcopenshell.util.element.get_aggregate(new[0])
if not new_aggregate:
blenderbim.core.aggregate.assign_object(
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(element_aggregate),
related_obj=tool.Ifc.get_object(new[0]),
)
tool.Ifc,
tool.Aggregate,
tool.Collector,
relating_obj=tool.Ifc.get_object(element_aggregate),
related_obj=tool.Ifc.get_object(new[0]),
)
for old, new in old_to_new.items():
new_obj = tool.Ifc.get_object(new[0])
set_original_name(new_obj, original_names)
blenderbim.bim.handler.refresh_ui_data()
operator_time = time() - refresh_start_time
@@ -212,7 +212,7 @@ class BIM_PT_representation_items(Panel):
if props.is_editing_item_style:
# NOTE: we currently support 1 item having just 1 style
# when IfcStyledItem can actually have multiple styles
row.prop(props, "representation_item_style", icon="SHADING_RENDERED", text="")
prop_with_search(row, props, "representation_item_style", icon="SHADING_RENDERED", text="")
row.operator("bim.edit_representation_item_style", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_representation_item_style", icon="CANCEL", text="")
else:
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell
import ifcopenshell.util.doc
import ifcopenshell.util.schema
import blenderbim.tool as tool
@@ -109,29 +110,41 @@ class MaterialsData:
def active_styles(cls):
props = bpy.context.scene.BIMMaterialProperties
results = []
if props.materials and props.active_material_index < len(props.materials):
material = props.materials[props.active_material_index].ifc_definition_id
if not material:
return results
material = tool.Ifc.get().by_id(material)
for definition in material.HasRepresentation:
for representation in definition.Representations:
if not representation.is_a("IfcStyledRepresentation"):
if not props.materials or props.active_material_index >= len(props.materials):
return results
material = props.materials[props.active_material_index].ifc_definition_id
if not material:
return results
material = tool.Ifc.get_entity_by_id(material)
# NOTE: it's possible that data will be refreshed during material removal
# (when it will try to fetch active material type and will reach for material_types)
# and props.materials[i].ifc_definition_id will contain already removed id
if not material or not material.is_a("IfcMaterial"):
return results
for definition in material.HasRepresentation:
for representation in definition.Representations:
if not representation.is_a("IfcStyledRepresentation"):
continue
context = representation.ContextOfItems
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
continue
context = representation.ContextOfItems
for item in representation.Items:
if not item.is_a("IfcStyledItem"):
continue
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
results.append({
for style in item.Styles:
if style.is_a("IfcSurfaceStyle"):
results.append(
{
"context_type": context.ContextType,
"context_identifier": getattr(context, "ContextIdentifier", ""),
"target_view": getattr(context, "TargetView", ""),
"name": style.Name or "Unnamed",
"id": style.id(),
"context_id": context.id(),
})
}
)
return results
@@ -118,7 +118,7 @@ class AddMaterial(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_material"
bl_label = "Add Material"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj: bpy.props.StringProperty(name="Material Name")
name: bpy.props.StringProperty(default="Default")
def invoke(self, context, event):
@@ -23,6 +23,7 @@ from math import sin, cos, radians
from bpy.types import SpaceView3D
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from typing import Union
ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
@@ -35,15 +36,6 @@ def transparent_color(color, alpha=0.1):
return color
def bm_check_vertex_in_groups(vertex, deform_layer, groups):
"""returns tuple boolean (whether vertex is in any of the groups)
and related group index"""
for group_index in vertex[deform_layer].keys():
if group_index in groups:
return True, group_index
return False, None
class ProfileDecorator:
installed = None
@@ -148,12 +140,12 @@ class ProfileDecorator:
# deform_layer is None if there are no verts assigned to vertex groups
# even if there are vertex groups in the obj.vertex_groups
if deform_layer:
is_arc, group_index = bm_check_vertex_in_groups(vertex, deform_layer, arc_groups)
is_arc, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, arc_groups)
if is_arc:
arcs.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
is_circle, group_index = bm_check_vertex_in_groups(vertex, deform_layer, circle_groups)
is_circle, group_index = tool.Blender.bmesh_check_vertex_in_groups(vertex, deform_layer, circle_groups)
if is_circle:
circles.setdefault(group_index, []).append(vertex)
special_vertex_indices[vertex.index] = group_index
@@ -20,9 +20,12 @@ import bpy
import json
import bmesh
import ifcopenshell
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.api
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import ifcopenshell.util.type
import blenderbim.bim.handler
import blenderbim.core.type
import blenderbim.core.geometry
@@ -698,9 +701,7 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
new_footprint = ifcopenshell.api.run(
"geometry.add_footprint_representation", tool.Ifc.get(), context=footprint_context, curves=curves
)
old_footprint = ifcopenshell.util.representation.get_representation(
element, "Plan", "FootPrint", "SKETCH_VIEW"
)
old_footprint = ifcopenshell.util.representation.get_representation(element, "Plan", "FootPrint", "SKETCH_VIEW")
if old_footprint:
for inverse in tool.Ifc.get().get_inverse(old_footprint):
ifcopenshell.util.element.replace_attribute(inverse, old_footprint, new_footprint)
@@ -516,7 +516,7 @@ class BimToolUI:
prop_with_search(row, cls.props, "relating_type_id", text="")
else:
row.label(text="No Construction Type", icon="FILE_3D")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
else:
if AuthoringData.data["ifc_element_type"]:
row.label(text=f"No {AuthoringData.data['ifc_element_type']} Found", icon="ERROR")
@@ -529,7 +529,7 @@ class BimToolUI:
op.ifc_element_type = AuthoringData.data["ifc_element_type"]
else:
row.label(text="No Element Types Found", icon="ERROR")
row.operator("bim.launch_type_manager", icon="LIGHTPROBE_GRID", text="Launch Type Manager")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
@@ -113,6 +113,10 @@ class CreateProject(bpy.types.Operator):
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
for mesh in bpy.data.meshes:
bpy.data.meshes.remove(mesh)
for mat in bpy.data.materials:
bpy.data.materials.remove(mat)
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
tool.Blender.register_toolbar()
@@ -856,7 +860,10 @@ class LinkIfc(bpy.types.Operator):
continue
new = context.scene.BIMProjectProperties.links.add()
if self.use_relative_path:
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
try:
filepath = os.path.relpath(filepath, bpy.path.abspath("//")).replace("\\", "/")
except:
pass # Perhaps on another drive or something
new.name = filepath
bpy.ops.bim.load_link(filepath=filepath, false_origin=self.false_origin)
print(f"Finished linking {len(files)} IFCs", time.time() - start)
@@ -899,11 +906,11 @@ class UnloadLink(bpy.types.Operator):
if filepath.suffix.lower() == ".ifc":
filepath = filepath.with_suffix(".ifc.cache.blend")
for collection in context.scene.collection.children:
for collection in context.scene.collection.children[:]:
if collection.library and Path(collection.library.filepath) == filepath:
context.scene.collection.children.unlink(collection)
bpy.data.collections.remove(collection)
for scene in bpy.data.scenes:
for scene in bpy.data.scenes[:]:
if scene.library and Path(scene.library.filepath) == filepath:
bpy.data.scenes.remove(scene)
@@ -1008,24 +1015,24 @@ class ToggleLinkSelectability(bpy.types.Operator):
bl_label = "Toggle Link Selectability"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle selectability"
link: bpy.props.StringProperty()
link: bpy.props.StringProperty(name="Linked IFC Filepath")
def execute(self, context):
props = context.scene.BIMProjectProperties
link = props.links.get(self.link)
self.filepath = self.link
if not os.path.isabs(self.filepath):
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
if not os.path.isabs(self.link):
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
for collection in self.get_linked_collections():
collection.hide_select = not collection.hide_select
link.is_selectable = not collection.hide_select
return {"FINISHED"}
def get_linked_collections(self):
def get_linked_collections(self) -> list[bpy.types.Collection]:
return [
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
]
@@ -1034,15 +1041,15 @@ class ToggleLinkVisibility(bpy.types.Operator):
bl_label = "Toggle Link Visibility"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle visibility between SOLID and WIREFRAME"
link: bpy.props.StringProperty()
mode: bpy.props.StringProperty()
link: bpy.props.StringProperty(name="Linked IFC Filepath")
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
def execute(self, context):
props = context.scene.BIMProjectProperties
link = props.links.get(self.link)
self.filepath = self.link
if not os.path.isabs(self.filepath):
self.filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.filepath)).replace("\\", "/")
if not os.path.isabs(self.link):
self.link = os.path.abspath(os.path.join(bpy.path.abspath("//"), self.link))
self.library_filepath = Path(self.link).with_suffix(".ifc.cache.blend")
if self.mode == "WIREFRAME":
self.toggle_wireframe(link)
elif self.mode == "VISIBLE":
@@ -1063,25 +1070,17 @@ class ToggleLinkVisibility(bpy.types.Operator):
def toggle_visibility(self, link):
linked_collections = self.get_linked_collections()
queue = [bpy.context.view_layer.layer_collection]
layer_collection = None
while queue:
layer = queue.pop()
if layer.collection in linked_collections:
layer_collection = layer
break
queue.extend(list(layer.children))
if layer_collection:
layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections)
for layer_collection in layer_collections.values():
layer_collection.exclude = not layer_collection.exclude
link.is_hidden = layer_collection.exclude
def get_linked_collections(self):
def get_linked_collections(self) -> list[bpy.types.Collection]:
return [
c
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and c.library.filepath.replace("\\", "/") == self.filepath
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == self.library_filepath
]
@@ -1295,6 +1294,8 @@ class LoadLinkedProject(bpy.types.Operator):
if len(shape.geometry.faces) > 1000 and self.is_local(shape): # 333 tris
self.process_occurrence(shape)
if not iterator.next():
if not chunked_verts:
break
mats = np.concatenate(chunked_materials)
midx = np.concatenate(chunked_material_ids)
mats, mapping = np.unique(mats, axis=0, return_inverse=True)
@@ -85,7 +85,6 @@ class EnablePsetEditing(bpy.types.Operator):
def execute(self, context):
self.props = get_pset_props(context, self.obj, self.obj_type)
self.props.properties.clear()
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
if self.pset_id:
pset = tool.Ifc.get().by_id(self.pset_id)
@@ -250,7 +249,7 @@ class DisablePsetEditing(bpy.types.Operator, Operator):
props = get_pset_props(context, self.obj, self.obj_type)
if props.active_pset_id:
pset = tool.Ifc.get().by_id(props.active_pset_id)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
if tool.Pset.is_pset_empty(pset):
ifcopenshell.api.run(
"pset.remove_pset", tool.Ifc.get(), product=tool.Ifc.get().by_id(ifc_definition_id), pset=pset
@@ -272,7 +271,7 @@ class EditPset(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
properties = {}
@@ -343,7 +342,7 @@ class RemovePset(bpy.types.Operator, Operator):
pset_name = tool.Ifc.get().by_id(self.pset_id).Name
for obj in objects:
props = get_pset_props(context, obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
pset = ifcopenshell.util.element.get_psets(element, should_inherit=False).get(pset_name, None)
if pset:
@@ -374,7 +373,7 @@ class AddQto(bpy.types.Operator, Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
props = get_pset_props(context, self.obj, self.obj_type)
ifc_definition_id = blenderbim.bim.helper.get_obj_ifc_definition_id(context, self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=props.qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
@@ -25,8 +25,16 @@ from shapely.geometry import Polygon
from shapely.ops import unary_union
import blenderbim.tool as tool
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.element
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
import blenderbim.bim
from typing import Literal, Union, Optional
AxisType = Literal["x", "y", "z"]
VectorTuple = tuple[float, float, float]
QuanityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"]
class QtoCalculator:
@@ -45,7 +53,7 @@ class QtoCalculator:
else:
self.mapping_dict[key][item] = None
def calculate_quantity(self, qto_name, quantity_name, obj):
def calculate_quantity(self, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
"""calculates the value of the quantity in the project units"""
string = "self.mapping_dict[qto_name][quantity_name](obj"
if isinstance(mapper[qto_name][quantity_name], dict):
@@ -54,11 +62,13 @@ class QtoCalculator:
args = ""
string += args
string += ")"
value = eval(string)
value: float = eval(string)
return tool.Qto.convert_to_project_units(value, qto_name, quantity_name) or value
def guess_quantity(self, prop_name, alternative_prop_names, obj):
def guess_quantity(
self, prop_name: str, alternative_prop_names: list[str], obj: bpy.types.Object
) -> Union[float, None]:
"""guess the value of the quantity by name, returns the value in the project units"""
prop_name = prop_name.lower()
alternative_prop_names = [p.lower() for p in alternative_prop_names]
@@ -89,7 +99,7 @@ class QtoCalculator:
if value is None:
return
unit_type_keywords = {
unit_type_keywords: dict[str, QuanityTypes] = {
"length": "Q_LENGTH",
"width": "Q_LENGTH",
"height": "Q_LENGTH",
@@ -102,10 +112,10 @@ class QtoCalculator:
unit_type = next(unit_type_keywords[k] for k in unit_type_keywords if k in prop_name)
return tool.Qto.convert_to_project_units(value, quantity_type=unit_type) or value
def get_units(self, o, vg_index):
def get_units(self, o: bpy.types.Object, vg_index: int) -> int:
return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]])
def get_linear_length(self, o):
def get_linear_length(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the length of the longest edge of the object bounding box
:param blender-object o: Blender Object
@@ -116,7 +126,7 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x, y, z)
def get_length(self, o, vg_index=None, main_axis: str = ""):
def get_length(self, o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "") -> float:
if vg_index is None:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
@@ -141,23 +151,23 @@ class QtoCalculator:
length += self.get_edge_distance(o, e)
return length
def get_stair_length(self, obj):
def get_stair_length(self, obj: bpy.types.Object) -> float:
length = self.get_length(obj)
height = self.get_height(obj)
stair_length = math.sqrt(pow(length, 2) + pow(height, 2))
return stair_length
def get_net_stair_area(self, obj):
def get_net_stair_area(self, obj: bpy.types.Object) -> float:
OBB_obj = self.get_OBB_object(obj)
OBB_net_footprint_area = self.get_net_footprint_area(OBB_obj)
return OBB_net_footprint_area
def get_gross_stair_area(self, obj):
def get_gross_stair_area(self, obj: bpy.types.Object) -> float:
OBB_obj = self.get_OBB_object(obj)
OBB_gross_footprint_area = self.get_gross_footprint_area(OBB_obj)
return OBB_gross_footprint_area
def get_parametric_axis(self, obj):
def get_parametric_axis(self, obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]:
relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj))
if relating_type:
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
@@ -172,7 +182,7 @@ class QtoCalculator:
return None
return None
def get_covering_gross_area(self, obj):
def get_covering_gross_area(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_gross_footprint_area(obj)
@@ -181,7 +191,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_gross_footprint_area(obj)
def get_covering_net_area(self, obj):
def get_covering_net_area(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_net_footprint_area(obj)
@@ -190,7 +200,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_net_footprint_area(obj)
def get_covering_width(self, obj):
def get_covering_width(self, obj: bpy.types.Object) -> float:
get_parametric_axis = self.get_parametric_axis(obj)
if not get_parametric_axis:
return self.get_height(obj)
@@ -199,7 +209,7 @@ class QtoCalculator:
elif get_parametric_axis == "AXIS3":
return self.get_height(obj)
def get_width(self, o):
def get_width(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the width of the object bounding box
:param blender-object o: blender object
@@ -209,7 +219,7 @@ class QtoCalculator:
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
return min(x, y)
def get_height(self, o):
def get_height(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the height of the object bounding box
:param blender-object o: blender object
@@ -217,32 +227,32 @@ class QtoCalculator:
"""
return (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
def get_opening_height(self, obj):
def get_opening_height(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_width(obj)
else:
return self.get_height(obj)
def get_opening_depth(self, obj):
def get_opening_depth(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_height(obj)
else:
return self.get_width(obj)
def get_opening_mapping_area(self, obj):
def get_opening_mapping_area(self, obj: bpy.types.Object) -> float:
if self.is_opening_horizontal(obj):
return self.get_net_footprint_area(obj)
else:
return self.get_net_side_area(obj)
def get_finish_ceiling_height(self, obj):
def get_finish_ceiling_height(self, obj: bpy.types.Object) -> float:
space_height = self.get_height(obj)
floor_height = self.get_finish_floor_height(obj)
ceiling_height = self.get_ceiling_height(obj)
finish_ceiling_height = space_height - floor_height - ceiling_height
return finish_ceiling_height
def get_finish_floor_height(self, obj):
def get_finish_floor_height(self, obj: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
finish_floor_height = 0
@@ -258,7 +268,7 @@ class QtoCalculator:
return finish_floor_height
def get_ceiling_height(self, obj):
def get_ceiling_height(self, obj: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
finish_ceiling_height = 0
@@ -274,7 +284,7 @@ class QtoCalculator:
return finish_ceiling_height
def get_net_perimeter(self, o):
def get_net_perimeter(self, o: bpy.types.Object) -> float:
parsed_edges = []
shared_edges = []
perimeter = 0
@@ -289,7 +299,7 @@ class QtoCalculator:
perimeter -= self.get_edge_key_distance(o, edge_key)
return perimeter
def get_gross_perimeter(self, o):
def get_gross_perimeter(self, o: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(o)
mesh = self.get_gross_element_mesh(element)
gross_obj = bpy.data.objects.new("GrossObj", mesh)
@@ -297,15 +307,15 @@ class QtoCalculator:
self.delete_obj(gross_obj)
return gross_perimeter
def get_space_net_perimeter(self, obj):
def get_space_net_perimeter(self, obj: bpy.types.Object) -> float:
pass
def get_rectangular_perimeter(self, obj):
def get_rectangular_perimeter(self, obj: bpy.types.Object) -> float:
length = self.get_length(obj, main_axis="x")
height = self.get_height(obj)
return (length + height) * 2
def get_lowest_polygons(self, o):
def get_lowest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
lowest_polygons = []
lowest_z = None
for polygon in o.data.polygons:
@@ -321,7 +331,7 @@ class QtoCalculator:
lowest_z = z
return lowest_polygons
def get_highest_polygons(self, o):
def get_highest_polygons(self, o: bpy.types.Object) -> list[bpy.types.MeshPolygon]:
highest_polygons = []
highest_z = None
for polygon in o.data.polygons:
@@ -337,13 +347,13 @@ class QtoCalculator:
highest_z = z
return highest_polygons
def get_edge_key_distance(self, obj, edge_key):
def get_edge_key_distance(self, obj: bpy.types.Object, edge_key: tuple[int, int]) -> float:
return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length
def get_edge_distance(self, obj, edge):
def get_edge_distance(self, obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float:
return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length
def get_net_floor_area(self, obj):
def get_net_floor_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_footprint_area(obj)
@@ -359,7 +369,7 @@ class QtoCalculator:
return total_net_floor_area
def get_gross_ceiling_area(self, obj):
def get_gross_ceiling_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_top_area(obj)
@@ -375,7 +385,7 @@ class QtoCalculator:
return total_gross_ceiling_area
def get_net_ceiling_area(self, obj):
def get_net_ceiling_area(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_net_top_area(obj)
@@ -395,7 +405,7 @@ class QtoCalculator:
return total_net_ceiling_area
def get_space_net_volume(self, obj):
def get_space_net_volume(self, obj: bpy.types.Object) -> float:
decompositions = self.get_obj_decompositions(obj)
if not decompositions:
return self.get_gross_volume(obj)
@@ -410,7 +420,7 @@ class QtoCalculator:
return total_space_net_volume
def get_net_footprint_area(self, o):
def get_net_footprint_area(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the area of the footprint of the object, excluding any holes
:param blender-object o: blender object
@@ -421,7 +431,7 @@ class QtoCalculator:
area += polygon.area
return area
def get_gross_footprint_area(self, o):
def get_gross_footprint_area(self, o: bpy.types.Object) -> float:
"""_summary_: Returns the area of the footprint of the object, without related opening and excluding any holes
:param blender-object o: blender object
@@ -437,7 +447,7 @@ class QtoCalculator:
self.delete_mesh(mesh)
return gross_footprint_area
def get_net_roofprint_area(self, o):
def get_net_roofprint_area(self, o: bpy.types.Object) -> float:
# Is roofprint the right word? Couldn't think of anything better - vulevukusej
"""_summary_: Returns the area of the net roofprint of the object, excluding any holes
@@ -449,7 +459,7 @@ class QtoCalculator:
area += polygon.area
return area
def get_side_area(self, o):
def get_side_area(self, o: bpy.types.Object) -> float:
# There are a few dumb options for this, but this seems the dumbest
# until I get more practical experience on what works best.
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
@@ -457,8 +467,7 @@ class QtoCalculator:
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return max(x * z, y * z)
def get_cross_section_area(self, obj):
element = tool.Ifc.get_entity(obj)
def get_cross_section_area(self, obj: bpy.types.Object) -> float:
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
item = representation.Items[0]
while True:
@@ -474,7 +483,7 @@ class QtoCalculator:
return area
# TODO handle other types of sections, and then fall back to mesh parsing
def get_gross_surface_area(self, o, vg_index=None):
def get_gross_surface_area(self, o: bpy.types.Object, vg_index: Optional[int] = None) -> float:
if vg_index is None:
if not self.has_openings(o):
return self.get_net_surface_area(o)
@@ -492,29 +501,29 @@ class QtoCalculator:
area += polygon.area
return area
def get_net_surface_area(self, obj):
def get_net_surface_area(self, obj: bpy.types.Object) -> float:
return self.get_mesh_area(obj.data)
def get_mesh_area(self, mesh):
def get_mesh_area(self, mesh: bpy.types.Mesh) -> float:
area = 0
for polygon in mesh.polygons:
area += polygon.area
return area
def is_polygon_in_vg(self, polygon, vertices_in_vg):
def is_polygon_in_vg(self, polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool:
for v in polygon.vertices:
if v not in vertices_in_vg:
return False
return True
def get_net_volume(self, o):
def get_net_volume(self, o: bpy.types.Object) -> float:
o_mesh = bmesh.new()
o_mesh.from_mesh(o.data)
volume = o_mesh.calc_volume()
o_mesh.free()
return volume
def get_gross_volume(self, o):
def get_gross_volume(self, o: bpy.types.Object) -> float:
if not self.has_openings(o):
return self.get_net_volume(o)
@@ -529,16 +538,18 @@ class QtoCalculator:
return gross_volume
def has_openings(self, obj):
def has_openings(
self, obj: bpy.types.Object
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
element = tool.Ifc.get_entity(obj)
return element and getattr(element, "HasOpenings", [])
def get_obj_decompositions(self, obj):
def get_obj_decompositions(self, obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]:
element = tool.Ifc.get_entity(obj)
decompositions = ifcopenshell.util.element.get_decomposition(element)
return decompositions
def get_gross_weight(self, obj):
def get_gross_weight(self, obj: bpy.types.Object) -> Union[float, None]:
obj_mass_density = self.get_obj_mass_density(obj)
if not obj_mass_density:
return
@@ -546,7 +557,7 @@ class QtoCalculator:
gross_weight = obj_mass_density * gross_volume
return gross_weight
def get_net_weight(self, obj):
def get_net_weight(self, obj: bpy.types.Object) -> Union[float, None]:
obj_mass_density = self.get_obj_mass_density(obj)
if not obj_mass_density:
return
@@ -554,7 +565,7 @@ class QtoCalculator:
net_weight = obj_mass_density * net_volume
return net_weight
def get_obj_mass_density(self, obj):
def get_obj_mass_density(self, obj: bpy.types.Object) -> Union[float, None]:
entity = tool.Ifc.get_entity(obj)
material = ifcopenshell.util.element.get_material(entity)
if material is None:
@@ -629,7 +640,7 @@ class QtoCalculator:
# volume += v1.dot(v2.cross(v3)) / 6.0
# return volume
def get_opening_type(self, opening, obj):
def get_opening_type(self, opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]:
"""_summary_: Returns the opening type - OPENING / RECESS
:param blender-object opening: blender opening object
@@ -649,14 +660,22 @@ class QtoCalculator:
return "OPENING" if ray_intersections % 2 == 0 else "RECESS"
def get_opening_area(
self, obj, angle_z1: int = 45, angle_z2: int = 135, min_area: int = 0, ignore_recesses: bool = False
):
self,
obj: bpy.types.Object,
angle_z1: int = 45,
angle_z2: int = 135,
min_area: int = 0,
ignore_recesses: bool = False,
) -> float:
"""_summary_: Returns the lateral area of the openings in the object.
:param obj: blender object
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area. Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored, defaults to 0
:param int angle_z1: Angle measured from the positive z-axis to the normal-vector of the opening area.
Openings with a normal_vector lower than this value will be ignored, defaults to 45
:param int angle_z2: Angle measured from the positive z-axis to the normal-vector of the opening area.
Openings with a normal_vector greater than this value will be ignored,defaults to 135
:param float min_area: Minimum opening area to consider. Values lower than this will be ignored,
defaults to 0
:param bool ignore_recesses: Toggle whether recess areas should be considered, defaults to False
:return float: Opening Area
"""
@@ -702,14 +721,14 @@ class QtoCalculator:
def get_lateral_area(
self,
obj,
obj: bpy.types.Object,
subtract_openings: bool = True,
exclude_end_areas: bool = False,
exclude_side_areas: bool = False,
angle_z1: int = 45,
angle_z2: int = 135,
main_axis: str = "",
):
) -> float:
"""_summary_
:param blender-object obj: blender object, bpy.types.Object
@@ -760,7 +779,7 @@ class QtoCalculator:
area += polygon.area
return area + total_opening_area
def get_gross_side_area(self, obj):
def get_gross_side_area(self, obj: bpy.types.Object) -> float:
if not self.has_openings(obj):
return self.get_net_side_area(obj)
@@ -768,15 +787,15 @@ class QtoCalculator:
return gross_side_area
def get_net_side_area(self, obj):
def get_net_side_area(self, obj: bpy.types.Object) -> float:
net_side_area = self.get_lateral_area(obj, exclude_end_areas=True, main_axis="x") / 2
return net_side_area
def get_outer_surface_area(self, obj):
def get_outer_surface_area(self, obj: bpy.types.Object) -> float:
outer_surface_area = self.get_lateral_area(obj, exclude_end_areas=True, angle_z1=0, angle_z2=360)
return outer_surface_area
def get_end_area(self, obj):
def get_end_area(self, obj: bpy.types.Object) -> float:
element = tool.Ifc.get_entity(obj)
gross_mesh = self.get_gross_element_mesh(element)
gross_obj = bpy.data.objects.new("MyObject", gross_mesh)
@@ -790,7 +809,7 @@ class QtoCalculator:
return end_area
def get_gross_top_area(self, obj, angle: int = 45):
def get_gross_top_area(self, obj: bpy.types.Object, angle: int = 45) -> float:
"""_summary_: Returns the gross top area of the object.
:param blender-object obj: blender object
@@ -827,12 +846,14 @@ class QtoCalculator:
return area + opening_area
# curently net top area is larger then projected area, because its taking into account internal polygons, or window sills
def get_net_top_area(self, obj, angle: int = 45, ignore_internal: bool = True):
def get_net_top_area(self, obj: bpy.types.Object, angle: int = 45, ignore_internal: bool = True) -> float:
"""_summary_: Returns the net top area of the object.
:param blender-object obj: blender object
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area. Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills), defaults to True
:param int angle: Angle measured from the positive z-axis to the normal-vector of the area.
Values lower than this will be ignored, defaults to 45
:param bool ignore_internal: Toggle whether internal areas should be subtracted (Like window sills),
defaults to True
:return float: Net Top Area
"""
z_axis = (0, 0, 1)
@@ -852,11 +873,11 @@ class QtoCalculator:
return area
def get_projected_area(self, obj, projection_axis: str = "z", is_gross: bool = True):
def get_projected_area(self, obj, projection_axis: AxisType = "z", is_gross: bool = True) -> float:
"""_summary_: Returns the projected area of the object.
:param blender-object obj: blender object
:param str projection_axis: Axis to project the area onto. Can be "X", "Y" or "Z"
:param str projection_axis: Axis to project the area onto. Can be "x", "y" or "z"
:param bool is_gross: if True, the projected area will include openings, if False, the projected area will exclude openings
:return float: Projected Area
"""
@@ -891,7 +912,7 @@ class QtoCalculator:
return projected_polygon.area + void_area
return projected_polygon.area
def get_OBB_object(self, obj):
def get_OBB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
"""_summary_: Returns the Oriented-Bounding-Box (OBB) of the object.
:param blender-object obj: Blender Object
@@ -932,7 +953,7 @@ class QtoCalculator:
return new_OBB_object
def get_AABB_object(self, obj):
def get_AABB_object(self, obj: bpy.types.Object) -> bpy.types.Object:
"""_summary_: Returns the Axis-Aligned-Bounding-Box (AABB) of the object.
:param blender-object obj: Blender Object
@@ -988,12 +1009,12 @@ class QtoCalculator:
def get_bisected_obj(
self,
obj,
plane_co_pos,
plane_no_pos,
plane_co_neg,
plane_no_neg,
):
obj: bpy.types.Object,
plane_co_pos: VectorTuple,
plane_no_pos: VectorTuple,
plane_co_neg: VectorTuple,
plane_no_neg: VectorTuple,
) -> bpy.types.Object:
"""_summary_: Returns the object bisected by two planes.
:param blender-object obj: Blender Object
@@ -1031,11 +1052,12 @@ class QtoCalculator:
return bis_obj
def get_total_contact_area(self, obj, class_filter: str = ["IfcElement"]):
def get_total_contact_area(self, obj: bpy.types.Object, class_filter: list[str] = ["IfcElement"]) -> float:
"""_summary_: Returns the total contact area of the object with other objects.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:param list [] class_filter: A list of classes used to filter the objects
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return float: Total contact area of the object with other objects.
"""
total_contact_area = 0
@@ -1046,11 +1068,12 @@ class QtoCalculator:
return total_contact_area
def get_touching_objects(self, obj, class_filter):
def get_touching_objects(self, obj: bpy.types.Object, class_filter: list[str]) -> list[bpy.types.Object]:
"""_summary_: Returns a list of objects that are touching the object.
:param blender-object obj: Blender Object
:param list [] class_filter: A list of classes used to filter the objects to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:param list [] class_filter: A list of classes used to filter the objects
to be considered for the calculation. Example: ["IfcWall"] or ["IfcWall", "IfcSlab"]
:return list: List of touching objects
"""
# rotate the object ever so slightly, otherwise bvhtree.overlap won't work properly. https://blender.stackexchange.com/a/275244/130742
@@ -1094,7 +1117,7 @@ class QtoCalculator:
return touching_objects
def get_contact_area(self, object1, object2):
def get_contact_area(self, object1: bpy.types.Object, object2: bpy.types.Object) -> float:
"""_summary_: Returns the contact area between two objects.
:param blender-object obj: Blender Object
@@ -1109,7 +1132,13 @@ class QtoCalculator:
total_area += self.get_intersection_between_polygons(object1, poly1, object2, poly2)
return total_area
def get_intersection_between_polygons(self, object1, poly1, object2, poly2):
def get_intersection_between_polygons(
self,
object1: bpy.types.Object,
poly1: bpy.types.MeshPolygon,
object2: bpy.types.Object,
poly2: bpy.types.MeshPolygon,
) -> float:
"""_summary_: Returns the intersection between two polygons.
:param blender-object object1: Blender Object
@@ -1152,7 +1181,9 @@ class QtoCalculator:
# TopologicalError - Generated Geometry might be invalid
return 0
def create_shapely_polygon(self, obj, polygon, trans_matrix):
def create_shapely_polygon(
self, obj: bpy.types.Object, polygon: bpy.types.MeshPolygon, trans_matrix: Matrix
) -> Polygon:
"""_summary_: Create a shapely polygon
:param blender-object obj: Blender Object
@@ -1171,12 +1202,14 @@ class QtoCalculator:
polygon_tuples.append((x, y))
return Polygon(polygon_tuples)
def get_gross_element_mesh(self, element):
def get_gross_element_mesh(self, element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
settings = ifcopenshell.geom.settings()
settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True)
return self.create_mesh_from_shape(element, settings)
def create_mesh_from_shape(self, element, settings=None):
def create_mesh_from_shape(
self, element: ifcopenshell.entity_instance, settings: Optional[ifcopenshell.geom.settings] = None
) -> bpy.types.Mesh:
if settings is None:
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, element)
@@ -1203,12 +1236,12 @@ class QtoCalculator:
mesh.update()
return mesh
def get_bmesh_from_mesh(self, mesh):
def get_bmesh_from_mesh(self, mesh: bpy.types.Mesh) -> bmesh.types.BMesh:
bm = bmesh.new()
bm.from_mesh(mesh)
return bm
def get_object_main_axis(self, o):
def get_object_main_axis(self, o: bpy.types.Object) -> AxisType:
"""_summary_: Returns the main object axis. Useful for profile-defined objects.
:param blender-object o: Blender Object
@@ -1227,18 +1260,18 @@ class QtoCalculator:
else:
return "x"
def is_opening_horizontal(self, o):
def is_opening_horizontal(self, o: bpy.types.Object) -> bool:
x = (Vector(o.bound_box[4]) - Vector(o.bound_box[0])).length
y = (Vector(o.bound_box[3]) - Vector(o.bound_box[0])).length
z = (Vector(o.bound_box[1]) - Vector(o.bound_box[0])).length
return z < x and z < y
def delete_mesh(self, mesh):
def delete_mesh(self, mesh: bpy.types.Mesh) -> None:
mesh.user_clear()
bpy.data.meshes.remove(mesh)
def delete_obj(self, obj):
def delete_obj(self, obj: bpy.types.Object) -> None:
bpy.data.objects.remove(obj, do_unlink=True)
+21 -13
View File
@@ -90,7 +90,7 @@ def get_active_pset_obj_name(context, obj_type):
return ""
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True):
box = layout.box()
row = box.row(align=True)
if "is_expanded" not in pset:
@@ -114,16 +114,20 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
op = row.operator("bim.remove_pset", icon="X", text="")
remove_pset_row = row.row(align=True)
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
remove_pset_row.enabled = allow_removing
elif props.active_pset_id != pset_id:
row.label(text=pset["Name"], icon="COPY_ID")
op = row.operator("bim.remove_pset", icon="X", text="")
remove_pset_row = row.row(align=True)
op = remove_pset_row.operator("bim.remove_pset", icon="X", text="")
op.pset_id = pset_id
op.obj = obj_name
op.obj_type = obj_type
remove_pset_row.enabled = allow_removing
if pset["is_expanded"]:
if props.active_pset_id == pset_id:
for prop in props.properties:
@@ -171,7 +175,7 @@ def draw_psetqto_editable_ui(box, props, prop):
class BIM_PT_object_psets(Panel):
bl_label = "Object Property Sets"
bl_label = "Property Sets"
bl_idname = "BIM_PT_object_psets"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
@@ -203,17 +207,19 @@ class BIM_PT_object_psets(Panel):
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET":
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
for pset in ObjectPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
if ObjectPsetsData.data["psets"]:
self.layout.label(text="Instance:")
for pset in ObjectPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
if ObjectPsetsData.data["inherited_psets"]:
self.layout.label(text="Inherited Psets:", icon="FILE_PARENT")
self.layout.label(text="Type:")
for pset in ObjectPsetsData.data["inherited_psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False)
class BIM_PT_object_qtos(Panel):
bl_label = "Object Quantity Sets"
bl_label = "Quantity Sets"
bl_idname = "BIM_PT_object_qtos"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
@@ -245,13 +251,15 @@ class BIM_PT_object_qtos(Panel):
if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO":
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
for qto in ObjectQtosData.data["qtos"]:
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
if ObjectQtosData.data["qtos"]:
self.layout.label(text="Instance:")
for qto in ObjectQtosData.data["qtos"]:
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
if ObjectQtosData.data["inherited_qsets"]:
self.layout.label(text="Inherited Qsets:", icon="FILE_PARENT")
self.layout.label(text="Type:")
for qset in ObjectQtosData.data["inherited_qsets"]:
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object")
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False)
class BIM_PT_material_psets(Panel):
@@ -34,6 +34,7 @@ classes = (
operator.RemovePropTemplate,
operator.RemovePsetTemplate,
operator.RemovePsetTemplateFile,
operator.SavePsetTemplateFile,
prop.PsetTemplate,
prop.EnumerationValues,
prop.PropTemplate,
@@ -215,7 +215,7 @@ class UnlinkObject(bpy.types.Operator):
bl_idname = "bim.unlink_object"
bl_label = "Unlink Object"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj: bpy.props.StringProperty(name="Object Name")
should_delete: bpy.props.BoolProperty(name="Delete IFC Element", default=True)
def execute(self, context):
@@ -227,6 +227,7 @@ class UnlinkObject(bpy.types.Operator):
else:
objects = context.selected_objects
objects: list[bpy.types.Object]
for obj in objects:
was_active_object = obj == context.active_object
@@ -242,6 +243,27 @@ class UnlinkObject(bpy.types.Operator):
if obj.data:
obj_copy.data = obj.data.copy()
# prevent unlinking materials that might be used elsewhere
replacements: dict[bpy.types.Material, bpy.types.Material] = dict()
for material_slot in obj_copy.material_slots:
material = material_slot.material
if material is None:
continue
if material in replacements:
material_replacement = replacements[material]
# no need to copy non-ifc materials as unlinking won't do anything to them
elif tool.Ifc.get_entity(material) is None and tool.Style.get_style(material) is None:
replacements[material] = material
continue
else:
material_replacement = material.copy()
replacements[material] = material_replacement
material_slot.material = material_replacement
tool.Geometry.delete_ifc_object(obj)
obj = obj_copy
@@ -24,21 +24,15 @@ classes = (
operator.ActivateIfcClassFilter,
operator.AddFilter,
operator.AddFilterGroup,
operator.AddToIfcGroup,
operator.ColourByProperty,
operator.EditFilterQuery,
operator.FilterModelElements,
operator.IfcSelector,
operator.LoadColourscheme,
operator.LoadQuery,
operator.LoadSearch,
operator.OpenQueryLibrary,
operator.RemoveFilter,
operator.RemoveFilterGroup,
operator.ResetObjectColours,
operator.SaveColourscheme,
operator.SaveSearch,
operator.SaveSelectorQuery,
operator.Search,
operator.SelectByProperty,
operator.SelectFilterElements,
@@ -51,11 +45,6 @@ classes = (
prop.BIMFilterClasses,
prop.BIMFilterBuildingStoreys,
prop.BIMSearchProperties,
prop.SearchCollection,
prop.SearchQueryFilter,
prop.SearchQuery,
prop.SearchQueryGroup,
prop.IfcSelectorProperties,
ui.BIM_PT_search,
ui.BIM_PT_filter,
ui.BIM_PT_colour_by_property,
@@ -63,15 +52,12 @@ classes = (
ui.BIM_UL_colourscheme,
ui.BIM_UL_ifc_class_filter,
ui.BIM_UL_ifc_building_storey_filter,
ui.BIM_PT_IFCSelector,
)
def register():
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
bpy.types.Scene.IfcSelectorProperties = bpy.props.PointerProperty(type=prop.IfcSelectorProperties)
def unregister():
del bpy.types.Scene.BIMSearchProperties
del bpy.types.Scene.IfcSelectorProperties
@@ -271,25 +271,29 @@ class ColourByProperty(Operator):
colourscheme = {}
if len(props.colourscheme):
colourscheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
value = str(ifcopenshell.util.selector.get_element_value(element, query))
if value not in colourscheme:
colourscheme[value] = next(colours)[0:3]
obj.color = (*colourscheme[value], 1)
if value in colourscheme:
colourscheme[value]["total"] += 1
else:
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
obj.color = (*colourscheme[value]["colour"], 1)
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
if areas:
areas[0].spaces[0].shading.color_type = "OBJECT"
props.colourscheme.clear()
for value, colour in colourscheme.items():
for value in sorted(colourscheme.keys()):
data = colourscheme[value]
new = props.colourscheme.add()
new.name = str(value)
new.colour = colour[0:3]
new.total = data["total"]
new.colour = data["colour"][0:3]
return {"FINISHED"}
def store_state(self, context):
@@ -351,16 +355,16 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
query = props.colourscheme_query
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
coulour_scheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme}
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": cs.total} for cs in props.colourscheme}
if group:
group = group[0]
description = json.loads(group.Description)
description["colourscheme"] = coulour_scheme
description["colourscheme"] = colourscheme
description["colourscheme_query"] = query
group.Description = json.dumps(description)
else:
description = json.dumps(
{"type": "BBIM_Search", "colourscheme": coulour_scheme, "colourscheme_query": query}
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
@@ -380,10 +384,11 @@ class LoadColourscheme(Operator, tool.Ifc.Operator):
description = json.loads(group.Description)
props.colourscheme_query = description.get("colourscheme_query")
props.colourscheme.clear()
for name, colour in description.get("colourscheme", {}).items():
for name, data in description.get("colourscheme", {}).items():
new = props.colourscheme.add()
new.name = name
new.colour = colour
new.total = data["total"]
new.colour = data["colour"]
def draw(self, context):
props = context.scene.BIMSearchProperties
@@ -623,223 +628,6 @@ class ShowAllElements(Operator):
return {"FINISHED"}
class FilterModelElements(Operator):
"""Filter model elements based on selection"""
bl_idname = "bim.filter_model_elements"
bl_label = "Filter Model Elements"
option: StringProperty("select|isolate|hide")
def execute(self, context):
selection_props = context.scene.IfcSelectorProperties
selection = (
selection_props.selector_query_syntax
if selection_props.manual_override
else self.add_groups(selection_props)
)
selection_props.selector_query_syntax = selection
r = core.search(tool.Search, tool.Spatial, query=selection, action=self.option)
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
def add_groups(self, selector: str) -> str:
selection = ""
for group_index, group in enumerate(selector.groups):
if group_index != 0:
selection += " | "
selection += "(" if len(selector.groups) > 1 else ""
selection = self.add_queries(selection, group)
selection += ")" if len(selector.groups) > 1 else ""
return selection
def add_queries(self, selection: str, group: str) -> str:
for query_index, query in enumerate(group.queries):
if query_index != 0:
selection += " & " if query.and_or == "and" else " | "
if query.selector == "IFC Class":
active_option = query.active_option.split(": ")[1]
selection += f".{active_option}"
selection = self.add_filters(selection, query)
elif query.selector == "GlobalId":
selection += f"#{query.value}"
elif query.selector == "IfcElementType":
index = int(query.active_sub_option.split(":")[0])
selection += f"* #{query.sub_options[index].global_id}"
elif query.selector == "IfcSpatialElement":
index = int(query.active_sub_option.split(":")[0])
selection += f"@ #{query.sub_options[index].global_id}"
return selection
def add_filters(self, selection: str, query: str) -> str:
for f_index, f in enumerate(query.filters):
if f_index != 0:
selection += " & " if f.and_or == "and" else " | "
selection += f".{query.active_option}"
if f.value in ("True", "False"):
value = f.value
elif f.value.isnumeric() and str(float(f.value))[0] == f.value[0]:
value = f.value
else:
value = f'"{f.value}"'
selection += "["
if f.selector == "IfcPropertySet":
selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}{f.comparison} {value}'
elif f.selector == "Attribute":
# we're using the prop_search functionality in blender which returns the index of the option. Sometimes the user can override this and enter a value that doesn't exist in the list. In this case there is no index and we need to handle it. @vulevukusej
pattern = re.compile(r"^[0-9]+:")
match = pattern.search(f.active_option)
selection += f'{f.active_option.split(": ")[1] if match else f.active_option} {"!" if f.negation else ""}{f.comparison} {value}'
selection += "]"
return selection
# This needs to be moved into ui code, I know ;) - vulevukusej
class IfcSelector(Operator):
"""Select elements in model with IFC Selector"""
bl_idname = "bim.ifc_selector"
bl_label = "Select elements with IFC Selector"
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=800)
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
from . import ui
ui.IfcSelectorUI.draw(context, self.layout)
class SaveSelectorQuery(Operator):
bl_idname = "bim.save_selector_query"
bl_label = "Save Selector Query"
save_name: StringProperty()
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context):
layout = self.layout
layout.prop(self, "save_name")
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
new = ifc_selector.query_library.add()
new.name = self.save_name
new.query = ifc_selector.selector_query_syntax
return {"FINISHED"}
class OpenQueryLibrary(Operator):
"""Open Query Library"""
bl_idname = "bim.open_query_library"
bl_label = "Open Query Library"
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=400)
def draw(self, context):
layout = self.layout
ifc_selector = context.scene.IfcSelectorProperties
for index, query in enumerate(ifc_selector.query_library):
row = layout.row(align=True)
row.prop(query, "query", text=query.name)
op = row.operator("bim.load_query", icon="SORT_ASC", text="")
op.index = index
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "query_library"
op.index = index
def execute(self, context):
return {"FINISHED"}
class LoadQuery(Operator):
bl_idname = "bim.load_query"
bl_label = "Load Query"
index: IntProperty()
def invoke(self, context, event):
close_operator_panel(event)
return self.execute(context)
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
return {"FINISHED"}
class AddToIfcGroup(Operator):
bl_idname = "bim.add_to_ifc_group"
bl_label = "Add to IFC Group"
group_name: StringProperty(name="Group Name")
def invoke(self, context, event):
bpy.ops.bim.load_groups()
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context):
self.props = context.scene.BIMGroupProperties
row = self.layout.row()
row.operator("bim.add_group")
self.layout.template_list(
"BIM_UL_groups",
"",
self.props,
"groups",
self.props,
"active_group_index",
)
if self.props.active_group_id:
for attribute in self.props.group_attributes:
if attribute.name in ["Name", "Description"]:
row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name)
def execute(self, context):
active_group_index = self.props.active_group_index
ifc_definition_id = self.props.groups[active_group_index].ifc_definition_id
bpy.ops.bim.enable_editing_group(group=ifc_definition_id)
selector_query_syntax = context.scene.IfcSelectorProperties.selector_query_syntax
for attribute in self.props.group_attributes:
if attribute.name == "Description":
if "*selector*" not in attribute.string_value:
attribute.string_value += f" *selector*{selector_query_syntax}*selector*"
else:
new_description = attribute.string_value.split("*selector*")
new_description[1] = selector_query_syntax
attribute.string_value = "*selector*".join(new_description)
bpy.ops.bim.edit_group()
bpy.ops.bim.disable_group_editing_ui()
return {"FINISHED"}
class SelectSimilar(Operator, tool.Ifc.Operator):
bl_idname = "bim.select_similar"
bl_label = "Select Similar"
@@ -78,7 +78,7 @@ def update_is_container_selected(self, context):
else:
for obj in context.selected_objects:
container = tool.Spatial.get_container(tool.Ifc.get_entity(obj))
if (container and container.Name == self.name) or (not container and self.name== "None"):
if (container and container.Name == self.name) or (not container and self.name == "None"):
obj.select_set(False)
new = self.unselected_objects.add()
new.obj = obj
@@ -100,6 +100,7 @@ class BIMFilterBuildingStoreys(PropertyGroup):
class BIMColour(PropertyGroup):
name: StringProperty(name="Name")
total: IntProperty(name="Total")
colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0)
@@ -138,139 +139,3 @@ def get_classes(self, ifc_product):
declarations = util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
return [(c, c, "") for c in sorted(names)]
def load_sub_options(self, context):
if self.selector not in ["IfcClass"]:
self.load_option = "sub_options"
load_selection_options(self, context)
def load_selection_options(self, context):
ifc = IfcStore.file
load_option = self.load_option
op = getattr(self, load_option)
op.clear()
options = []
if load_option == "options":
self.sub_options.clear()
if self.selector == "IFC Class":
options = get_classes(self, "IfcElement")
options.append(("IfcSpace", "IfcSpace", ""))
elif self.selector == "IfcSpatialElement":
options = get_classes(self, "IfcSpatialElement")
elif self.selector == "IfcElementType":
options = get_classes(self, "IfcElementType")
elif self.selector == "GlobalId":
return
elif self.selector == "IfcPropertySet":
psets = Selector.parse(ifc, ".IfcPropertySet")
options = set([o.Name for o in psets])
elif self.selector == "Attribute":
attributes = ["GlobalId", "Name", "Description", "ObjectType", "Tag", "PredefinedType"]
for a in attributes:
options.append((a, a, ""))
elif load_option == "sub_options":
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
active_option = self.active_option.split(": ")[1]
options = Selector.parse(ifc, f".{active_option}")
elif self.selector == "IfcPropertySet":
options = set()
active_pset = self.active_option.split(": ")[1]
psets_in_file = Selector.parse(ifc, f'.IfcPropertySet[Name="{active_pset}"]')
for pset in psets_in_file:
for prop in pset.HasProperties:
options.add(prop.Name)
for index, option in enumerate(options):
new = op.add()
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
if self.load_option == "sub_options":
new.name = f"{index}: {option.Name}"
new.global_id = option.GlobalId
else:
new.name = f"{index}: {option[0]}"
elif self.selector == "IfcPropertySet":
new.name = f"{index}: {option}"
else:
new.name = f"{index}: {option[0]}"
self.load_option = "options"
def load_spatial_elements(self, context):
ifc = IfcStore.file
col_items = Selector.parse(ifc, f".{self.selected_spatial_element}")
collection = getattr(self, "sub_spatial_elements", None)
collection.clear()
for index, c in enumerate(col_items):
new = collection.add()
new.name = f"{index}-{c.Name}"
class SearchCollection(PropertyGroup):
name: StringProperty()
long_name: StringProperty()
global_id: StringProperty()
query: StringProperty()
class IfcSelector:
and_or: EnumProperty(
items=[(i, i, i) for i in ["and", "or"]],
)
negation: BoolProperty(name="not")
comparison: EnumProperty(
items=[
("=", "equal to", ""),
("*=", "contains", ""),
(">=", "greater than or equal to", ""),
("<=", "lesser than or equal to", ""),
(">", "greater than", ""),
("<", "less than", ""),
],
)
load_option: StringProperty(
default="options", description="controls whether or not options or sub_options are loaded"
)
options: CollectionProperty(type=SearchCollection)
active_option: StringProperty(update=load_sub_options)
sub_options: CollectionProperty(type=SearchCollection)
active_sub_option: StringProperty()
value: StringProperty(description="generic 'value' that can be used in multiple scenarios")
class SearchQueryFilter(PropertyGroup, IfcSelector):
selector: EnumProperty(
items=[(i, i, i) for i in ["-", "IfcPropertySet", "Attribute"]],
name="Filter selection by",
update=load_selection_options,
default="-",
)
class SearchQuery(PropertyGroup, IfcSelector):
filters: CollectionProperty(type=SearchQueryFilter)
selector: EnumProperty(
items=[(i, i, i) for i in ["-", "IFC Class", "IfcSpatialElement", "IfcElementType", "GlobalId"]],
name="Selector type",
update=load_selection_options,
default="-",
)
class SearchQueryGroup(PropertyGroup, IfcSelector):
queries: CollectionProperty(type=SearchQuery)
class IfcSelectorProperties(PropertyGroup, IfcSelector):
groups: CollectionProperty(type=SearchQueryGroup)
selector_query_syntax: StringProperty()
query_library: CollectionProperty(type=SearchCollection)
active_query: StringProperty()
active_query: StringProperty()
manual_override: BoolProperty(default=False, description="Toggle to allow manual typing of query-syntax")
@@ -125,7 +125,7 @@ class BIM_UL_colourscheme(bpy.types.UIList):
if not item:
return
row = layout.row(align=True)
row.label(text=item.name)
row.label(text=f"{item.name} ({item.total})")
row.prop(item, "colour", text="")
@@ -159,153 +159,3 @@ class BIM_UL_ifc_building_storey_filter(bpy.types.UIList):
split = split.column()
split.scale_x = 0.5
split.label(text=str(item.total))
class BIM_PT_IFCSelector(Panel):
bl_label = "Selector"
bl_idname = "BIM_PT_ifc_selector"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_tab_sandbox"
def draw(self, context):
layout = self.layout
layout.operator("bim.ifc_selector")
# this doesn't inherit from Panel since it's just a class to abstract away UI code from the IfcSelector Operator
class IfcSelectorUI:
@classmethod
def draw(self, context, layout):
ifc_selector = context.scene.IfcSelectorProperties
row = layout.row()
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", text="Add selection group")
op.option = "add"
op.collection = "groups"
layout.separator()
self.draw_query_group_ui(self, ifc_selector, layout)
if len(ifc_selector.groups) != 0:
row = layout.row(align=True)
row.prop(ifc_selector, "selector_query_syntax", text="Query Syntax")
row.prop(ifc_selector, "manual_override", text="")
select = row.operator("bim.filter_model_elements", text="", icon="RESTRICT_SELECT_OFF")
select.option = "select"
isolate = row.operator("bim.filter_model_elements", text="", icon="ZOOM_SELECTED")
isolate.option = "isolate"
hide = row.operator("bim.filter_model_elements", text="", icon="HIDE_ON")
hide.option = "hide"
unhide = row.operator("bim.filter_model_elements", text="", icon="HIDE_OFF")
unhide.option = "unhide"
row = layout.row(align=True)
row.alignment = "CENTER"
row.operator("bim.save_selector_query", text="Save Query")
op = row.operator("bim.open_query_library", text="Load Query")
row.operator("bim.add_to_ifc_group", text="Add to IFC Group")
row = layout.row(align=True)
row.operator("bim.show_scene_elements", text="Show All Elements", icon="HIDE_OFF")
def draw_query_group_ui(self, ifc_selector, layout):
for index, group in enumerate(ifc_selector.groups):
row = layout.row()
row.alignment = "CENTER"
row.label(text="or") if index != 0 else None
box = layout.box()
row = box.row(align=True)
row.alignment = "CENTER"
row.label(text=f"Group #{str(index+1)}")
row.context_pointer_set(name="bim_prop_group", data=group)
op = row.operator("bim.edit_blender_collection", text="Add query", icon="PLUS")
op.option = "add"
op.collection = "queries"
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", text="Remove selection group")
op.option = "remove"
op.collection = "groups"
op.index = index
self.draw_query_ui(self, group, box)
def draw_query_ui(self, group, box):
for index, query in enumerate(group.queries):
row = box.row()
row.alignment = "LEFT"
row.prop(query, "and_or", text="") if index != 0 else None
if query.and_or == "or":
row = box.row()
row.alignment = "LEFT"
row.prop(query, "selector", text="")
if query.selector in ["IFC Class", "IfcSpatialElement", "IfcElementType"]:
row.label(text="Equals")
row.prop_search(query, "active_option", query, "options", text="")
row.prop_search(query, "active_sub_option", query, "sub_options", text="") if len(
query.sub_options
) != 0 else None
self.draw_filter_ui(self, box, query)
elif query.selector in ["GlobalId", "Attribute"]:
row.prop(query, "negation", text="")
row.prop(query, "comparison", text="")
row.prop(query, "value", text="")
row.context_pointer_set(name="bim_prop_group", data=group)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "queries"
op.index = index
row.context_pointer_set(name="bim_prop_group", data=query)
op = (
row.operator("bim.edit_blender_collection", text="Add filter")
if query.selector == "IFC Class"
else None
)
if op:
op.option = "add"
op.collection = "filters"
def draw_filter_ui(self, box, query):
for filter_index, f in enumerate(query.filters):
row = box.row()
row.alignment = "LEFT"
row.label(text="")
row.prop(f, "and_or", text="") if filter_index != 0 else None
if f.and_or == "or":
row = box.row()
row.alignment = "LEFT"
row.label(text="")
row.prop(f, "selector", text="")
if f.selector == "Attribute":
row.prop_search(f, "active_option", f, "options", text="", results_are_suggestions=True)
row.prop(
f,
"negation",
)
row.prop(f, "comparison", text="")
row.prop(f, "value", text="")
elif f.selector == "IfcPropertySet":
row.prop_search(f, "active_option", f, "options", text="")
row.prop_search(f, "active_sub_option", f, "sub_options", text="")
row.prop(f, "negation")
row.prop(f, "comparison", text="")
row.prop(f, "value", text="")
row.context_pointer_set(name="bim_prop_group", data=query)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "filters"
op.index = filter_index
+3 -3
View File
@@ -388,13 +388,13 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
should_delete_existing_annotation = False
should_create_annotation = False
# remove annotation only if the reference object was changed
# otherwise we rely on the existing annotation
if annotation:
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
should_delete_existing_annotation = True
elif not reference_obj:
should_delete_existing_annotation = True
if reference_obj and (should_delete_existing_annotation or not annotation):
if (should_delete_existing_annotation or not annotation):
should_create_annotation = True
if should_delete_existing_annotation:
+13 -2
View File
@@ -76,7 +76,7 @@ class Blender:
def get_name(cls, ifc_class, name): pass
def get_selected_objects(cls): pass
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None): pass
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None): pass
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None, context=None): pass
def is_ifc_object(cls, obj): pass
def is_ifc_class_active(cls, ifc_class): pass
def get_viewport_context(cls): pass
@@ -153,6 +153,17 @@ class Bsdd:
def should_load_preview_domains(cls): pass
@interface
class Clash:
def export_clash_sets(cls): pass
def get_clash(cls, clash_set, a_global_id, b_global_id): pass
def get_clash_set(cls, name): pass
def get_clash_sets(cls): pass
def import_active_clashes(cls): pass
def load_clash_sets(cls, fn): pass
def look_at(cls, target, location): pass
@interface
class Collector:
def assign(cls, obj): pass
@@ -508,7 +519,7 @@ class Misc:
class Model:
def convert_si_to_unit(cls, value): pass
def convert_unit_to_si(cls, value): pass
def export_curve(cls, position, edge_indices, points=None): pass
def export_curve(cls, position, edge_indices): pass
def export_points(cls, position, indices): pass
def export_profile(cls, obj, position=None): pass
def generate_occurrence_name(cls, element_type, ifc_class): pass
@@ -22,6 +22,7 @@ from blenderbim.tool.boundary import Boundary
from blenderbim.tool.brick import Brick
from blenderbim.tool.bsdd import Bsdd
from blenderbim.tool.cad import Cad
from blenderbim.tool.clash import Clash
from blenderbim.tool.collector import Collector
from blenderbim.tool.context import Context
from blenderbim.tool.debug import Debug
+135 -57
View File
@@ -17,13 +17,17 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bmesh
import json
import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.bim
import addon_utils
from mathutils import Vector
from pathlib import Path
import addon_utils
from blenderbim.bim.ifc import IFC_CONNECTED_TYPE
from typing import Any, Optional, Union, Literal, Iterable, Callable
VIEWPORT_ATTRIBUTES = [
@@ -37,13 +41,16 @@ VIEWPORT_ATTRIBUTES = [
"clip_planes",
]
OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera]
class Blender(blenderbim.core.tool.Blender):
OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE = ("MESH", "CURVE", "SURFACE", "META", "FONT", "LATTICE", "ARMATURE")
OBJECT_TYPES_THAT_SUPPORT_EDIT_GPENCIL_MODE = ("GPENCIL",)
TYPE_MANAGER_ICON = "LIGHTPROBE_VOLUME" if bpy.app.version >= (4, 1, 0) else "LIGHTPROBE_GRID"
@classmethod
def get_area_props(cls, context):
def get_area_props(cls, context: bpy.types.Context) -> Any:
try:
if context.screen.name.endswith("-nonnormal"): # Ctrl-space temporary fullscreen
screen = bpy.data.screens[context.screen.name[0 : -len("-nonnormal")]]
@@ -55,12 +62,12 @@ class Blender(blenderbim.core.tool.Blender):
return
@classmethod
def set_active_object(cls, obj):
def set_active_object(cls, obj: bpy.types.Object) -> None:
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
@classmethod
def setup_tabs(cls):
def setup_tabs(cls) -> None:
# https://blender.stackexchange.com/questions/140644/how-can-make-the-state-of-a-boolean-property-relative-to-the-3d-view-area
for screen in bpy.data.screens:
if len(screen.BIMAreaProperties) == 20:
@@ -70,7 +77,7 @@ class Blender(blenderbim.core.tool.Blender):
screen.BIMAreaProperties.add()
@classmethod
def is_tab(cls, context, tab):
def is_tab(cls, context: bpy.types.Context, tab: str) -> bool:
aprops = cls.get_area_props(context)
if not aprops:
return context.screen.BIMTabProperties.tab == tab
@@ -79,7 +86,7 @@ class Blender(blenderbim.core.tool.Blender):
return aprops.tab == tab
@classmethod
def is_default_scene(cls):
def is_default_scene(cls) -> bool:
if len(bpy.context.scene.objects) != 3:
return False
if {obj.type for obj in bpy.context.scene.objects} == {"MESH", "LIGHT", "CAMERA"}:
@@ -87,7 +94,7 @@ class Blender(blenderbim.core.tool.Blender):
return False
@classmethod
def get_name(cls, ifc_class, name):
def get_name(cls, ifc_class: str, name: str) -> str:
if not bpy.data.objects.get(f"{ifc_class}/{name}"):
return name
i = 2
@@ -96,13 +103,15 @@ class Blender(blenderbim.core.tool.Blender):
return f"{name} {i}"
@classmethod
def get_selected_objects(cls):
def get_selected_objects(cls) -> set[bpy.types.Object]:
if bpy.context.selected_objects:
return set(bpy.context.selected_objects + [bpy.context.active_object])
return set([bpy.context.active_object])
@classmethod
def create_ifc_object(cls, ifc_class: str, name: str = None, data=None) -> bpy.types.Object:
def create_ifc_object(
cls, ifc_class: str, name: Optional[str] = None, data: Optional[OBJECT_DATA_TYPE] = None
) -> bpy.types.Object:
name = name or "My " + ifc_class
name = cls.get_name(ifc_class, name)
obj = bpy.data.objects.new(name, data)
@@ -110,7 +119,11 @@ class Blender(blenderbim.core.tool.Blender):
return obj
@classmethod
def get_obj_ifc_definition_id(cls, obj=None, obj_type=None):
def get_obj_ifc_definition_id(
cls, obj: Optional[str] = None, obj_type: Optional[str] = None, context: Optional[bpy.types.Context] = None
) -> Union[int, None]:
if context is None:
context = bpy.context
if obj_type == "Object":
return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id
elif obj_type == "Material":
@@ -122,41 +135,42 @@ class Blender(blenderbim.core.tool.Blender):
elif obj_type == "MaterialSetItem":
return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id
elif obj_type == "Task":
return bpy.context.scene.BIMTaskTreeProperties.tasks[
bpy.context.scene.BIMWorkScheduleProperties.active_task_index
return context.scene.BIMTaskTreeProperties.tasks[
context.scene.BIMWorkScheduleProperties.active_task_index
].ifc_definition_id
elif obj_type == "Cost":
return bpy.context.scene.BIMCostProperties.cost_items[
bpy.context.scene.BIMCostProperties.active_cost_item_index
return context.scene.BIMCostProperties.cost_items[
context.scene.BIMCostProperties.active_cost_item_index
].ifc_definition_id
elif obj_type == "Resource":
return bpy.context.scene.BIMResourceTreeProperties.resources[
bpy.context.scene.BIMResourceProperties.active_resource_index
return context.scene.BIMResourceTreeProperties.resources[
context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
elif obj_type == "Profile":
return bpy.context.scene.BIMProfileProperties.profiles[
bpy.context.scene.BIMProfileProperties.active_profile_index
return context.scene.BIMProfileProperties.profiles[
context.scene.BIMProfileProperties.active_profile_index
].ifc_definition_id
elif obj_type == "WorkSchedule":
return bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id
return context.scene.BIMWorkScheduleProperties.active_work_schedule_id
elif obj_type == "Group":
prop = bpy.context.scene.BIMGroupProperties
prop = context.scene.BIMGroupProperties
return prop.groups[prop.active_group_index].ifc_definition_id
@classmethod
def is_ifc_object(cls, obj):
def is_ifc_object(cls, obj: bpy.types.Object) -> bool:
return bool(obj.BIMObjectProperties.ifc_definition_id)
@classmethod
def is_ifc_class_active(cls, ifc_class):
if bpy.context.active_object:
if cls.is_ifc_object(bpy.context.active_object):
return tool.Ifc.get_entity(bpy.context.active_object).is_a(ifc_class)
def is_ifc_class_active(cls, ifc_class: str) -> bool:
obj = bpy.context.active_object
if obj:
if cls.is_ifc_object(obj):
return tool.Ifc.get_entity(obj).is_a(ifc_class)
return False
return False
@classmethod
def show_info_message(cls, text, message_type="INFO"):
def show_info_message(cls, text: str, message_type: Literal["INFO", "ERROR"] = "INFO") -> None:
"""useful for showing error messages outside blender operators
Possible `message_type`: `INFO` / `ERROR`"""
@@ -167,14 +181,14 @@ class Blender(blenderbim.core.tool.Blender):
bpy.context.window_manager.popup_menu(message_ui, title=message_type.capitalize(), icon=message_type)
@classmethod
def get_view3d_area(cls):
def get_view3d_area(cls) -> Union[bpy.types.Area, None]:
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == "VIEW_3D":
return area
@classmethod
def get_blender_prop_default_value(cls, props, prop_name):
def get_blender_prop_default_value(cls, props, prop_name: str) -> Any:
prop_bl_rna = props.bl_rna.properties[prop_name]
if getattr(prop_bl_rna, "array_length", 0) > 0:
prop_value = prop_bl_rna.default_array
@@ -183,7 +197,7 @@ class Blender(blenderbim.core.tool.Blender):
return prop_value
@classmethod
def get_viewport_context(cls):
def get_viewport_context(cls) -> dict:
"""Get viewport area context for context overriding.
Useful for calling operators outside viewport context.
@@ -198,25 +212,25 @@ class Blender(blenderbim.core.tool.Blender):
return context_override
@classmethod
def get_viewport_position(cls):
def get_viewport_position(cls) -> dict:
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
copy_if_possible = lambda x: x.copy() if hasattr(x, "copy") else x
viewport_data = {attr: copy_if_possible(getattr(region_3d, attr)) for attr in VIEWPORT_ATTRIBUTES}
return viewport_data
@classmethod
def set_viewport_position(cls, data):
def set_viewport_position(cls, data: dict) -> None:
region_3d = cls.get_viewport_context()["area"].spaces[0].region_3d
for attr in VIEWPORT_ATTRIBUTES:
setattr(region_3d, attr, data[attr])
@classmethod
def set_viewport_tool(cls, tool_name):
def set_viewport_tool(cls, tool_name: str) -> None:
with bpy.context.temp_override(**tool.Blender.get_viewport_context()):
bpy.ops.wm.tool_set_by_id(name=tool_name)
@classmethod
def get_shader_editor_context(cls):
def get_shader_editor_context(cls) -> Union[dict, None]:
for screen in bpy.data.screens:
for area in screen.areas:
if area.type == "NODE_EDITOR":
@@ -226,7 +240,7 @@ class Blender(blenderbim.core.tool.Blender):
return context_override
@classmethod
def copy_node_graph(cls, material_to, material_from):
def copy_node_graph(cls, material_to: bpy.types.Material, material_from: bpy.types.Material) -> None:
# https://projects.blender.org/blender/blender/issues/108763
if bpy.app.version[:2] == (4, 0):
print(
@@ -270,7 +284,9 @@ class Blender(blenderbim.core.tool.Blender):
shader_editor.pin = previous_pin_setting
@classmethod
def get_material_node(cls, blender_material, node_type, kwargs={}):
def get_material_node(
cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {}
) -> Union[bpy.types.ShaderNode, None]:
"""returns first node from the `blender_material` shader graph with type `node_type`"""
if not blender_material.use_nodes:
return
@@ -280,15 +296,15 @@ class Blender(blenderbim.core.tool.Blender):
return node
@classmethod
def update_screen(cls):
def update_screen(cls) -> None:
bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
@classmethod
def update_viewport(cls):
def update_viewport(cls) -> None:
tool.Blender.get_viewport_context()["area"].tag_redraw()
@classmethod
def force_depsgraph_update(cls):
def force_depsgraph_update(cls) -> None:
"""useful if you need to trigger callbacks like `depsgraph_update_pre`"""
# blender is requiring some ID to be changed
# to trigger depsgraph update
@@ -297,9 +313,11 @@ class Blender(blenderbim.core.tool.Blender):
bpy.context.view_layer.update()
@classmethod
def ensure_unique_name(cls, name, objects, iteration=0):
def ensure_unique_name(cls, name: str, objects: Iterable[str], iteration=0) -> str:
"""returns a unique name for the given name and dictionary of objects
blender style name with .001, .002, etc. suffix
blender style name with .001, .002, etc. suffix.
objects can be `bpy.data.objects`.
"""
current_iteration = name if not iteration else f"{name}.{iteration:03d}"
if current_iteration not in objects:
@@ -307,7 +325,7 @@ class Blender(blenderbim.core.tool.Blender):
return cls.ensure_unique_name(name, objects, iteration + 1)
@classmethod
def blender_path_to_posix(cls, blender_path):
def blender_path_to_posix(cls, blender_path: str) -> str:
"""Process blender path to be saved as posix.
If path is relative the method will keep it relative to .ifc file
@@ -322,7 +340,7 @@ class Blender(blenderbim.core.tool.Blender):
return path.as_posix()
@classmethod
def get_default_selection_keypmap(cls):
def get_default_selection_keypmap(cls) -> tuple:
"""keymap to replicate default blender selection behaviour with click and box selection"""
# code below comes from blender_default.py which is part of default blender scripts licensed under GPL v2
# https://github.com/blender/blender/blob/master/release/scripts/presets/keyconfig/keymap_data/blender_default.py
@@ -369,7 +387,9 @@ class Blender(blenderbim.core.tool.Blender):
return keymap
@classmethod
def add_layout_hotkey_operator(cls, tool_name, layout, text, hotkey, description):
def add_layout_hotkey_operator(
cls, tool_name: str, layout: bpy.types.UILayout, text: str, hotkey: str, description: str
) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]:
modifiers = {
"A": "EVENT_ALT",
"S": "EVENT_SHIFT",
@@ -386,7 +406,7 @@ class Blender(blenderbim.core.tool.Blender):
return op, row
@classmethod
def get_object_bounding_box(cls, obj):
def get_object_bounding_box(cls, obj: bpy.types.Object) -> dict:
"""Returns dict with local min and max x, y, z values for the object.
Careful with using this method for objects in EDIT mode because
@@ -407,14 +427,20 @@ class Blender(blenderbim.core.tool.Blender):
return bbox_dict
@classmethod
def select_and_activate_single_object(cls, context, active_object):
def select_and_activate_single_object(cls, context: bpy.types.Context, active_object: bpy.types.Object) -> None:
for obj in context.selected_objects:
obj.select_set(False)
context.view_layer.objects.active = active_object
active_object.select_set(True)
@classmethod
def set_objects_selection(cls, context, active_object, selected_objects, clear_previous_selection=True):
def set_objects_selection(
cls,
context: bpy.types.Context,
active_object: Optional[bpy.types.Object] = None,
selected_objects: list[bpy.types.Object] = list(),
clear_previous_selection=True,
) -> None:
if clear_previous_selection:
for obj in context.selected_objects:
obj.select_set(False)
@@ -425,7 +451,7 @@ class Blender(blenderbim.core.tool.Blender):
active_object.select_set(True)
@classmethod
def enum_property_has_valid_index(cls, props, prop_name, enum_items):
def enum_property_has_valid_index(cls, props, prop_name: str, enum_items: tuple) -> bool:
"""method created for readibility and to avoid console warnings like
`pyrna_enum_to_py: current value '17' matches no enum in 'BIMModelProperties', '', 'relating_type_id'`
"""
@@ -436,7 +462,7 @@ class Blender(blenderbim.core.tool.Blender):
return current_value_index < len(enum_items)
@classmethod
def append_data_block(cls, filepath, data_block_type, name, link=False, relative=False):
def append_data_block(cls, filepath: str, data_block_type: str, name: str, link=False, relative=False) -> dict:
if Path(filepath) == Path(bpy.data.filepath):
data_block = getattr(bpy.data, data_block_type).get(name, None)
if not data_block:
@@ -450,7 +476,7 @@ class Blender(blenderbim.core.tool.Blender):
return {"data_block": getattr(data_to, data_block_type)[0], "msg": ""}
@classmethod
def remove_data_block(cls, data_block, do_unlink=True):
def remove_data_block(cls, data_block: bpy.types.ID, do_unlink=True) -> None:
"""Removes a datablock (such as a mesh)
See https://projects.blender.org/blender/blender/issues/118787 for more
@@ -472,7 +498,7 @@ class Blender(blenderbim.core.tool.Blender):
## BMESH UTILS ##
@classmethod
def apply_bmesh(cls, mesh, bm, obj=None):
def apply_bmesh(cls, mesh: bpy.types.Mesh, bm: bmesh.types.BMesh, obj: Optional[bpy.types.Object] = None) -> None:
"""`obj` argument is not optional if you plan to update mesh in EDIT mode
and it's possible that that mesh object won't be currenly active."""
import bmesh
@@ -506,7 +532,7 @@ class Blender(blenderbim.core.tool.Blender):
mesh.update()
@classmethod
def get_bmesh_for_mesh(cls, mesh, clean=False):
def get_bmesh_for_mesh(cls, mesh: bpy.types.Mesh, clean=False) -> bmesh.types.BMesh:
import bmesh
if mesh.is_editmode:
@@ -520,7 +546,16 @@ class Blender(blenderbim.core.tool.Blender):
return bm
@classmethod
def bmesh_join(cls, bm_a, bm_b, callback=None):
def bmesh_join(
cls,
bm_a: bmesh.types.BMesh,
bm_b: bmesh.types.BMesh,
callback: Optional[
Callable[
[bmesh.types.BMesh, list[bmesh.types.BMVert], list[bmesh.types.BMEdge], list[bmesh.types.BMFace]], None
]
] = None,
):
"""Join two meshes into single one, store it in `bm_a`"""
import bmesh
@@ -535,7 +570,19 @@ class Blender(blenderbim.core.tool.Blender):
return bm_a
@classmethod
def toggle_edit_mode(cls, context):
def bmesh_check_vertex_in_groups(
cls, vertex: bmesh.types.BMVert, deform_layer: bmesh.types.BMLayerItem, groups: list[int]
) -> Union[tuple[Literal[True], int], tuple[Literal[False], None]]:
"""returns tuple boolean (whether vertex is in any of the groups) and related group index"""
for group_index in vertex[deform_layer].keys():
# ignore vertex groups assignments produced by edge subdivision near arcs
# they usually have weight = 0.5
if group_index in groups and vertex[deform_layer][group_index] == 1.0:
return True, group_index
return False, None
@classmethod
def toggle_edit_mode(cls, context: bpy.types.Context) -> set:
ao = context.active_object
if not ao:
return {"CANCELLED"}
@@ -547,21 +594,21 @@ class Blender(blenderbim.core.tool.Blender):
return {"CANCELLED"}
@classmethod
def is_object_an_ifc_class(cls, obj, classes):
def is_object_an_ifc_class(cls, obj: bpy.types.Object, classes: Iterable[str]) -> bool:
if not tool.Ifc.get():
return False
element = tool.Ifc.get_entity(obj)
return element and element.is_a() in classes
@classmethod
def get_object_from_guid(cls, guid):
def get_object_from_guid(cls, guid: str) -> Union[IFC_CONNECTED_TYPE, None]:
element = tool.Ifc.get().by_guid(guid)
obj = tool.Ifc.get_object(element)
if obj:
return obj
@classmethod
def lock_transform(cls, obj, lock_state=True):
def lock_transform(cls, obj: bpy.types.ObjectBase, lock_state=True) -> None:
for prop in ("lock_location", "lock_rotation", "lock_scale"):
attr = getattr(obj, prop)
for axis_idx in range(3):
@@ -570,7 +617,9 @@ class Blender(blenderbim.core.tool.Blender):
operator_invoke_filepath_hotkeys_description = "Hold Shift to open the file, Alt to browse containing directory"
@classmethod
def operator_invoke_filepath_hotkeys(cls, operator, context, event, filepath: Path):
def operator_invoke_filepath_hotkeys(
cls, operator: bpy.types.Operator, context: bpy.types.Context, event: bpy.types.Event, filepath: Path
) -> Union[set, None]:
if not event.alt and not event.shift:
return
@@ -611,6 +660,35 @@ class Blender(blenderbim.core.tool.Blender):
open_file_or_folder(filepath.as_posix())
return {"PASS_THROUGH"}
@classmethod
def get_layer_collection(
cls, collection: bpy.types.Collection, view_layer: Optional[bpy.types.ViewLayer] = None
) -> Union[bpy.types.LayerCollection, None]:
return cls.get_layer_collections_mapping([collection], view_layer).get(collection)
@classmethod
def get_layer_collections_mapping(
cls, collections: list[bpy.types.Collection], view_layer: Optional[bpy.types.ViewLayer] = None
) -> dict[bpy.types.Collection, bpy.types.LayerCollection]:
if view_layer is None:
view_layer = bpy.context.view_layer
collections = list(collections) # copy to prevent mutation
collections_mapping = dict()
queue = [view_layer.layer_collection]
while queue:
layer = queue.pop()
collection = layer.collection
if collection in collections:
collections_mapping[collection] = layer
collections.remove(collection)
if not collections:
break
queue.extend(list(layer.children))
return collections_mapping
class Modifier:
@classmethod
def is_eligible_for_railing_modifier(cls, obj):
+117
View File
@@ -0,0 +1,117 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 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/>.
import os
import bpy
import json
import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
from contextlib import contextmanager
from mathutils import Vector
class Clash(blenderbim.core.tool.Clash):
@classmethod
def export_clash_sets(cls):
clash_sets = []
for clash_set in bpy.context.scene.BIMClashProperties.clash_sets:
a = []
b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
query = tool.Search.export_filter_query(data.filter_groups)
if query and data.mode != "a":
clash_source["selector"] = query
clash_source["mode"] = data.mode
if ab == "a":
a.append(clash_source)
elif ab == "b":
b.append(clash_source)
clash_set_data = {"name": clash_set.name, "mode": clash_set.mode, "a": a, "b": b}
if clash_set.mode == "intersection":
clash_set_data["tolerance"] = clash_set.tolerance
clash_set_data["check_all"] = clash_set.check_all
elif clash_set.mode == "collision":
clash_set_data["allow_touching"] = clash_set.allow_touching
elif clash_set.mode == "clearance":
clash_set_data["clearance"] = clash_set.clearance
clash_set_data["check_all"] = clash_set.check_all
clash_sets.append(clash_set_data)
return clash_sets
@classmethod
def get_clash(cls, clash_set, a_global_id, b_global_id):
clashes = clash_set.get("clashes", None)
if not clashes:
return
return clashes.get(f"{a_global_id}-{b_global_id}", None)
@classmethod
def get_clash_set(cls, name):
for clash_set in ClashStore.clash_sets:
if clash_set["name"] == name:
return clash_set
@classmethod
def get_clash_sets(cls):
return ClashStore.clash_sets
@classmethod
def import_active_clashes(cls):
clash_set = bpy.context.scene.BIMClashProperties.active_clash_set
if not clash_set:
return
clash_set.clashes.clear()
result = tool.Clash.get_clash_set(clash_set.name)
for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]):
blender_clash = clash_set.clashes.add()
blender_clash.a_global_id = clash["a_global_id"]
blender_clash.b_global_id = clash["b_global_id"]
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
blender_clash.status = False if not "status" in clash.keys() else clash["status"]
@classmethod
def load_clash_sets(cls, fn):
with open(fn) as f:
ClashStore.clash_sets = json.load(f)
@classmethod
def look_at(cls, target, location):
camera_location = location
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
region = next(region for region in area.regions if region.type == "WINDOW")
space = next(space for space in area.spaces if space.type == "VIEW_3D")
override = {"area": area, "region": region, "space_data": space}
space.region_3d.view_location = target
space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y")
space.region_3d.view_distance = (camera_location - target).length
space.shading.show_xray = True
class ClashStore:
clash_sets = None
path = None
@staticmethod
def purge():
ClashStore.clash_sets = None
ClashStore.path = None
+146 -31
View File
@@ -18,6 +18,7 @@
import os
import re
import collections
import bpy
import math
import json
@@ -27,27 +28,31 @@ import shutil
import logging
import shapely
import platform
from shapely.ops import unary_union
import mathutils
import subprocess
import numpy as np
import blenderbim.core.tool
import blenderbim.core.geometry
import blenderbim.tool as tool
import ifcopenshell.api
import ifcopenshell.geom
import ifcopenshell.util.representation
import ifcopenshell.util.element
import ifcopenshell.util.selector
import blenderbim.bim.import_ifc
import blenderbim.bim.module.drawing.sheeter as sheeter
import blenderbim.bim.module.drawing.scheduler as scheduler
import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.helper as helper
import blenderbim.core.root
from shapely.ops import unary_union
from blenderbim.bim.module.drawing.data import FONT_SIZES, DecoratorData
from blenderbim.bim.module.drawing.prop import get_diagram_scales, BOX_ALIGNMENT_POSITIONS, ANNOTATION_TYPES_DATA
from lxml import etree
from mathutils import Vector
from mathutils import Vector, Matrix
from fractions import Fraction
import collections
from typing import Optional, Union
from typing import Optional, Union, Iterable
from pathlib import Path
class Drawing(blenderbim.core.tool.Drawing):
@@ -247,7 +252,7 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.data.collections.remove(collection, do_unlink=True)
@classmethod
def delete_drawing_elements(cls, elements):
def delete_drawing_elements(cls, elements: Iterable[ifcopenshell.entity_instance]) -> None:
for element in elements:
obj = tool.Ifc.get_object(element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
@@ -258,7 +263,7 @@ class Drawing(blenderbim.core.tool.Drawing):
tool.Blender.remove_data_block(obj_data)
@classmethod
def delete_object(cls, obj):
def delete_object(cls, obj: bpy.types.Object) -> None:
bpy.data.objects.remove(obj)
@classmethod
@@ -366,7 +371,9 @@ class Drawing(blenderbim.core.tool.Drawing):
)
@classmethod
def get_annotation_context(cls, target_view, object_type=None):
def get_annotation_context(
cls, target_view: str, object_type: Optional[str] = None
) -> Union[ifcopenshell.entity_instance, None]:
# checking PLAN target view and annotation type that doesn't require 3d
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") and object_type not in (
"FALL",
@@ -381,7 +388,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@classmethod
def get_document_uri(cls, document, description=None):
def get_document_uri(
cls, document: ifcopenshell.entity_instance, description: Optional[str] = None
) -> Union[str, None]:
if getattr(document, "Location", None):
if os.path.isabs(document.Location):
return document.Location
@@ -426,7 +435,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return rel.RelatingGroup
@classmethod
def get_drawing_document(cls, drawing):
def get_drawing_document(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
for rel in drawing.HasAssociations:
if rel.is_a("IfcRelAssociatesDocument"):
return rel.RelatingDocument
@@ -440,7 +449,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return results
@classmethod
def get_drawing_target_view(cls, drawing):
def get_drawing_target_view(cls, drawing: ifcopenshell.entity_instance) -> str:
return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("TargetView", "MODEL_VIEW")
@classmethod
@@ -593,7 +602,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return rel.RelatingProduct
@classmethod
def import_annotations_in_group(cls, group):
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
elements = set(
[e for e in cls.get_group_elements(group) if e.is_a("IfcAnnotation") and e.ObjectType != "DRAWING"]
)
@@ -603,6 +612,7 @@ class Drawing(blenderbim.core.tool.Drawing):
ifc_importer.file = tool.Ifc.get()
ifc_importer.calculate_unit_scale()
ifc_importer.process_context_filter()
ifc_importer.load_existing_meshes()
ifc_importer.material_creator.load_existing_materials()
ifc_importer.create_generic_elements(elements)
for obj in ifc_importer.added_data.values():
@@ -612,7 +622,7 @@ class Drawing(blenderbim.core.tool.Drawing):
# but BIMCameraProperties are only synced with EPsetDrawing at drawing import
# therefore camera props can differ from pset if the user changed them from pset.
@classmethod
def import_drawing(cls, drawing):
def import_drawing(cls, drawing: ifcopenshell.entity_instance) -> bpy.types.Object:
settings = ifcopenshell.geom.settings()
settings.set(settings.STRICT_TOLERANCE, True)
@@ -865,7 +875,7 @@ class Drawing(blenderbim.core.tool.Drawing):
should_add_representation=True,
context=None,
ifc_representation_class=None,
):
) -> Union[ifcopenshell.entity_instance, None]:
return blenderbim.core.root.assign_class(
tool.Ifc,
tool.Collector,
@@ -1051,7 +1061,9 @@ class Drawing(blenderbim.core.tool.Drawing):
shutil.copy(ootb_resource, resource_path)
@classmethod
def get_potential_reference_elements(cls, drawing):
def get_potential_reference_elements(
cls, drawing: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]:
elements = []
existing_references = cls.get_group_elements(cls.get_drawing_group(drawing))
for element in tool.Ifc.get().by_type("IfcAnnotation"):
@@ -1072,7 +1084,9 @@ class Drawing(blenderbim.core.tool.Drawing):
return elements
@classmethod
def get_drawing_reference_annotation(cls, drawing, reference_element):
def get_drawing_reference_annotation(
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, bool, None]:
if drawing == reference_element:
return True
for element in cls.get_group_elements(cls.get_drawing_group(drawing)):
@@ -1088,26 +1102,50 @@ class Drawing(blenderbim.core.tool.Drawing):
return element
@classmethod
def generate_reference_annotation(cls, drawing, reference_element, context):
def generate_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
if reference_element.is_a("IfcGridAxis"):
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
obj = tool.Ifc.get_object(drawing)
if obj is None:
obj = cls.import_drawing(drawing)
# there is no need for other drawing annotations in that case
# but we import them anyway so we won't break that drawing activation
cls.import_annotations_in_group(cls.get_drawing_group(drawing))
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
psets = ifcopenshell.util.element.get_psets(reference_element)
target_view = psets.get("EPset_Drawing", {}).get("TargetView", None)
if target_view == "ELEVATION_VIEW":
ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
elif target_view == "SECTION_VIEW":
ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_section_reference_annotation(drawing, reference_element, context)
elif reference_element.is_a("IfcBuildingStorey"):
return cls.generate_storey_annotation(drawing, reference_element, context)
@classmethod
def generate_storey_annotation(cls, drawing, reference_element, context):
def generate_storey_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
camera = tool.Ifc.get_object(drawing)
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
reference_obj = tool.Ifc.get_object(reference_element)
def to_camera_coords(camera, reference_obj):
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
mat = reference_obj.matrix_world.copy()
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
xyz[2] = 0
@@ -1162,13 +1200,18 @@ class Drawing(blenderbim.core.tool.Drawing):
return element
@classmethod
def generate_section_reference_annotation(cls, drawing, reference_element, context):
def generate_section_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
reference_obj = tool.Ifc.get_object(reference_element)
reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing)
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
def to_camera_coords(camera, reference_obj):
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
mat = reference_obj.matrix_world.copy()
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
xyz[2] = 0
@@ -1179,7 +1222,7 @@ class Drawing(blenderbim.core.tool.Drawing):
mat.translation += annotation_offset
return mat
def clip_to_camera_boundary(mesh, bounds):
def clip_to_camera_boundary(mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float]) -> Union[bpy.types.Mesh, None]:
mesh.verts.ensure_lookup_table()
points = [v.co for v in mesh.verts[0:2]]
points = helper.clip_segment(bounds, points)
@@ -1240,12 +1283,17 @@ class Drawing(blenderbim.core.tool.Drawing):
return element
@classmethod
def generate_elevation_reference_annotation(cls, drawing, reference_element, context):
def generate_elevation_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
reference_obj = tool.Ifc.get_object(reference_element)
reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing)
def to_camera_coords(camera, reference_obj):
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
mat = reference_obj.matrix_world.copy()
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
xyz[2] = 0
@@ -1272,7 +1320,12 @@ class Drawing(blenderbim.core.tool.Drawing):
return element
@classmethod
def generate_grid_axis_reference_annotation(cls, drawing, reference_element, context):
def generate_grid_axis_reference_annotation(
cls,
drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance:
target_view = tool.Drawing.get_drawing_target_view(drawing)
camera = tool.Ifc.get_object(drawing)
@@ -1300,7 +1353,7 @@ class Drawing(blenderbim.core.tool.Drawing):
mesh.to_mesh(obj.data)
return obj
def to_camera_coords(obj, mesh):
def to_camera_coords(obj: bpy.types.Object, mesh: bpy.types.Mesh) -> tuple[bpy.types.Object, bpy.types.Mesh]:
mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
obj.matrix_world = camera.matrix_world
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
@@ -1357,7 +1410,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return element
@classmethod
def is_perpendicular(cls, a, b):
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
a_quaternion = a.matrix_world.to_quaternion()
b_quaternion = b.matrix_world.to_quaternion()
@@ -1367,7 +1420,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return False
@classmethod
def get_camera_block(cls, obj):
def get_camera_block(cls, obj: bpy.types.Object) -> dict:
raster_x = obj.data.BIMCameraProperties.raster_x
raster_y = obj.data.BIMCameraProperties.raster_y
@@ -1400,7 +1453,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return {"verts": verts, "faces": faces}
@classmethod
def is_intersecting(cls, a, b):
def is_intersecting(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
a_block = cls.get_camera_block(a)
a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
b_block = cls.get_camera_block(b)
@@ -1622,7 +1675,7 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.ops.bim.activate_model()
@classmethod
def activate_drawing(cls, camera):
def activate_drawing(cls, camera: bpy.types.Object) -> None:
area = tool.Blender.get_view3d_area()
is_local_view = area.spaces[0].local_view is not None
if is_local_view:
@@ -1661,6 +1714,8 @@ class Drawing(blenderbim.core.tool.Drawing):
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
filtered_elements.add(drawing)
# hide non-filtered elemenets
for view_layer_object in bpy.context.view_layer.objects:
element = tool.Ifc.get_entity(view_layer_object)
if not element or element.is_a("IfcTypeProduct"):
@@ -1669,7 +1724,7 @@ class Drawing(blenderbim.core.tool.Drawing):
view_layer_object.hide_set(hide)
view_layer_object.hide_render = hide
subcontexts = []
subcontexts: list[ifcopenshell.entity_instance] = []
target_view = cls.get_drawing_target_view(drawing)
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"):
@@ -1702,6 +1757,7 @@ class Drawing(blenderbim.core.tool.Drawing):
if subcontext:
subcontexts.append(context_filter)
# switch representations and hide elements without representations
for element in filtered_elements:
obj = tool.Ifc.get_object(element)
current_representation = tool.Geometry.get_active_representation(obj)
@@ -1730,7 +1786,9 @@ class Drawing(blenderbim.core.tool.Drawing):
has_context = True
break
if not has_context:
# don't hide IfcAnnotations as some of them might exist without representations
# e.g. with ObjectType = SYMBOL
if not has_context and not element.is_a("IfcAnnotation"):
obj.hide_set(True)
obj.hide_render = True
@@ -1937,3 +1995,60 @@ class Drawing(blenderbim.core.tool.Drawing):
rotate180z = mathutils.Matrix.Rotation(math.radians(180.0), 4, "Z")
return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4() @ rotate180z
return mathutils.Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
@classmethod
def convert_svg_to_dxf(cls, svg_filepath: Path, dxf_filepath: Path) -> None:
import ezdxf
import xml.etree.ElementTree as ET
SVG = "{http://www.w3.org/2000/svg}"
IFC = "{http://www.ifcopenshell.org/ns}"
doc = ezdxf.new("R2010")
msp = doc.modelspace()
svg = ET.parse(svg_filepath).getroot()
def finalize_dxf():
doc.saveas(dxf_filepath)
drawing = svg.findall(f"{SVG}g[@{IFC}name]")
if not drawing:
finalize_dxf()
return
drawing = drawing[0]
NUMBER = r"-?\d+\.?\d+"
COORD = rf"{NUMBER},{NUMBER}"
POLYLINE_PATTERN = rf"M{COORD} (?:L{COORD} ?)+Z? ?"
MULTI_POLYLINE_PATTERN = rf"^({POLYLINE_PATTERN})+$"
for element_g in drawing.findall(f"{SVG}g"):
paths = element_g.findall(f"{SVG}path")
for path in paths:
path = path.attrib["d"]
if not re.match(MULTI_POLYLINE_PATTERN, path):
# print(f'Path "{path}" doesn\'t match expected pattern {MULTI_POLYLINE_PATTERN}')
continue
for polyline_path in re.findall(POLYLINE_PATTERN, path):
points = re.findall(rf"{NUMBER}", polyline_path)
points = [float(p) for p in points]
POINT_SIZE = 2
grouped_points = []
for i in range(0, len(points), POINT_SIZE):
point = points[i : i + POINT_SIZE]
point[1] *= -1
grouped_points.append(point)
points = grouped_points
# Z marks closed polylines
is_closed_polyline = polyline_path.rstrip().endswith("Z")
if is_closed_polyline or len(points) > 2:
msp.add_lwpolyline(points, close=is_closed_polyline)
else: # LINE
msp.add_line(*points)
finalize_dxf()
+4 -2
View File
@@ -32,7 +32,7 @@ import blenderbim.bim.import_ifc
from math import radians, pi
from mathutils import Vector, Matrix
from blenderbim.bim.ifc import IfcStore
from typing import List
from typing import List, Union
class Geometry(blenderbim.core.tool.Geometry):
@@ -315,7 +315,9 @@ class Geometry(blenderbim.core.tool.Geometry):
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@classmethod
def get_subcontext_parameters(cls, subcontext):
def get_subcontext_parameters(
cls, subcontext: ifcopenshell.entity_instance
) -> tuple[Union[str, None], Union[str, None], Union[str, None]]:
return (
subcontext.ContextType,
subcontext.ContextIdentifier,
+1 -1
View File
@@ -79,7 +79,7 @@ class Ifc(blenderbim.core.tool.Ifc):
return IfcStore.get_schema()
@classmethod
def get_entity(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance:
def get_entity(cls, obj: IFC_CONNECTED_TYPE) -> ifcopenshell.entity_instance:
ifc = IfcStore.get_file()
props = getattr(obj, "BIMObjectProperties", None)
if ifc and props and props.ifc_definition_id:
+27 -18
View File
@@ -20,6 +20,7 @@ import bpy
import json
import bmesh
import collections
import collections.abc
import numpy as np
import ifcopenshell
import ifcopenshell.util.unit
@@ -36,23 +37,26 @@ from blenderbim.bim import import_ifc
from blenderbim.bim.module.geometry.helper import Helper
from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData
from ifcopenshell.util.shape_builder import V, ShapeBuilder
from typing import Optional, Union, TypeVar, Any
T = TypeVar("T")
class Model(blenderbim.core.tool.Model):
@classmethod
def convert_si_to_unit(cls, value):
def convert_si_to_unit(cls, value: T) -> T:
if isinstance(value, (tuple, list)):
return [v / cls.unit_scale for v in value]
return value / cls.unit_scale
@classmethod
def convert_unit_to_si(cls, value):
def convert_unit_to_si(cls, value: T) -> T:
if isinstance(value, (tuple, list)):
return [v * cls.unit_scale for v in value]
return value * cls.unit_scale
@classmethod
def convert_data_to_project_units(cls, data, non_si_props=[]):
def convert_data_to_project_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]:
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for prop_name in data:
if prop_name in non_si_props:
@@ -65,7 +69,7 @@ class Model(blenderbim.core.tool.Model):
return data
@classmethod
def convert_data_to_si_units(cls, data, non_si_props=[]):
def convert_data_to_si_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]:
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for prop_name in data:
if prop_name in non_si_props:
@@ -78,34 +82,35 @@ class Model(blenderbim.core.tool.Model):
return data
@classmethod
def export_curve(cls, position, edge_indices, points=None):
def export_curve(cls, position: Matrix, edge_indices: list[tuple[int, int]]) -> ifcopenshell.entity_instance:
position_i = position.inverted()
ifc_file = tool.Ifc.get()
if len(edge_indices) == 2:
diameter = edge_indices[0]
p1 = cls.bm.verts[diameter[0]].co
p2 = cls.bm.verts[diameter[1]].co
center = cls.convert_si_to_unit(list(position_i @ p1.lerp(p2, 0.5)))
radius = cls.convert_si_to_unit((p1 - p2).length / 2)
return tool.Ifc.get().createIfcCircle(
tool.Ifc.get().createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2])), radius
return ifc_file.createIfcCircle(
ifc_file.createIfcAxis2Placement2D(ifc_file.createIfcCartesianPoint(center[0:2])), radius
)
if tool.Ifc.get().schema == "IFC2X3":
if ifc_file.schema == "IFC2X3":
points = []
for edge in edge_indices:
local_point = (position_i @ Vector(cls.bm.verts[edge[0]].co)).to_2d()
points.append(tool.Ifc.get().createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
points.append(ifc_file.createIfcCartesianPoint(cls.convert_si_to_unit(local_point)))
points.append(points[0])
return tool.Ifc.get().createIfcPolyline(points)
return ifc_file.createIfcPolyline(points)
segments = []
for segment in edge_indices:
if len(segment) == 2:
segments.append(tool.Ifc.get().createIfcLineIndex([i + 1 for i in segment]))
segments.append(ifc_file.createIfcLineIndex([i + 1 for i in segment]))
elif len(segment) == 3:
segments.append(tool.Ifc.get().createIfcArcIndex([i + 1 for i in segment]))
return tool.Ifc.get().createIfcIndexedPolyCurve(cls.points, segments, False)
segments.append(ifc_file.createIfcArcIndex([i + 1 for i in segment]))
return ifc_file.createIfcIndexedPolyCurve(cls.points, segments, False)
@classmethod
def export_points(cls, position, indices):
def export_points(cls, position: Matrix, indices: list[Vector]) -> ifcopenshell.entity_instance:
position_i = position.inverted()
points = []
for point in indices:
@@ -114,7 +119,10 @@ class Model(blenderbim.core.tool.Model):
return tool.Ifc.get().createIfcCartesianPointList2D(points)
@classmethod
def export_profile(cls, obj, position=None):
def export_profile(
cls, obj: bpy.types.Object, position: Optional[Matrix] = None
) -> Union[ifcopenshell.entity_instance, None]:
"""Returns `None` in case if profile was invalid."""
if position is None:
position = Matrix()
@@ -150,7 +158,7 @@ class Model(blenderbim.core.tool.Model):
return profile
@classmethod
def export_surface(cls, obj):
def export_surface(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
edge1 = p2 - p1
@@ -202,7 +210,7 @@ class Model(blenderbim.core.tool.Model):
return surface
@classmethod
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str):
def generate_occurrence_name(cls, element_type: ifcopenshell.entity_instance, ifc_class: str) -> str:
props = bpy.context.scene.BIMModelProperties
if props.occurrence_name_style == "CLASS":
return ifc_class[3:]
@@ -216,7 +224,8 @@ class Model(blenderbim.core.tool.Model):
return "Instance"
@classmethod
def get_extrusion(cls, representation):
def get_extrusion(cls, representation: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""return first found IfcExtrudedAreaSolid"""
item = representation.Items[0]
while True:
if item.is_a("IfcExtrudedAreaSolid"):
+35 -20
View File
@@ -23,14 +23,17 @@ import blenderbim.tool as tool
import ifcopenshell
from mathutils import Vector
from ifcopenshell import util
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
import ifcopenshell.util.unit
import ifcopenshell.util.element
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator, QuanityTypes
from blenderbim.bim.module.pset.calc_quantity_function_mapper import mapper
import blenderbim.bim.schema
from typing import Optional, Union, Literal
class Qto(blenderbim.core.tool.Qto):
@classmethod
def get_radius_of_selected_vertices(cls, obj):
def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float:
selected_verts = [v.co for v in obj.data.vertices if v.select]
total = Vector()
for v in selected_verts:
@@ -39,16 +42,16 @@ class Qto(blenderbim.core.tool.Qto):
return max([(v - circle_center).length for v in selected_verts])
@classmethod
def set_qto_result(cls, result):
def set_qto_result(cls, result: float) -> None:
bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3))
@classmethod
def add_object_base_qto(cls, obj):
def add_object_base_qto(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
product = tool.Ifc.get_entity(obj)
return cls.add_product_base_qto(product)
@classmethod
def add_product_base_qto(cls, product):
def add_product_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
base_quantity_name = cls.get_applicable_base_quantity_name(product)
if base_quantity_name:
return tool.Ifc.run(
@@ -58,7 +61,7 @@ class Qto(blenderbim.core.tool.Qto):
)
@classmethod
def get_applicable_quantity_names(cls, qto_name):
def get_applicable_quantity_names(cls, qto_name: str) -> list[str]:
pset_template = blenderbim.bim.schema.ifc.psetqto.get_by_name(qto_name)
return (
[property.Name for property in pset_template.HasPropertyTemplates]
@@ -67,7 +70,9 @@ class Qto(blenderbim.core.tool.Qto):
)
@classmethod
def get_applicable_base_quantity_name(cls, product=None):
def get_applicable_base_quantity_name(
cls, product: Optional[ifcopenshell.entity_instance] = None
) -> Union[str, None]:
if not product:
return
applicable_qto_names = blenderbim.bim.schema.ifc.psetqto.get_applicable_names(
@@ -76,36 +81,44 @@ class Qto(blenderbim.core.tool.Qto):
return next((qto_name for qto_name in applicable_qto_names if "Qto_" in qto_name and "Base" in qto_name), None)
@classmethod
def get_new_calculated_quantity(cls, qto_name, quantity_name, obj):
def get_new_calculated_quantity(cls, qto_name: str, quantity_name: str, obj: bpy.types.Object) -> float:
return QtoCalculator().calculate_quantity(qto_name, quantity_name, obj)
@classmethod
def get_new_guessed_quantity(cls, obj, quantity_name, alternative_prop_names):
def get_new_guessed_quantity(
cls, obj: bpy.types.Object, quantity_name: str, alternative_prop_names: list[str]
) -> Union[float, None]:
return QtoCalculator().guess_quantity(quantity_name, alternative_prop_names, obj)
@classmethod
def get_rounded_value(cls, new_quantity):
def get_rounded_value(cls, new_quantity: float) -> float:
return round(new_quantity, 3)
@classmethod
def get_calculated_object_quantities(cls, calculator, qto_name, obj):
def get_calculated_object_quantities(
cls, calculator: QtoCalculator, qto_name: str, obj: bpy.types.Object
) -> dict[str, float]:
return {
quantity_name: cls.get_rounded_value(calculator.calculate_quantity(qto_name, quantity_name, obj))
quantity_name: cls.get_rounded_value(value)
for quantity_name in cls.get_applicable_quantity_names(qto_name) or []
if cls.has_calculator(qto_name, quantity_name)
and calculator.calculate_quantity(qto_name, quantity_name, obj) is not None
and (value := calculator.calculate_quantity(qto_name, quantity_name, obj)) is not None
}
@classmethod
def has_calculator(cls, qto_name, quantity_name):
def has_calculator(cls, qto_name: str, quantity_name: str) -> bool:
return bool(mapper.get(qto_name, {}).get(quantity_name, None))
@classmethod
def convert_to_project_units(cls, value, qto_name=None, quantity_name=None, quantity_type=None):
def convert_to_project_units(
cls,
value: float,
qto_name: Optional[str] = None,
quantity_name: Optional[str] = None,
quantity_type: Optional[QuanityTypes] = None,
) -> Union[float, None]:
"""You can either specify `quantity_type` or provide `qto_name/quantity_name`
to let method figure the `quantity_type` from the templates
`quantity_type` values are `Q_LENGTH`, `Q_AREA`, `Q_VOLUME`
"""
ifc_file = tool.Ifc.get()
quantity_to_unit_types = {
@@ -135,7 +148,9 @@ class Qto(blenderbim.core.tool.Qto):
return value
@classmethod
def get_guessed_quantities(cls, obj, pset_qto_properties):
def get_guessed_quantities(
cls, obj: bpy.types.Object, pset_qto_properties: list[ifcopenshell.entity_instance]
) -> dict[str, float]:
calculated_quantities = {}
for pset_qto_property in pset_qto_properties:
quantity_name = pset_qto_property.get_info()["Name"]
@@ -153,7 +168,7 @@ class Qto(blenderbim.core.tool.Qto):
return calculated_quantities
@classmethod
def get_base_qto(cls, product):
def get_base_qto(cls, product: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
if not hasattr(product, "IsDefinedBy"):
return
for rel in product.IsDefinedBy or []:
@@ -166,7 +181,7 @@ class Qto(blenderbim.core.tool.Qto):
return rel.RelatingPropertyDefinition
@classmethod
def get_related_cost_item_quantities(cls, product):
def get_related_cost_item_quantities(cls, product: ifcopenshell.entity_instance) -> list[dict]:
"""_summary_: Returns the related cost item and related quantities of the product
:param ifc-instance product: ifc instance
+2
View File
@@ -21,6 +21,8 @@ import ifcopenshell
import ifcopenshell.util.representation
import blenderbim.core.tool
import blenderbim.core.geometry
import blenderbim.core.material
import blenderbim.core.style
import blenderbim.tool as tool
from mathutils import Vector
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
+9
View File
@@ -20,10 +20,17 @@ class Search(blenderbim.core.tool.Search):
return bpy.context.scene.BIMSearchProperties.filter_groups
elif module == "csv":
return bpy.context.scene.CsvProperties.filter_groups
elif module == "diff":
return bpy.context.scene.DiffProperties.filter_groups
elif module == "drawing_include":
return bpy.context.active_object.data.BIMCameraProperties.include_filter_groups
elif module == "drawing_exclude":
return bpy.context.active_object.data.BIMCameraProperties.exclude_filter_groups
elif module.startswith("clash"):
_, clash_set_index, ab, clash_source_index = module.split("_")
return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[
int(clash_source_index)
].filter_groups
@classmethod
def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None:
@@ -189,6 +196,8 @@ class ImportFilterQueryTransformer(lark.Transformer):
def query(self, args):
keys, comparison, value = args
if comparison == "=":
comparison = ""
return {"type": "query", "name": keys, "value": f"{comparison}{value}"}
def comparison(self, args):
+1 -1
View File
@@ -150,9 +150,9 @@ class Spatial(blenderbim.core.tool.Spatial):
for product in products:
obj = tool.Ifc.get_object(product)
if obj and bpy.context.view_layer.objects.get(obj.name):
obj.select_set(True)
if unhide:
obj.hide_set(False)
obj.select_set(True)
@classmethod
def filter_products(cls, products, action):
+1 -1
View File
@@ -96,7 +96,7 @@ class Style(blenderbim.core.tool.Style):
return obj.name
@classmethod
def get_style(cls, obj):
def get_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]:
if obj.BIMMaterialProperties.ifc_style_id:
try:
return tool.Ifc.get().by_id(obj.BIMMaterialProperties.ifc_style_id)
+37 -5
View File
@@ -1,12 +1,44 @@
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700&display=swap');
h1, h2, h3, h4 {
font-weight: normal;
color: #7cbf33;
background: -webkit-linear-gradient(0deg, #7cbf33, #049344);
background-clip: border-box;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
a {
text-decoration: none;
}
.toc-tree .reference:hover {
color: #d98014;
}
.sidebar-brand-text {
font-size: 1rem;
}
.blockbutton {
border: 1px solid var(--color-admonition-title--seealso);
background-color: var(--color-admonition-title-background--seealso);
padding: 5px;
text-align: center;
border-radius: 5px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
text-align: center;
}
.blockbutton a {
display: block;
padding-top: 15px;
padding-bottom: 15px;
border-radius: 5px;
background: #40b74c;
width: 100%;
height: 100%;
color: #fff;
text-decoration: none;
}
.blockbutton a:hover {
background: #70ba35;
}
.highlight .hll {
background-color: #ffc2;
}
section img {
display: block;
+58 -2
View File
@@ -36,7 +36,7 @@
# -- Project information -----------------------------------------------------
project = "BlenderBIM Add-on"
copyright = "2020-2023 IfcOpenShell Contributors"
copyright = "2020-2024 IfcOpenShell Contributors"
author = "IfcOpenShell Contributors"
# The full version, including alpha/beta/rc tags
@@ -48,7 +48,7 @@ release = "0.0.231104"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel"]
extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosectionlabel", "sphinx_copybutton"]
# Auto add document prefixes to help guarantee uniqueness of automatic section references.
autosectionlabel_prefix_document = True
@@ -75,3 +75,59 @@ html_theme = "furo"
html_static_path = ["_static"]
html_css_files = ["custom.css"]
# Code block styles. Dark styling helps important code examples "pop" on the
# page even on light themes.
pygments_style = "one-dark"
pygments_dark_style = "one-dark"
html_logo = "https://blenderbim.org/assets/images/blender/blender-logo.png"
html_theme_options = {
"source_repository": "https://github.com/IfcOpenShell/IfcOpenShell/",
"source_branch": "v0.7.0",
"source_directory": "src/blenderbim/docs/",
"light_css_variables": {
"color-brand-primary": "#39b54a",
"color-brand-content": "#39b54a",
"color-brand-visited": "#d9e021",
"color-background-primary": "#f7f7f6",
"color-background-secondary": "#eeeeec",
"color-background-border": "#cfd0cb",
"color-foreground-primary": "#2e3436",
"color-sidebar-item-background--hover": "#f7f7f6",
"color-link--hover": "#d98014",
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
},
"dark_css_variables": {
"color-brand-primary": "#39b54a",
"color-brand-content": "#39b54a",
"color-brand-visited": "#d9e021",
"color-background-primary": "#2e3436",
"color-background-border": "#2e3436",
"color-foreground-primary": "#eeeeec",
"color-sidebar-item-background--hover": "#2e3436",
"color-link--hover": "#d98014",
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
},
"footer_icons": [
{
"name": "IfcOpenShell",
"url": "https://ifcopenshell.org",
"html": """
<img src="https://ifcopenshell.org/assets/images/logo.png" style="width: auto;" />
""",
"class": "",
},
{
"name": "GitHub",
"url": "https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim/docs",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
""",
"class": "",
},
],
}
+8 -10
View File
@@ -22,8 +22,8 @@ You will need to choose which build to download.
- If you are on Blender >=4.1, choose py311
- If you are on Blender >=3.1 and <=4.0, choose py10
- If you are on Blender >=2.93 and <3.1, choose py39
- Choose ``linux``, ``macos``, ``macosm1`` (for Apple M1 devices), or ``win``
depending on your operating system
- Choose ``linux``, ``macos`` (Apple Intel), ``macosm1`` (Apple Silicon), or
``win`` depending on your operating system
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
but it happens.
@@ -43,11 +43,11 @@ compile IfcOpenShell is out of scope of this document.
You can create your own package by using the Makefile as shown below. You can
choose between a ``PLATFORM`` of ``linux``, ``macos``, ``macosm1``, and ``win``.
You can choose between a ``PYVERSION`` of ``py39``, or ``py310``.
You can choose between a ``PYVERSION`` of ``py311``, ``py310``, or ``py39``.
::
$ cd src/blenderbim
$ make dist PLATFORM=linux PYVERSION=py310
$ make dist PLATFORM=linux PYVERSION=py311
$ ls dist/
This will give you a fully packaged Blender add-on zip that you can distribute
@@ -258,10 +258,12 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
ifcclash
bimtester
ifccobie
ifcdiff
ifccsv
ifcdiff
ifc4d
ifc5d
ifcpatch
ifcp6
ifctester
pystache
svgwrite
dateutil
@@ -276,17 +278,13 @@ Required Python modules to be stored in ``libs/site/packages/`` are:
elementpath
six
lark-parser
hppfcl
behave
parse
parse_type
xlsxwriter
odfpy
defusedxml
boto3
botocore
jmespath
s3transfer
ifcjson
Notes:
+6 -2
View File
@@ -6,9 +6,13 @@ the Python version shipped by the Blender Foundation for the most recent three
major Blender versions:
- 64-bit Linux on Python 3.10
- 64-bit MacOS on Python 3.10
- 64-bit MacOS M1 on Python 3.10
- 64-bit Linux on Python 3.11
- 64-bit MacOS Intel on Python 3.10
- 64-bit MacOS Intel on Python 3.11
- 64-bit MacOS Silicon on Python 3.10
- 64-bit MacOS Silicon on Python 3.11
- 64-bit Windows on Python 3.10
- 64-bit Windows on Python 3.11
Note that developer builds may exist for older versions of Python but there will
be no guarantee of the uptime or stability of these builds.
+26
View File
@@ -12,6 +12,8 @@ Writing technical documentation
All documentation is written in ReStructured Text and is available in the
`BlenderBIM Add-on docs directory
<https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim/docs>`_.
You can press the edit button on the top right on any documentation page to
quickly edit their content.
You can link to `external websites
<https://docs.readthedocs.io/en/stable/guides/cross-referencing-with-sphinx.html>`_.
@@ -86,3 +88,27 @@ Tables can be very annoying to format. You can use a CSV table instead.
"ABC", "01", "02"
"DEF", "03", "04"
Building documentation
----------------------
If you want to build the documentation locally, the documentation system uses
`Sphinx <https://www.sphinx-doc.org/en/master/>`_. First, install the theme and
theme dependencies:
.. code-block:: console
$ pip install furo
$ pip install sphinx-autoapi
$ pip install sphinx-copybutton
Now you can generate the documentation:
.. code-block:: console
$ cd /path/to/ifcopenshell/src/blenderbim/docs/
$ make html
$ cd _build/html
$ python -m http.server
You will now have a local webserver running hosting the documentation.
@@ -70,8 +70,8 @@ In the top left **File** menu, Save your new IFC model on your computer.
Congratulations! You have now created your first OpenBIM model from Blender! You
can open the IFC file in any other program, and you will see something similar
to the image below. Three simple open source online viewers you can test with
are `IfcPipeline <https://view.ifcopenshell.org>`__, `IFC.JS
<https://ifcjs.github.io/web-ifc-viewer/example/index>`__, and `3DViewer
are `IfcPipeline <https://view.ifcopenshell.org>`__, `ThatOpenEditor
<https://platform.thatopen.com/app>`__, and `3DViewer
<https://3dviewer.net/>`__.
.. image:: images/ifc-pipeline.png
+1 -1
View File
@@ -25,7 +25,7 @@ Installation
.. warning::
If you are not using Blender version >=3.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>`
If you are not using Blender version >=4.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>`
3. **Install the BlenderBIM Add-on**
Binary file not shown.
@@ -0,0 +1,138 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 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/>.
import os
import bpy
import ifcopenshell
import ifcopenshell.api
import blenderbim.tool as tool
from pathlib import Path
# When run from Blender
BLEND_DIR = os.path.dirname(bpy.data.filepath)
OUT_PATH = os.path.join(BLEND_DIR, "..", "blenderbim", "bim", "data", "libraries", "IFC4 Entourage Library.ifc")
class LibraryGenerator:
def generate(self, library_name, output_filename):
ifcopenshell.api.pre_listeners = {}
ifcopenshell.api.post_listeners = {}
self.materials = {}
self.file = ifcopenshell.api.run("project.create_file")
self.project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject", name=library_name)
self.library = ifcopenshell.api.run(
"root.create_entity", self.file, ifc_class="IfcProjectLibrary", name=library_name
)
ifcopenshell.api.run(
"project.assign_declaration", self.file, definition=self.library, relating_context=self.project
)
unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
self.representations = {
"Model/Body/MODEL_VIEW": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model,
),
"Plan/Body/PLAN_VIEW": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Plan",
context_identifier="Body",
target_view="PLAN_VIEW",
parent=plan,
),
"Model/Body/PLAN_VIEW": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="PLAN_VIEW",
parent=model,
),
"Model/Body/SECTION_VIEW": ifcopenshell.api.run(
"context.add_context",
self.file,
context_type="Model",
context_identifier="Body",
target_view="SECTION_VIEW",
parent=model,
),
}
# Manually modeled trees
for obj in bpy.data.objects:
if not obj.type == "MESH":
continue
if "Plan/" in obj.name or "Model/" in obj.name:
continue
representations = {"Model/Body/MODEL_VIEW": obj.name}
for rep_key in self.representations.keys():
rep_obj = bpy.data.objects.get(obj.name + " " + rep_key)
if rep_obj:
representations[rep_key] = rep_obj.name
self.create_type("IfcBuildingElementProxyType", obj.name, representations)
self.file.write(output_filename)
def create_type(self, ifc_class, name, representations):
element = ifcopenshell.api.run(
"root.create_entity", self.file, ifc_class=ifc_class, predefined_type="ENTOURAGE", name=name
)
for rep_name, obj_name in representations.items():
obj = bpy.data.objects.get(obj_name)
representation = ifcopenshell.api.run(
"geometry.add_representation",
self.file,
context=self.representations[rep_name],
blender_object=obj,
geometry=obj.data,
total_items=max(1, len(obj.material_slots)),
)
styles = []
for slot in obj.material_slots:
style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name)
ifcopenshell.api.run(
"style.add_surface_style",
self.file,
style=style,
ifc_class="IfcSurfaceStyleShading",
attributes=tool.Style.get_surface_shading_attributes(slot.material),
)
styles.append(style)
if styles:
ifcopenshell.api.run(
"style.assign_representation_styles", self.file, shape_representation=representation, styles=styles
)
ifcopenshell.api.run(
"geometry.assign_representation", self.file, product=element, representation=representation
)
ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
if __name__ == "__main__":
LibraryGenerator().generate("Entourage Assets Library", output_filename=OUT_PATH)
@@ -438,7 +438,7 @@ Scenario: Override duplicate move - copying an aggregate
And the object "IfcElementAssembly/Assembly.001" exists
And the object "IfcElementAssembly/Assembly.001" is in the collection "IfcElementAssembly/Assembly.001"
And the collection "IfcElementAssembly/Assembly.001" is in the collection "IfcBuildingStorey/My Storey"
Scenario: Override duplicate move - copying objects with connection
Given an empty IFC project
And I load the demo construction library
@@ -467,7 +467,7 @@ Scenario: Override duplicate move - copying objects with connection
Then the object "IfcSlab/Slab.001" exists
And the variable "slab_name" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcSlab/Slab.001'][0]"
Then the object "{wall_name}" has a connection with "{slab_name}"
Scenario: Override duplicate move - copying walls with mitre joint
Given an empty IFC project
And I load the demo construction library
@@ -477,24 +477,24 @@ Scenario: Override duplicate move - copying walls with mitre joint
And I press "bim.hotkey(hotkey='S_A')"
And the cursor is at "0.5,0,0"
And I press "bim.hotkey(hotkey='S_A')"
And the object "IfcWall/Wall" is selected
And additionally the object "IfcWall/Wall.001" is selected
And the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall" is selected
When I press "bim.hotkey(hotkey='S_Y')"
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
When I deselect all objects
And the object "IfcWall/Wall" is selected
And additionally the object "IfcWall/Wall.001" is selected
And the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall" is selected
When I duplicate the selected objects
Then the object "IfcWall/Wall.002" exists
And the variable "wall_name1" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.002'][0]"
Then the object "IfcWall/Wall.003" exists
And the variable "wall_name2" is "[o.name for o in bpy.context.selected_objects if o.name == 'IfcWall/Wall.003'][0]"
Then the object "{wall_name1}" has a connection with "{wall_name2}"
Scenario: Override duplicate move linked - without active IFC data
Given an empty Blender session
And I add a cube
@@ -544,14 +544,14 @@ Scenario: Override paste buffer - with active IFC data
And the object "IfcWall/Cube" is selected
And additionally the object "IfcBuildingStorey/My Storey" is selected
When I press "view3d.copybuffer"
# IFC elements unlinked on paste for safety
And I press "bim.override_paste_buffer"
Then the object "IfcWall/Cube" exists
And the object "IfcWall/Cube" is an "IfcWall"
And the object "IfcWall/Cube.001" exists
And the object "IfcWall/Cube.001" is an "IfcWall"
And the object "IfcWall/Cube.001" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
And the object "IfcBuildingStorey/My Storey.001" exists
And the object "IfcBuildingStorey/My Storey.001" is an "IfcBuildingStorey"
And the object "Cube.001" exists
And the object "Cube.001" is not an IFC element
And the object "My Storey.001" exists
And the object "My Storey.001" is not an IFC element
Scenario: Duplicate linked aggregate
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
@@ -559,8 +559,8 @@ Scenario: Duplicate linked aggregate
When I duplicate linked aggregate the selected objects
Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists
And the object "IfcElementAssembly/Assembly_02" exists
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_02" belong to the same Linked Aggregate Group
And the object "IfcElementAssembly/Assembly_01" exists
Then the object "IfcElementAssembly/Assembly" and "IfcElementAssembly/Assembly_01" belong to the same Linked Aggregate Group
Scenario: Refresh linked aggregate
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
@@ -588,7 +588,7 @@ Scenario: Refresh linked aggregate - after deleting an object
When I refresh linked aggregate the selected object
Then the object "IfcWall/Wall_01" does not exist
And the object "IfcWall/Wall_02" exists
Scenario: Refresh linked aggregate - after duplicating an object
Given I load the IFC test file "/test/files/linked-aggregates.ifc"
And the object "IfcWall/Wall_01" is selected
@@ -606,4 +606,4 @@ Scenario: Refresh linked aggregate - after duplicating an object
Then the object "IfcWall/Wall_01.001" exists
And the object "IfcWall/Wall_02.001" exists
And the object "IfcWall/Wall_03.001" exists
+42 -42
View File
@@ -107,11 +107,11 @@ Scenario: Add a wall perpendicular to an existing wall
And I press "bim.hotkey(hotkey='S_A')"
And the cursor is at "0.5,0,0"
When I press "bim.hotkey(hotkey='S_A')"
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
Scenario: Extend one wall to another
Given an empty IFC project
@@ -122,35 +122,17 @@ Scenario: Extend one wall to another
And I press "bim.hotkey(hotkey='S_A')"
And the cursor is at "0.5,0,0"
And I press "bim.hotkey(hotkey='S_A')"
And the object "IfcWall/Wall" is moved to "0.5,-1,0"
And the object "IfcWall/Wall" is selected
And additionally the object "IfcWall/Wall.001" is selected
And the object "IfcWall/Wall.001" is moved to "0.5,-1,0"
And the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall" is selected
When I press "bim.hotkey(hotkey='S_E')"
Then the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0,0,0"
And the object "IfcWall/Wall" dimensions are "2,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall" top right corner is at "0.6,-2,3"
Then the object "IfcWall/Wall" dimensions are "1,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0,0,0"
And the object "IfcWall/Wall.001" dimensions are "2,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall.001" top right corner is at "0.6,-2,3"
Scenario: Join two walls with a butt joint - first wall has priority
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.hotkey(hotkey='S_A')"
And the cursor is at "0.5,0,0"
And I press "bim.hotkey(hotkey='S_A')"
And the object "IfcWall/Wall" is selected
And additionally the object "IfcWall/Wall.001" is selected
When I press "bim.hotkey(hotkey='S_T')"
Then the object "IfcWall/Wall.001" dimensions are "0.4,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.6,0,0"
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
Scenario: Join two walls with a butt joint - second wall has priority
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
@@ -162,13 +144,13 @@ Scenario: Join two walls with a butt joint - second wall has priority
And the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall" is selected
When I press "bim.hotkey(hotkey='S_T')"
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall" dimensions are "1,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
Then the object "IfcWall/Wall" dimensions are "0.4,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.6,0,0"
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
Scenario: Join two walls with a mitre joint
Scenario: Join two walls with a butt joint - second wall has priority
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
@@ -179,12 +161,30 @@ Scenario: Join two walls with a mitre joint
And I press "bim.hotkey(hotkey='S_A')"
And the object "IfcWall/Wall" is selected
And additionally the object "IfcWall/Wall.001" is selected
When I press "bim.hotkey(hotkey='S_Y')"
Then the object "IfcWall/Wall.001" dimensions are "0.5,0.1,3"
When I press "bim.hotkey(hotkey='S_T')"
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall.001" dimensions are "1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall" dimensions are "1.1,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0.1,0"
And the object "IfcWall/Wall" top right corner is at "0.6,-1,3"
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
Scenario: Join two walls with a mitre joint
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.hotkey(hotkey='S_A')"
And the cursor is at "0.5,0,0"
And I press "bim.hotkey(hotkey='S_A')"
And the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall" is selected
When I press "bim.hotkey(hotkey='S_Y')"
Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3"
And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0"
And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0"
And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3"
Scenario: Change the height of a wall
Given an empty IFC project
+44 -28
View File
@@ -313,49 +313,65 @@ Scenario: Unload project
Scenario: Link IFC
Given an empty IFC project
When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "True"
And the object "IfcWall/Wall" exists
And the object "IfcSlab/Slab" exists
And the object "IfcElementAssembly/Empty" exists
And the object "IfcBeam/Beam" exists
And the object "IfcBuildingStorey/Ground Floor" exists
And the object "IfcBuildingStorey/Level 1" exists
When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
And the collection "IfcProject/basic.ifc" exists
And the object "Chunk" exists
And the object "Chunk" is placed in the collection "IfcProject/basic.ifc"
Scenario: Toggle link visibility - wireframe mode
Given an empty IFC project
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.blend', mode='WIREFRAME')"
Then nothing happens
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "True"
And the object "Chunk" should display as "WIRE"
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "False"
And the object "Chunk" should display as "TEXTURED"
Scenario: Toggle link selectability
Given an empty IFC project
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "False"
And the collection "IfcProject/basic.ifc" is unselectable
When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "True"
And the collection "IfcProject/basic.ifc" is selectable
Scenario: Toggle link visibility - visible mode
Given an empty IFC project
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.blend', mode='VISIBLE')"
Then nothing happens
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "True"
And the collection "IfcProject/basic.ifc" exclude status is "True"
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "False"
And the collection "IfcProject/basic.ifc" exclude status is "False"
Scenario: Unload link
Given an empty Blender session
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
When I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "False"
And "scene.collection.children.get('IfcProject/My Project')" is "None"
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
When I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "False"
And the collection "IfcProject/basic.ifc" does not exist
Scenario: Load link
Given an empty Blender session
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')"
When I press "bim.load_link(filepath='{cwd}/test/files/basic.blend')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.blend'].is_loaded" is "True"
And "scene.collection.children['IfcProject/My Project'].users" is "2"
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
When I press "bim.load_link(filepath='{cwd}/test/files/basic.ifc')"
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
And the collection "IfcProject/basic.ifc" exists in viewlayer
Scenario: Unlink IFC
Given an empty Blender session
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.blend')"
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.blend')"
When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.blend')"
Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.blend')" is "None"
And "scene.collection.children.get('IfcProject/My Project')" is "None"
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.ifc')"
Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.ifc')" is "None"
And "scene.collection.children.get('IfcProject/basic.ifc')" is "None"
And the object "Chunk" does not exist
Scenario: Export IFC - blank project
Given an empty IFC project
+5 -6
View File
@@ -387,10 +387,10 @@ Scenario: Edit pset length property
When I set "active_object.PsetProperties.properties['NosingLength'].metadata.length_value" to "0.45"
Then "active_object.PsetProperties.properties['NosingLength'].metadata.float_value" is roughly "450"
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
Then nothing happens
Scenario: Edit qset length property
Given an empty IFC project
And I press "mesh.add_clever_stair"
@@ -401,15 +401,14 @@ Scenario: Edit qset length property
# Testing Q_LENGTH type of prop
Then "active_object.PsetProperties.properties['Length'].metadata.special_type" is "LENGTH"
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57495"
And "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "2492.57397"
And "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "2.49257"
When I set "active_object.PsetProperties.properties['Length'].metadata.float_value" to "350"
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "350"
Then "active_object.PsetProperties.properties['Length'].metadata.length_value" is roughly "0.35"
When I set "active_object.PsetProperties.properties['Length'].metadata.length_value" to "0.45"
Then "active_object.PsetProperties.properties['Length'].metadata.float_value" is roughly "450"
When I press "bim.edit_pset(obj='IfcStairFlight/StairFlight', obj_type='Object')"
Then nothing happens
+1 -1
View File
@@ -24,7 +24,7 @@ Scenario: Unlink object
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I press "bim.add_material"
And I press "bim.add_style()"
When I press "bim.unlink_object(obj='IfcWall/Cube')"
Then the object "Cube" is not an IFC element
And the material "Material" is an IFC style
@@ -8,9 +8,9 @@ Scenario: Select all walls
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I press "bim.assign_class"
And I add a new item to "scene.IfcSelectorProperties.groups"
And I add a new item to "scene.IfcSelectorProperties.groups[0].queries"
And I set "scene.IfcSelectorProperties.groups[0].queries[0].selector" to "IFC Class"
And I set "scene.IfcSelectorProperties.groups[0].queries[0].active_option" to "124: IfcWall"
When I press "bim.filter_model_elements(option='select')"
And I press "bim.add_filter_group(module='search')"
And I set "scene.BIMSearchProperties.facet" to "entity"
And I press "bim.add_filter(index=0, type='entity', module='search')"
And I set "scene.BIMSearchProperties.filter_groups[0].filters[0].value" to "IfcWall"
When I press "bim.search"
Then the object "IfcWall/Cube" is selected
+23 -16
View File
@@ -18,7 +18,7 @@ Scenario: Remove style
When I press "bim.remove_style(style={style})"
Then nothing happens
Scenario: Add style
Scenario: Add style to current blender material
Given an empty IFC project
And I add a cube
And I add a material
@@ -33,32 +33,39 @@ Scenario: Unlink style
When I press "bim.unlink_style"
Then the material "Material" is not an IFC style
Scenario: Enable editing style
Scenario: Add a new style
Given an empty IFC project
And I add a cube
And I add a material
And I press "bim.add_style"
When I press "bim.enable_editing_style"
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
And I press "bim.enable_adding_presentation_style"
When I press "bim.add_presentation_style"
Then nothing happens
Scenario: Disable editing style
Scenario: Disable adding a new style
Given an empty IFC project
And I add a cube
And I add a material
And I press "bim.add_style"
And I press "bim.enable_editing_style"
When I press "bim.disable_editing_style"
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
When I press "bim.disable_adding_presentation_style"
Then nothing happens
Scenario: Edit style
Given an empty IFC project
And I add a cube
And I add a material
And I press "bim.add_style"
And I press "bim.enable_editing_style"
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
And I press "bim.enable_adding_presentation_style"
And I press "bim.add_presentation_style"
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
And I press "bim.enable_editing_style(style={style})"
When I press "bim.edit_style"
Then nothing happens
Scenario: Disable editing style
Given an empty IFC project
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
And I press "bim.enable_adding_presentation_style"
And I press "bim.add_presentation_style"
And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
And I press "bim.enable_editing_style(style={style})"
When I press "bim.disable_editing_style"
Then nothing happens
Scenario: Load styles
Given an empty IFC project
And I add a cube
+68 -32
View File
@@ -29,11 +29,12 @@ from blenderbim.bim.module.model.data import AuthoringData
from pytest_bdd import scenarios, given, when, then, parsers
from mathutils import Vector
from math import radians
from pathlib import Path
scenarios("feature")
variables = {
"cwd": os.getcwd(),
"cwd": Path.cwd().as_posix(),
"ifc": "IfcStore.get_file()",
"pset_ifc": "IfcStore.pset_template_file",
"classification_ifc": "IfcStore.classification_file",
@@ -44,15 +45,13 @@ webbrowser.open = lambda x: True
def replace_variables(value):
if "{cwd}" in value and os.name == "nt":
value = value.replace("/", "\\").replace("{cwd}", os.getcwd()).replace("\\", "\\\\")
for key, new_value in variables.items():
value = value.replace("{" + key + "}", str(new_value))
return value
def is_x(number, x):
return abs(number - x) < 1e-6
return abs(number - x) < 1e-5
def vectors_are_equal(v1, v2):
@@ -177,7 +176,7 @@ def the_material_name_colour_is_set_to_colour(name, colour):
@given("I add an array modifier")
def i_add_a_cube():
def i_add_an_array_modifier():
bpy.ops.object.modifier_add(type="ARRAY")
@@ -202,11 +201,13 @@ def i_press_operator_and_expect_error(operator, error_msg):
else:
exec(f"bpy.ops.{operator}()")
assert False, f"Operator bpy.ops.{operator} ran without exception '{error_msg}'"
except Exception as e:
except Exception as e:
actual_error_msg = str(e).strip()
if str(e).strip() != error_msg:
traceback.print_exc()
assert False, f"Got different exception running bpy.ops.{operator} - '{actual_error_msg}' instead of '{error_msg}'"
msg = f"Got different exception running bpy.ops.{operator} - '{actual_error_msg}' instead of '{error_msg}'"
assert False, msg
@given(parsers.parse('I press "{operator}"'))
@when(parsers.parse('I press "{operator}"'))
@@ -233,12 +234,14 @@ def i_evaluate_expression(expression):
def i_duplicate_the_selected_objects():
bpy.ops.bim.override_object_duplicate_move()
blenderbim.bim.handler.active_object_callback()
@when("I duplicate linked aggregate the selected objects")
def i_duplicate_the_selected_objects():
def i_duplicate_linked_aggregate_the_selected_objects():
bpy.ops.bim.object_duplicate_move_linked_aggregate()
blenderbim.bim.handler.active_object_callback()
@when("I refresh linked aggregate the selected object")
def i_refresh_the_selected_objects():
bpy.ops.bim.refresh_linked_aggregate()
@@ -291,12 +294,18 @@ def the_object_name_is_scaled_to_scale(name, scale):
@given(parsers.parse('the object "{name}" is placed in the collection "{collection}"'))
@when(parsers.parse('the object "{name}" is placed in the collection "{collection}"'))
def the_object_name_is_placed_in_the_collection_collection(name, collection):
def the_object_name_is_placed_in_the_collection_collection(name: str, collection: str) -> None:
obj = the_object_name_exists(name)
[c.objects.unlink(obj) for c in obj.users_collection]
bpy.data.collections.get(collection).objects.link(obj)
@then(parsers.parse('the object "{name}" is placed in the collection "{collection}"'))
def then_the_object_name_is_placed_in_the_collection_collection(name: str, collection: str) -> None:
obj = the_object_name_exists(name)
assert obj in bpy.data.collections.get(collection).objects[:]
@given(parsers.parse('additionally the object "{name}" is selected'))
@when(parsers.parse('additionally the object "{name}" is selected'))
def additionally_the_object_name_is_selected(name):
@@ -329,7 +338,7 @@ def i_set_prop_to_value(prop, value):
@given(parsers.parse('I set "{prop}" to ""'))
@when(parsers.parse('I set "{prop}" to ""'))
def i_set_prop_to_value(prop):
def i_set_prop_to_empty_string(prop):
try:
eval(f"bpy.context.{prop}")
except:
@@ -363,7 +372,7 @@ def nothing_happens():
@then(parsers.parse('the object "{name}" exists'))
def the_object_name_exists(name) -> bpy.types.Object:
def the_object_name_exists(name: str) -> bpy.types.Object:
obj = bpy.data.objects.get(name)
if not obj:
assert False, f'The object "{name}" does not exist'
@@ -377,6 +386,7 @@ def the_object_name_does_not_exist(name) -> bpy.types.Object:
assert True, f'The object "{name}" exists'
return obj
@given(parsers.parse('the collection "{name}" exists'))
@when(parsers.parse('the collection "{name}" exists'))
@then(parsers.parse('the collection "{name}" exists'))
@@ -387,6 +397,33 @@ def the_collection_name_exists(name) -> bpy.types.Collection:
return obj
@then(parsers.parse('the collection "{name}" is selectable'))
def the_collection_name_is_selectable(name: str) -> None:
col = the_collection_name_exists(name)
assert col.hide_select == False
@then(parsers.parse('the collection "{name}" is unselectable'))
def the_collection_name_is_unselectable(name: str) -> None:
col = the_collection_name_exists(name)
assert col.hide_select == True
@then(parsers.parse('the collection "{name}" exists in viewlayer'))
def the_collection_exists_in_viewlayer(name: str) -> bpy.types.LayerCollection:
col = the_collection_name_exists(name)
layer = tool.Blender.get_layer_collection(col)
if not layer:
assert False, f'The collection "{name}" is not present in the current viewlayer'
return layer
@then(parsers.parse('the collection "{name}" exclude status is "{exclude}"'))
def the_collection_exclude_status_is(name: str, exclude: str) -> None:
layer = the_collection_exists_in_viewlayer(name)
assert layer.exclude == (exclude == "True")
@then(parsers.parse('the object "{name1}" and "{name2}" are different elements'))
def the_object_name1_and_name2_are_different_elements(name1, name2):
ifc = an_ifc_file_exists()
@@ -413,7 +450,7 @@ def the_object_name_has_a_representation_type_of_context(name, type, context):
@given(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"'))
@then(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"'))
def the_object_name_has_a_representation_type_of_context(name, type, context):
def the_object_name_data_is_a_type_representation_of_context(name, type, context):
ifc = an_ifc_file_exists()
context, subcontext, target_view = context.split("/")
rep = ifc.by_id(the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id)
@@ -518,7 +555,7 @@ def the_object_name_has_no_data(name):
@then(parsers.parse('the object "{name}" has data which is an IFC representation'))
def the_object_name_is_not_an_ifc_element(name):
def the_object_name_has_ifc_representation_data(name):
id = the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id
assert id != 0, f"The ID is {id}"
@@ -552,7 +589,7 @@ def the_material_name_is_not_an_ifc_style(name):
@then(parsers.parse('the material "{name}" colour is "{colour}"'))
def the_material_name_colour_is_set_to_colour(name, colour):
def then_the_material_name_colour_is_set_to_colour(name, colour):
diffuse_color = list(the_material_name_exists(name).diffuse_color)
assert diffuse_color == [float(c) for c in colour.split(",")], f"The colour is {diffuse_color}"
@@ -616,7 +653,7 @@ def prop_is_value(prop, value):
@then(parsers.parse('"{prop}" is roughly "{value}"'))
def prop_is_value(prop, value):
def prop_is_roughly_value(prop, value):
prop = replace_variables(prop)
value = replace_variables(value)
is_value = False
@@ -661,6 +698,7 @@ def the_object_name_has_a_thickness_thick_layered_material_containing_the_materi
assert is_x(total_thickness, float(thickness))
assert material_name in material_names
@then(parsers.parse('the object "{name}" has no IFC materials'))
def the_object_has_no_ifc_materials(name):
element = tool.Ifc.get_entity(the_object_name_exists(name))
@@ -705,12 +743,6 @@ def the_collection_name1_is_in_the_collection_name2(name1, name2):
assert bpy.data.collections.get(name2).children.get(name1)
@then(parsers.parse('the object "{name}" does not exist'))
def the_object_name_does_not_exist(name):
obj = bpy.data.objects.get(name)
assert obj is None or len(obj.users_collection) == 0, f"Object {name} exists"
@then(parsers.parse('the collection "{name}" does not exist'))
def the_collection_name_does_not_exist(name):
obj = bpy.data.collections.get(name)
@@ -753,7 +785,7 @@ def the_object_name_dimensions_are_dimensions(name, dimensions):
@then(parsers.parse('the object "{name}" top right corner is at "{location}"'))
def the_object_name_is_at_location(name, location):
def the_object_name_top_right_corner_is_at_location(name, location):
obj = the_object_name_exists(name)
obj_corner = obj.matrix_world @ Vector(obj.bound_box[6])
assert (
@@ -762,7 +794,7 @@ def the_object_name_is_at_location(name, location):
@then(parsers.parse('the object "{name}" bottom left corner is at "{location}"'))
def the_object_name_is_at_location(name, location):
def the_object_name_bottom_left_corner_is_at_location(name, location):
obj = the_object_name_exists(name)
obj_corner = obj.matrix_world @ Vector(obj.bound_box[0])
assert (
@@ -791,11 +823,13 @@ def the_file_name_should_contain_value(name, value):
def the_object_name_has_no_modifiers(name):
assert len(the_object_name_exists(name).modifiers) == 0
@given(parsers.parse('I load the IFC test file "{filepath}"'))
def i_load_the_ifc_test_file(filepath):
filepath = f"{variables['cwd']}{filepath}"
bpy.ops.bim.load_project(filepath=filepath, use_relative_path=True)
@given("I load the demo construction library")
@when("I load the demo construction library")
def i_add_a_construction_library():
@@ -836,7 +870,7 @@ def move_cursor_bottom_left():
@given(parsers.parse("I prepare to undo"))
@when(parsers.parse("I prepare to undo"))
@then(parsers.parse("I prepare to undo"))
def hit_undo():
def prepare_undo():
bpy.ops.ed.undo_push(message="UNDO STEP")
@@ -847,6 +881,7 @@ def hit_undo():
bpy.ops.ed.undo_push(message="UNDO STEP")
bpy.ops.ed.undo()
@then(parsers.parse('the object "{obj_name1}" has a connection with "{obj_name2}"'))
def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2):
element1 = replace_variables(obj_name1)
@@ -866,9 +901,11 @@ def the_obj1_has_a_connection_with_obj2(obj_name1, obj_name2):
relationships[conn.RelatedElement] = conn.RelatingElement
for key, value in relationships.items():
assert (key.id() == element1.id() and value.id() == element2.id()) or (key.id() == element2.id() and value.id() == element1.id()), f"The object {obj_name1} is connected to {obj_name2}"
assert (key.id() == element1.id() and value.id() == element2.id()) or (
key.id() == element2.id() and value.id() == element1.id()
), f"The object {obj_name1} is connected to {obj_name2}"
@then(parsers.parse('the object "{obj_name1}" and "{obj_name2}" belong to the same Linked Aggregate Group'))
def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(obj_name1, obj_name2):
element1 = replace_variables(obj_name1)
@@ -892,6 +929,7 @@ def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(obj_name1, obj_name
assert groups[0] == groups[1], "Objects do not belong to the same Linked Aggregate group"
@when(parsers.parse('the object layer length is set to "{value}"'))
def the_obj_layer_lenght_is_set_to(value):
value = float(value)
@@ -900,11 +938,11 @@ def the_obj_layer_lenght_is_set_to(value):
except:
assert False, f"Property BIMModelProperties.length does not exist when trying to set to value {value}"
print(50*"@", bpy.context.selected_objects)
print(50 * "@", bpy.context.selected_objects)
bpy.context.scene.BIMModelProperties.length = value
bpy.ops.bim.change_layer_length(length=value)
# These definitions are not to be used in tests but simply in debugging failing tests
@@ -953,5 +991,3 @@ def run_pdb():
import pdb
pdb.set_trace()
+29 -2
View File
@@ -18,6 +18,7 @@
import datetime
from datetime import timedelta, date
from typing import List
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.date
@@ -25,7 +26,7 @@ import xml.etree.ElementTree as ET
class MSP2Ifc:
def __init__(self):
def __init__(self, optionalColumns: List[str] = []):
self.xml = None
self.file = None
self.ns = None
@@ -33,6 +34,7 @@ class MSP2Ifc:
self.project = {}
self.calendars = {}
self.tasks = {}
self.optionalColumns = optionalColumns
self.resources = {}
self.RESOURCE_TYPES_MAPPING = {"1": "LABOR", "0": "MATERIAL", "2": None}
@@ -106,6 +108,18 @@ class MSP2Ifc:
"ifc": None,
}
# retrieve optional columns
# If first column = "all" then retrieve all columns
if len(self.optionalColumns) and self.optionalColumns[0] == "all":
self.optionalColumns = [child.tag.split("}")[1] for child in task]
for column in self.optionalColumns:
if not self.tasks[task_id].get(column):
self.tasks[task_id][column] = task.find(f"pr:{column}", self.ns).text if task.find(f"pr:{column}", self.ns) else None
def parse_calendar_xml(self, project):
def parse_working_times(day):
working_times = []
@@ -200,7 +214,8 @@ class MSP2Ifc:
def create_tasks(self, work_schedule):
for task_id in self.tasks:
task = self.tasks[task_id]
if task["OutlineLevel"] == 0:
# Outline Level can be None or 0
if not task["OutlineLevel"]:
self.create_task(task, work_schedule=work_schedule)
def create_work_schedule(self):
@@ -268,6 +283,18 @@ class MSP2Ifc:
for subtask_id in task["subtasks"]:
self.create_task(self.tasks[subtask_id], parent_task=task)
# create pset for optional columns
if len(self.optionalColumns):
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=task["ifc"] , name="Pset_MSP_Task")
ifcopenshell.api.run(
"pset.edit_pset",
self.file,
pset=pset,
properties= {name: str(task[name]) for name in self.optionalColumns if task[name]}
)
def process_working_week(self, week, calendar):
day_map = {
"1": 7, # Sunday
-121
View File
@@ -1,121 +0,0 @@
# IfcClash - IFC-based clash detection.
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcClash.
#
# IfcClash is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcClash 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
import hppfcl
import numpy as np
import ifcopenshell
class Collider:
def __init__(self, logger):
self.logger = logger
self.groups = {}
self.tree = ifcopenshell.geom.tree()
def create_group(self, name):
self.logger.info(f"Creating group {name}")
self.groups[name] = {"elements": {}, "objects": {}}
def create_objects(self, name, ifc_file, iterator, elements):
import time
start = time.time()
self.logger.info(f"Adding objects {name}")
assert iterator.initialize()
while True:
self.tree.add_element(iterator.get_native())
shape = iterator.get()
self.create_object(name, shape.guid, shape)
if not iterator.next():
break
self.logger.info(f"Tree finished {time.time() - start}")
start = time.time()
self.groups[name]["elements"].update({e.GlobalId: e for e in elements})
self.logger.info(f"Element metadata finished {time.time() - start}")
start = time.time()
def create_object(self, group_name, id, shape):
obj = hppfcl.CollisionObject(
self.create_bvh(shape.geometry), self.create_transform(shape.transformation.matrix.data)
)
self.groups[group_name]["objects"][id] = obj
def collide_internal(self, name):
return self.collide_narrowphase(name, name, self.collide_broadphase(name, name))
def collide_group(self, name1, name2):
return self.collide_narrowphase(name1, name2, self.collide_broadphase(name1, name2))
def collide_broadphase(self, name1, name2):
import time
start = time.time()
self.logger.info("Starting broadphase")
potential_collisions = []
checked_collisions = set()
for id, element in self.groups[name1]["elements"].items():
checked_collisions.add(id)
box_filter = self.tree.select_box(element)
pairs = [
{"id1": id, "id2": e.GlobalId}
for e in box_filter
if e.GlobalId not in checked_collisions and e.GlobalId in self.groups[name2]["elements"]
]
potential_collisions.extend(pairs)
self.logger.info(f"Finished broadphase {time.time() - start}")
return potential_collisions
def collide_narrowphase(self, name1, name2, potential_collisions):
import time
start = time.time()
self.logger.info("Starting narrowphase")
collisions = []
for data in potential_collisions:
result = hppfcl.CollisionResult()
hppfcl.collide(
self.groups[name1]["objects"][data["id1"]],
self.groups[name2]["objects"][data["id2"]],
hppfcl.CollisionRequest(),
result,
)
if result.isCollision():
collisions.append({"id1": data["id1"], "id2": data["id2"], "collision": result})
self.logger.info(f"Finished narrowphase {time.time() - start}")
return collisions
def create_transform(self, m):
mat = np.array([[m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]])
mat.transpose()
return hppfcl.Transform3f(mat[:3, :3], mat[:3, 3])
def create_bvh(self, mesh):
v = mesh.verts
f = mesh.faces
mesh_verts = np.array([[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)])
mesh_faces = [(int(f[i]), int(f[i + 1]), int(f[i + 2])) for i in range(0, len(f), 3)]
bvh = hppfcl.BVHModelOBB()
bvh.beginModel(len(mesh_faces), len(mesh_verts))
vertices = hppfcl.StdVec_Vec3f()
[vertices.append(v) for v in mesh_verts]
triangles = hppfcl.StdVec_Triangle()
[triangles.append(hppfcl.Triangle(f[0], f[1], f[2])) for f in mesh_faces]
bvh.addSubModel(vertices, triangles)
bvh.endModel()
return bvh
+74 -45
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# IfcClash - IFC-based clash detection.
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# Copyright (C) 2020-2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcClash.
#
@@ -19,14 +19,13 @@
# along with IfcClash. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
import json
import sys
import time
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.selector
from . import collider
class Clasher:
@@ -34,55 +33,78 @@ class Clasher:
self.settings = settings
self.geom_settings = ifcopenshell.geom.settings()
self.clash_sets = []
self.collider = collider.Collider(self.settings.logger)
self.selector = ifcopenshell.util.selector.Selector()
self.logger = self.settings.logger
self.groups = {}
self.ifcs = {}
self.tree = None
def clash(self):
existing_limit = sys.getrecursionlimit()
for clash_set in self.clash_sets:
self.process_clash_set(clash_set)
def process_clash_set(self, clash_set):
self.collider.create_group("a")
self.tree = ifcopenshell.geom.tree()
self.create_group("a")
for source in clash_set["a"]:
source["ifc"] = self.load_ifc(source["file"])
self.add_collision_objects("a", source["ifc"], source.get("mode", None), source.get("selector", None))
if "b" in clash_set:
self.collider.create_group("b")
if "b" in clash_set and clash_set["b"]:
self.create_group("b")
for source in clash_set["b"]:
source["ifc"] = self.load_ifc(source["file"])
self.add_collision_objects("b", source["ifc"], source.get("mode", None), source.get("selector", None))
results = self.collider.collide_group("a", "b")
b = "b"
else:
results = self.collider.collide_internal("a")
b = "a"
mode = clash_set["mode"]
if mode == "intersection":
results = self.tree.clash_intersection_many(
list(self.groups["a"]["elements"].values()),
list(self.groups[b]["elements"].values()),
tolerance=clash_set["tolerance"],
check_all=clash_set["check_all"],
)
elif mode == "collision":
results = self.tree.clash_collision_many(
list(self.groups["a"]["elements"].values()),
list(self.groups[b]["elements"].values()),
allow_touching=clash_set["allow_touching"],
)
elif mode == "clearance":
results = self.tree.clash_clearance_many(
list(self.groups["a"]["elements"].values()),
list(self.groups[b]["elements"].values()),
clearance=clash_set["clearance"],
check_all=clash_set["check_all"],
)
processed_results = {}
for result in results:
element1 = self.get_element(clash_set["a"], result["id1"])
if "b" in clash_set:
element2 = self.get_element(clash_set["b"], result["id2"])
else:
element2 = self.get_element(clash_set["a"], result["id2"])
element1 = result.a
element2 = result.b
contact = result["collision"].getContacts()[0]
processed_results[f"{result['id1']}-{result['id2']}"] = {
"a_global_id": result["id1"],
"b_global_id": result["id2"],
processed_results[f"{element1.get_argument(0)}-{element2.get_argument(0)}"] = {
"a_global_id": element1.get_argument(0),
"b_global_id": element2.get_argument(0),
"a_ifc_class": element1.is_a(),
"b_ifc_class": element2.is_a(),
"a_name": element1.Name,
"b_name": element2.Name,
"normal": list(contact.normal),
"position": list(contact.pos),
"penetration_depth": contact.penetration_depth,
"a_name": element1.get_argument(2),
"b_name": element2.get_argument(2),
"type": ["protrusion", "pierce", "collision", "clearance"][result.clash_type],
"p1": list(result.p1),
"p2": list(result.p2),
"distance": result.distance,
}
clash_set["clashes"] = processed_results
self.logger.info(f"Found clashes: {len(processed_results.keys())}")
def create_group(self, name):
self.logger.info(f"Creating group {name}")
self.groups[name] = {"elements": {}, "objects": {}}
def load_ifc(self, path):
import time
start = time.time()
self.settings.logger.info(f"Loading IFC {path}")
ifc = self.ifcs.get(path, None)
@@ -93,21 +115,35 @@ class Clasher:
return ifc
def add_collision_objects(self, name, ifc_file, mode=None, selector=None):
import time
start = time.time()
self.settings.logger.info("Creating iterator")
if not mode:
elements = ifc_file.by_type("IfcElement")
if not mode or mode == "a" or not selector:
elements = set(ifc_file.by_type("IfcElement"))
elements -= set(ifc_file.by_type("IfcFeatureElement"))
elif mode == "e":
elements = set(ifc_file.by_type("IfcElement")) - set(self.selector.parse(ifc_file, selector))
elements = set(ifc_file.by_type("IfcElement"))
elements -= set(ifc_file.by_type("IfcFeatureElement"))
elements -= set(ifcopenshell.util.selector.filter_elements(ifc_file, selector))
elif mode == "i":
elements = self.selector.parse(ifc_file, selector)
elements = set(ifcopenshell.util.selector.filter_elements(ifc_file, selector))
iterator = ifcopenshell.geom.iterator(
self.geom_settings, ifc_file, multiprocessing.cpu_count(), include=elements
)
self.settings.logger.info(f"Iterator creation finished {time.time() - start}")
self.collider.create_objects(name, ifc_file, iterator, elements)
start = time.time()
self.logger.info(f"Adding objects {name}")
assert iterator.initialize()
while True:
self.tree.add_element(iterator.get())
shape = iterator.get()
if not iterator.next():
break
self.logger.info(f"Tree finished {time.time() - start}")
start = time.time()
self.groups[name]["elements"].update({e.GlobalId: e for e in elements})
self.logger.info(f"Element metadata finished {time.time() - start}")
start = time.time()
def export(self):
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
@@ -123,7 +159,9 @@ class Clasher:
title = f'{clash["a_ifc_class"]}/{clash["a_name"]} and {clash["b_ifc_class"]}/{clash["b_name"]}'
topic = bcfxml.add_topic(title, title, "IfcClash")
viewpoint = topic.add_viewpoint_from_point_and_guids(
np.array(clash["position"]), clash["a_global_id"], clash["b_global_id"],
np.array(clash["position"]),
clash["a_global_id"],
clash["b_global_id"],
)
snapshot = self.get_viewpoint_snapshot(viewpoint)
if snapshot:
@@ -147,15 +185,6 @@ class Clasher:
with open(self.settings.output, "w", encoding="utf-8") as clashes_file:
json.dump(clash_sets, clashes_file, indent=4)
def get_element(self, clash_set, global_id):
for source in clash_set:
try:
element = source["ifc"].by_guid(global_id)
if element:
return element
except:
pass
def smart_group_clashes(self, clash_sets, max_clustering_distance):
from sklearn.cluster import OPTICS
from collections import defaultdict
+8 -2
View File
@@ -240,13 +240,19 @@ class IfcCsv:
index = attributes.index(data["name"])
formatting_indices[index] = data["format"]
for row in self.results:
for index, format_query in formatting_indices.items():
for index, format_query in formatting_indices.items():
for row in self.results:
if row[index] == null:
continue
row[index] = '"' + str(row[index]).replace('"', '\\"') + '"'
row[index] = ifcopenshell.util.selector.format(format_query.replace("{{value}}", row[index]))
if self.summaries[index] is not None:
summary_label, summary_value = self.summaries[index].split(": ")
summary_value = '"' + str(summary_value).replace('"', '\\"') + '"'
summary_value = ifcopenshell.util.selector.format(format_query.replace("{{value}}", summary_value))
self.summaries[index] = summary_label + ": " + str(summary_value)
def sort_results(self, sort, attributes, include_global_id):
if not self.results:
return
+7 -4
View File
@@ -54,7 +54,7 @@ class IfcDiff:
that comparisons will take longer.
:type is_shallow: bool
:param filter_elements: An IFC filter query if you only want to compare a
subset of elements. For example: ``.IfcWall`` to only compare walls.
subset of elements. For example: ``IfcWall`` to only compare walls.
:type filter_elements: string
Example::
@@ -83,9 +83,12 @@ class IfcDiff:
self.precision = self.get_precision()
if self.filter_elements:
selector = ifcopenshell.util.selector.Selector()
old_elements = set(e.GlobalId for e in selector.parse(self.old, self.filter_elements))
new_elements = set(e.GlobalId for e in selector.parse(self.new, self.filter_elements))
old_elements = set(
e.GlobalId for e in ifcopenshell.util.selector.filter_elements(self.old, self.filter_elements)
)
new_elements = set(
e.GlobalId for e in ifcopenshell.util.selector.filter_elements(self.new, self.filter_elements)
)
else:
old_elements = self.old.by_type("IfcElement")
if self.old.schema == "IFC2X3":
+13 -1
View File
@@ -28,7 +28,19 @@
bool IfcGeom::Kernel::convert(const IfcSchema::IfcDerivedProfileDef* l, TopoDS_Shape& face) {
TopoDS_Face f;
gp_Trsf2d trsf2d;
if (convert_face(l->ParentProfile(), f) && IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
bool is_mirror = false;
#ifdef SCHEMA_HAS_IfcMirroredProfileDef
if (l->as<IfcSchema::IfcMirroredProfileDef>()) {
trsf2d.SetMirror(gp::Origin2d());
is_mirror = true;
}
#endif
if (!is_mirror) {
if (!IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
return false;
}
}
if (convert_face(l->ParentProfile(), f)) {
gp_Trsf trsf = trsf2d;
face = BRepBuilderAPI_Transform(f, trsf).Shape();
return true;
+3 -1
View File
@@ -859,7 +859,9 @@ IfcSchema::IfcRepresentation* IfcGeom::Kernel::representation_mapped_to(const If
}
}
} catch (const IfcParse::IfcException& e) {
Logger::Error(e);
// @todo the remove_boundingboxes() in IfcConvert leave dangling IfcRepresentationMap
// causing this error to be logged.
Logger::Error(e, representation);
// @todo reset representation_mapped_to to zero?
}
return representation_mapped_to;
@@ -371,6 +371,11 @@ IfcGeom::Representation::Triangulation::Triangulation(const BRep& shape_model)
triangles(i).Get(n3, n2, n1);
else triangles(i).Get(n1, n2, n3);
if (dict[n1] == dict[n2] || dict[n2] == dict[n3] || dict[n3] == dict[n1]) {
Logger::Warning("Mesher generated a degenerate triangle, ignoring");
continue;
}
/* An alternative would be to calculate normals based
* on the coordinates of the mesh vertices */
/*
File diff suppressed because it is too large Load Diff
@@ -39,6 +39,8 @@
#include <ShapeFix_Shape.hxx>
#include <BRepCheck_Analyzer.hxx>
#include <BRepClass3d_SolidClassifier.hxx>
#include <BRepCheck.hxx>
#include <BRepTools.hxx>
#include <TopTools_IndexedMapOfShape.hxx>
@@ -739,3 +741,43 @@ bool IfcGeom::util::create_solid_from_faces(const TopTools_ListOfShape& face_lis
return valid_shell;
}
bool IfcGeom::util::validate_shape(const TopoDS_Shape& s) {
BRepCheck_Analyzer ana(s);
if (ana.IsValid()) {
return true;
}
std::stringstream str;
bool any_emitted = false;
std::function<void(const TopoDS_Shape&)> dump;
dump = [&ana, &str, &dump, &any_emitted](const TopoDS_Shape& s) {
if (!ana.Result(s).IsNull()) {
BRepCheck_ListIteratorOfListOfStatus itl;
itl.Initialize(ana.Result(s)->Status());
for (; itl.More(); itl.Next()) {
if (itl.Value() != BRepCheck_NoError) {
if (any_emitted) {
str << ", ";
}
BRepCheck::Print(itl.Value(), str);
str.seekp(str.tellp() - (std::streamoff)1);
str << " on ";
TopAbs::Print(s.ShapeType(), str);
BRepTools::Dump(s, str);
any_emitted = true;
}
}
}
for (TopoDS_Iterator it(s); it.More(); it.Next()) {
dump(it.Value());
}
};
dump(s);
Logger::Warning(str.str());
return false;
}
+2
View File
@@ -70,6 +70,8 @@ namespace IfcGeom {
TopoDS_Shape apply_transformation(const TopoDS_Shape&, const gp_Trsf&);
TopoDS_Shape apply_transformation(const TopoDS_Shape&, const gp_GTrsf&);
bool validate_shape(const TopoDS_Shape&);
}
}
+588
View File
@@ -0,0 +1,588 @@
#include "clash_utils.h"
#include <cassert>
#define GU_CULLING_EPSILON_RAY_TRIANGLE FLT_EPSILON*FLT_EPSILON
#define PX_MAX_F32 3.4028234663852885981170418348452e+38F
typedef uint32_t PxU32;
// Why can't I use std::clamp?
template<typename TC>
const TC& ios_clamp(const TC& v, const TC& lo, const TC& hi) {
assert(!(hi < lo));
return (v < lo) ? lo : (hi < v) ? hi : v;
}
// Branchless slab method. Note that this can still be optimised further by batching boxes.
// From Tavian Barnes - MIT License
// https://tavianator.com/2022/ray_box_boundary.html
bool is_intersect_ray_box(const struct ray *ray, const struct box *box) {
float tmin = 0.0, tmax = INFINITY;
for (int d = 0; d < 3; ++d) {
bool sign = std::signbit(ray->dir_inv[d]);
float bmin = box->corners[sign][d];
float bmax = box->corners[!sign][d];
float dmin = (bmin - ray->origin[d]) * ray->dir_inv[d];
float dmax = (bmax - ray->origin[d]) * ray->dir_inv[d];
tmin = std::max(dmin, tmin);
tmax = std::min(dmax, tmax);
}
return tmin < tmax;
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionRayTriangle.h
// With minor modifications to use gp_Vec type.
// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
Standard_Real& at, Standard_Real& au, Standard_Real& av,
bool cull, float enlarge) {
// Find vectors for two edges sharing vert0
const gp_Vec edge1 = vert1 - vert0;
const gp_Vec edge2 = vert2 - vert0;
// Begin calculating determinant - also used to calculate U parameter
const gp_Vec pvec = dir.Crossed(edge2); // error ~ |v2-v0|
// If determinant is near zero, ray lies in plane of triangle
const Standard_Real det = edge1.Dot(pvec); // error ~ |v2-v0|*|v1-v0|
if(cull)
{
if(det<GU_CULLING_EPSILON_RAY_TRIANGLE)
return false;
// Calculate distance from vert0 to ray origin
const gp_Vec tvec = orig - vert0;
// Calculate U parameter and test bounds
const Standard_Real u = tvec.Dot(pvec);
const Standard_Real enlargeCoeff = enlarge*det;
const Standard_Real uvlimit = -enlargeCoeff;
const Standard_Real uvlimit2 = det + enlargeCoeff;
if(u<uvlimit || u>uvlimit2)
return false;
// Prepare to test V parameter
const gp_Vec qvec = tvec.Crossed(edge1);
// Calculate V parameter and test bounds
const Standard_Real v = dir.Dot(qvec);
if(v<uvlimit || (u+v)>uvlimit2)
return false;
// Calculate t, scale parameters, ray intersects triangle
const Standard_Real t = edge2.Dot(qvec);
const Standard_Real inv_det = 1.0f / det;
at = t*inv_det;
au = u*inv_det;
av = v*inv_det;
}
else
{
// the non-culling branch
if(std::abs(det)<GU_CULLING_EPSILON_RAY_TRIANGLE)
return false;
const Standard_Real inv_det = 1.0f / det;
// Calculate distance from vert0 to ray origin
const gp_Vec tvec = orig - vert0; // error ~ |orig-v0|
// Calculate U parameter and test bounds
const Standard_Real u = tvec.Dot(pvec) * inv_det;
if(u<-enlarge || u>1.0f+enlarge)
return false;
// prepare to test V parameter
const gp_Vec qvec = tvec.Crossed(edge1);
// Calculate V parameter and test bounds
const Standard_Real v = dir.Dot(qvec) * inv_det;
if(v<-enlarge || (u+v)>1.0f+enlarge)
return false;
// Calculate t, ray intersects triangle
const Standard_Real t = edge2.Dot(qvec) * inv_det;
at = t;
au = u;
av = v;
}
return true;
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/sweep/GuSweepCapsuleCapsule.cpp
// With minor modifications to use gp_Vec type.
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
const gp_Vec& q, const gp_Vec& b) // seg 2 origin, vector
{
const gp_Vec Tx = q - p;
const double ADotA = a.Dot(a);
const double BDotB = b.Dot(b);
const double ADotB = a.Dot(b);
const double ADotT = a.Dot(Tx);
const double BDotT = b.Dot(Tx);
// t parameterizes ray (p, a)
// u parameterizes ray (q, b)
// Compute t for the closest point on ray (p, a) to ray (q, b)
const Standard_Real Denom = ADotA*BDotB - ADotB*ADotB;
Standard_Real t; // We will clamp result so t is on the segment (p, a)
if(Denom!=0.0f)
t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0);
else
t = 0.0f;
// find u for point on ray (q, b) closest to point at t
Standard_Real u;
if(BDotB!=0.0f)
{
u = (t*ADotB - BDotT) / BDotB;
// if u is on segment (q, b), t and u correspond to closest points, otherwise, clamp u, recompute and clamp t
if(u<0.0f)
{
u = 0.0f;
if(ADotA!=0.0f)
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
else
t = 0.0f;
}
else if(u > 1.0f)
{
u = 1.0f;
if(ADotA!=0.0f)
t = ios_clamp((ADotB + ADotT) / ADotA, 0.0, 1.0);
else
t = 0.0f;
}
}
else
{
u = 0.0f;
if(ADotA!=0.0f)
t = ios_clamp(ADotT / ADotA, 0.0, 1.0);
else
t = 0.0f;
}
x = p + a * t;
y = q + b * u;
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/distance/GuDistanceTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q)
{
std::array<gp_Vec, 3> Sv;
Sv[0] = p[1] - p[0];
Sv[1] = p[2] - p[1];
Sv[2] = p[0] - p[2];
std::array<gp_Vec, 3> Tv;
Tv[0] = q[1] - q[0];
Tv[1] = q[2] - q[1];
Tv[2] = q[0] - q[2];
gp_Vec minP, minQ;
bool shown_disjoint = false;
float mindd = PX_MAX_F32;
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
edgeEdgeDist(cp, cq, p[i], Sv[i], q[j], Tv[j]);
const gp_Vec V = cq - cp;
const float dd = V.Dot(V);
if(dd<=mindd)
{
minP = cp;
minQ = cq;
mindd = dd;
int id = i+2;
if(id>=3)
id-=3;
gp_Vec Z = p[id] - cp;
float a = Z.Dot(V);
id = j+2;
if(id>=3)
id-=3;
Z = q[id] - cq;
float b = Z.Dot(V);
if((a<=0.0f) && (b>=0.0f))
return V.Dot(V);
if(a<=0.0f) a = 0.0f;
else if(b>0.0f) b = 0.0f;
if((mindd - a + b) > 0.0f)
shown_disjoint = true;
}
}
}
gp_Vec Sn = Sv[0].Crossed(Sv[1]);
float Snl = Sn.Dot(Sn);
if(Snl>1e-15f)
{
const std::array<double, 3> Tp = {(p[0] - q[0]).Dot(Sn),
(p[0] - q[1]).Dot(Sn),
(p[0] - q[2]).Dot(Sn)};
int index = -1;
if((Tp[0]>0.0f) && (Tp[1]>0.0f) && (Tp[2]>0.0f))
{
if(Tp[0]<Tp[1]) index = 0; else index = 1;
if(Tp[2]<Tp[index]) index = 2;
}
else if((Tp[0]<0.0f) && (Tp[1]<0.0f) && (Tp[2]<0.0f))
{
if(Tp[0]>Tp[1]) index = 0; else index = 1;
if(Tp[2]>Tp[index]) index = 2;
}
if(index >= 0)
{
shown_disjoint = true;
const gp_Vec& qIndex = q[index];
gp_Vec V = qIndex - p[0];
gp_Vec Z = Sn.Crossed(Sv[0]);
if(V.Dot(Z)>0.0f)
{
V = qIndex - p[1];
Z = Sn.Crossed(Sv[1]);
if(V.Dot(Z)>0.0f)
{
V = qIndex - p[2];
Z = Sn.Crossed(Sv[2]);
if(V.Dot(Z)>0.0f)
{
cp = qIndex + Sn * Tp[index]/Snl;
cq = qIndex;
return (cp - cq).SquareMagnitude();
}
}
}
}
}
gp_Vec Tn = Tv[0].Crossed(Tv[1]);
float Tnl = Tn.Dot(Tn);
if(Tnl>1e-15f)
{
const std::array<double, 3> Sp = {(q[0] - p[0]).Dot(Tn),
(q[0] - p[1]).Dot(Tn),
(q[0] - p[2]).Dot(Tn)};
int index = -1;
if((Sp[0]>0.0f) && (Sp[1]>0.0f) && (Sp[2]>0.0f))
{
if(Sp[0]<Sp[1]) index = 0; else index = 1;
if(Sp[2]<Sp[index]) index = 2;
}
else if((Sp[0]<0.0f) && (Sp[1]<0.0f) && (Sp[2]<0.0f))
{
if(Sp[0]>Sp[1]) index = 0; else index = 1;
if(Sp[2]>Sp[index]) index = 2;
}
if(index >= 0)
{
shown_disjoint = true;
const gp_Vec& pIndex = p[index];
gp_Vec V = pIndex - q[0];
gp_Vec Z = Tn.Crossed(Tv[0]);
if(V.Dot(Z)>0.0f)
{
V = pIndex - q[1];
Z = Tn.Crossed(Tv[1]);
if(V.Dot(Z)>0.0f)
{
V = pIndex - q[2];
Z = Tn.Crossed(Tv[2]);
if(V.Dot(Z)>0.0f)
{
cp = pIndex;
cq = pIndex + Tn * Sp[index]/Tnl;
return (cp - cq).SquareMagnitude();
}
}
}
}
}
if(shown_disjoint)
{
cp = minP;
cq = minQ;
return mindd;
}
else return 0.0f;
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
//Based on the paper A Fast Triangle-Triangle Intersection Test by T. Moeller
//http://web.stanford.edu/class/cs277/resources/papers/Moller1997b.pdf
namespace {
struct Interval
{
Standard_Real min;
Standard_Real max;
gp_Vec minPoint;
gp_Vec maxPoint;
Interval() : min(FLT_MAX), max(-FLT_MAX), minPoint(gp_Vec(NAN, NAN, NAN)), maxPoint(gp_Vec(NAN, NAN, NAN)) { }
static bool overlapOrTouch(const Interval& a, const Interval& b)
{
return !(a.min > b.max || b.min > a.max);
}
static Interval intersection(const Interval& a, const Interval& b)
{
Interval result;
if (!overlapOrTouch(a, b))
return result;
if (a.min > b.min) {
result.min = a.min;
result.minPoint = a.minPoint;
} else {
result.min = b.min;
result.minPoint = b.minPoint;
}
if (a.max < b.max) {
result.max = a.max;
result.maxPoint = a.maxPoint;
} else {
result.max = b.max;
result.maxPoint = b.maxPoint;
}
return result;
}
void include(Standard_Real d, const gp_Vec& p)
{
if (d < min) { min = d; minPoint = p; }
if (d > max) { max = d; maxPoint = p; }
}
};
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
static Interval computeInterval(Standard_Real distanceA, Standard_Real distanceB, Standard_Real distanceC, const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& dir)
{
Interval i;
const bool bA = distanceA > 0;
const bool bB = distanceB > 0;
const bool bC = distanceC > 0;
distanceA = std::abs(distanceA);
distanceB = std::abs(distanceB);
distanceC = std::abs(distanceC);
if (bA != bB)
{
const gp_Vec p = (distanceA / (distanceA + distanceB)) * b + (distanceB / (distanceA + distanceB)) * a;
i.include(dir.Dot(p), p);
}
if (bA != bC)
{
const gp_Vec p = (distanceA / (distanceA + distanceC)) * c + (distanceC / (distanceA + distanceC)) * a;
i.include(dir.Dot(p), p);
}
if (bB != bC)
{
const gp_Vec p = (distanceB / (distanceB + distanceC)) * c + (distanceC / (distanceB + distanceC)) * b;
i.include(dir.Dot(p), p);
}
return i;
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
Standard_Real orient2d(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, PxU32 x, PxU32 y)
{
return (a.Coord(y) - c.Coord(y)) * (b.Coord(x) - c.Coord(x)) - (a.Coord(x) - c.Coord(x)) * (b.Coord(y) - c.Coord(y));
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
Standard_Real pointInTriangle(const gp_Vec& a, const gp_Vec& b, const gp_Vec& c, const gp_Vec& point, PxU32 x, PxU32 y)
{
const Standard_Real ab = orient2d(a, b, point, x, y);
const Standard_Real bc = orient2d(b, c, point, x, y);
const Standard_Real ca = orient2d(c, a, point, x, y);
if ((ab >= 0) == (bc >= 0) && (ab >= 0) == (ca >= 0))
return true;
return false;
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
Standard_Real linesIntersect(const gp_Vec& startA, const gp_Vec& endA, const gp_Vec& startB, const gp_Vec& endB, PxU32 x, PxU32 y)
{
const Standard_Real aaS = orient2d(startA, endA, startB, x, y);
const Standard_Real aaE = orient2d(startA, endA, endB, x, y);
if ((aaS >= 0) == (aaE >= 0))
return false;
const Standard_Real bbS = orient2d(startB, endB, startA, x, y);
const Standard_Real bbE = orient2d(startB, endB, endA, x, y);
if ((bbS >= 0) == (bbE >= 0))
return false;
return true;
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
void getProjectionIndices(gp_Vec normal, PxU32& x, PxU32& y)
{
normal.SetCoord(std::abs(normal.X()), std::abs(normal.Y()), std::abs(normal.Z()));
if (normal.X() >= normal.Y() && normal.X() >= normal.Z())
{
//x is the dominant normal direction
x = 1;
y = 2;
}
else if (normal.Y() >= normal.X() && normal.Y() >= normal.Z())
{
//y is the dominant normal direction
x = 2;
y = 0;
}
else
{
//z is the dominant normal direction
x = 0;
y = 1;
}
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2)
{
PxU32 x = 0;
PxU32 y = 0;
getProjectionIndices(p1_n, x, y);
const Standard_Real third = (1.0f / 3.0f);
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
//difference is not very big and the coplanar case is not expected to be the most common case
if (linesIntersect(a1, b1, a2, b2, x, y) || linesIntersect(a1, b1, b2, c2, x, y) || linesIntersect(a1, b1, c2, a2, x, y) ||
linesIntersect(b1, c1, a2, b2, x, y) || linesIntersect(b1, c1, b2, c2, x, y) || linesIntersect(b1, c1, c2, a2, x, y) ||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
pointInTriangle(a1, b1, c1, third * (a2 + b2 + c2), x, y) || pointInTriangle(a2, b2, c2, third * (a1 + b1 + c1), x, y))
return true;
return false;
}
// From NVIDIA-Omniverse PhysX - BSD 3-Clause "New" or "Revised" License
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/LICENSE.md
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionTriangleTriangle.cpp
// With minor modifications to use gp_Vec type.
// Also with minor modification to return intersection points.
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar)
{
const Standard_Real tolerance = 1e-8f;
gp_Vec p1_n((b1 - a1).Crossed(c1 - a1).Normalized());
double p1_d = -a1.Dot(p1_n);
// const PxPlane p1(a1, b1, c1);
const Standard_Real p1ToA = a2.Dot(p1_n) + p1_d;
const Standard_Real p1ToB = b2.Dot(p1_n) + p1_d;
const Standard_Real p1ToC = c2.Dot(p1_n) + p1_d;
if(std::abs(p1ToA) < tolerance && std::abs(p1ToB) < tolerance &&std::abs(p1ToC) < tolerance)
return ignoreCoplanar ? false : trianglesIntersectCoplanar(p1_n, a1, b1, c1, a2, b2, c2); //Coplanar triangles
if ((p1ToA > 0) == (p1ToB > 0) && (p1ToA > 0) == (p1ToC > 0))
return false; //All points of triangle 2 on same side of triangle 1 -> no intersection
gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized());
double p2_d = -a2.Dot(p2_n);
// const PxPlane p2(a2, b2, c2);
const Standard_Real p2ToA = a1.Dot(p2_n) + p2_d;
const Standard_Real p2ToB = b1.Dot(p2_n) + p2_d;
const Standard_Real p2ToC = c1.Dot(p2_n) + p2_d;
if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0))
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
gp_Vec intersectionDirection = p1_n.Crossed(p2_n);
const Standard_Real l2 = intersectionDirection.SquareMagnitude();
intersectionDirection *= 1.0f / std::sqrt(l2);
const Interval i1 = computeInterval(p2ToA, p2ToB, p2ToC, a1, b1, c1, intersectionDirection);
const Interval i2 = computeInterval(p1ToA, p1ToB, p1ToC, a2, b2, c2, intersectionDirection);
if (Interval::overlapOrTouch(i1, i2))
{
/*if (intersection)
{
const Interval i = Interval::intersection(i1, i2);
intersection->p0 = i.minPoint;
intersection->p1 = i.maxPoint;
}*/
const Interval i = Interval::intersection(i1, i2);
int1 = i.minPoint;
int2 = i.maxPoint;
return true;
}
return false;
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <gp_Vec.hxx>
#include <array>
struct ray {
float origin[3];
float dir[3];
float dir_inv[3];
};
struct box {
float corners[2][3];
};
bool is_intersect_ray_box(const struct ray *ray, const struct box *box);
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
Standard_Real& at, Standard_Real& au, Standard_Real& av,
bool cull, float enlarge=0.0f);
void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
const gp_Vec& p, const gp_Vec& a, // seg 1 origin, vector
const gp_Vec& q, const gp_Vec& b); // seg 2 origin, vector
float distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<gp_Vec, 3> p, const std::array<gp_Vec, 3> q);
bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, const gp_Vec& a2, const gp_Vec& b2, const gp_Vec& c2/*, Segment* intersection*/, gp_Vec& int1, gp_Vec& int2, bool ignoreCoplanar);
+1 -1
View File
@@ -109,7 +109,7 @@ endif
mkdir -p dist/ifcopenshell
cp -r ifcopenshell/* dist/ifcopenshell/
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-e38eafd-$(PLATFORM)64.zip
cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-f7c03db-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell/ifcopenshell_wrapper.py dist/ifcopenshell/
ifeq ($(PLATFORM), win)
+37 -5
View File
@@ -1,12 +1,44 @@
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700&display=swap');
h1, h2, h3, h4 {
font-weight: normal;
color: #7cbf33;
background: -webkit-linear-gradient(0deg, #7cbf33, #049344);
background-clip: border-box;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
a {
text-decoration: none;
}
.toc-tree .reference:hover {
color: #d98014;
}
.sidebar-brand-text {
font-size: 1rem;
}
.blockbutton {
border: 1px solid var(--color-admonition-title--seealso);
background-color: var(--color-admonition-title-background--seealso);
padding: 5px;
text-align: center;
border-radius: 5px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
text-align: center;
}
.blockbutton a {
display: block;
padding-top: 15px;
padding-bottom: 15px;
border-radius: 5px;
background: #40b74c;
width: 100%;
height: 100%;
color: #fff;
text-decoration: none;
}
.blockbutton a:hover {
background: #70ba35;
}
.highlight .hll {
background-color: #ffc2;
}
section img {
display: block;
+64 -11
View File
@@ -1,20 +1,20 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2020-2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
# This file is part of IfcOpenShell.
#
# 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
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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,
# IfcOpenShell 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.
# GNU Lesser 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 should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
# Configuration file for the Sphinx documentation builder.
#
@@ -36,7 +36,7 @@ sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = "IfcOpenShell"
copyright = "2011-2023, IfcOpenShell Contributors"
copyright = "2011-2024, IfcOpenShell Contributors"
author = "IfcOpenShell Contributors"
# The full version, including alpha/beta/rc tags
@@ -54,7 +54,7 @@ release = "0.7.0"
# - No subnav making it really hard to navigate
# - Kinda hacky setup https://stackoverflow.com/questions/2701998/sphinx-autodoc-is-not-automatic-enough
# - I couldn't customise the template to show submodules above members which makes API discovery hard for users
extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel"]
extensions = ["autoapi.extension", "sphinx.ext.autosectionlabel", "sphinx_copybutton"]
# Auto add document prefixes to help guarantee uniqueness of automatic section references.
autosectionlabel_prefix_document = True
@@ -67,6 +67,8 @@ autoapi_type = 'python'
# autoapi works by reading source code instead of importing modules
autoapi_dirs = ['../ifcopenshell', '../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
# autoapi_dirs = ['../../ifcdiff']
# autoapi_dirs = ['../../ifcdiff', '../ifcopenshell/util']
# autoapi_dirs = ['../../bcf/src', '../../bsdd', '../../ifccsv', '../../ifcdiff', '../../ifcpatch/ifcpatch', '../../ifctester/ifctester']
# These are auto-generated based on the IFC schema, so exclude them
@@ -113,3 +115,54 @@ html_css_files = ["custom.css"]
# page even on light themes.
pygments_style = "one-dark"
pygments_dark_style = "one-dark"
html_logo = "https://ifcopenshell.org/assets/images/logo.png"
html_theme_options = {
"source_repository": "https://github.com/IfcOpenShell/IfcOpenShell/",
"source_branch": "v0.7.0",
"source_directory": "src/ifcopenshell-python/docs/",
"light_css_variables": {
"color-brand-primary": "#39b54a",
"color-brand-content": "#39b54a",
"color-brand-visited": "#d9e021",
"color-background-primary": "#f7f7f6",
"color-background-secondary": "#eeeeec",
"color-background-border": "#cfd0cb",
"color-foreground-primary": "#2e3436",
"color-sidebar-item-background--hover": "#f7f7f6",
"color-link--hover": "#d98014",
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
},
"dark_css_variables": {
"color-brand-primary": "#39b54a",
"color-brand-content": "#39b54a",
"color-brand-visited": "#d9e021",
"color-background-primary": "#2e3436",
"color-background-border": "#2e3436",
"color-foreground-primary": "#eeeeec",
"color-sidebar-item-background--hover": "#2e3436",
"color-link--hover": "#d98014",
"font-stack": "Nunito, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji"
},
"footer_icons": [
{
"name": "IfcOpenShell",
"url": "https://ifcopenshell.org",
"html": """
<img src="https://ifcopenshell.org/assets/images/logo.png" style="width: auto;" />
""",
"class": "",
},
{
"name": "GitHub",
"url": "https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcopenshell-python/docs",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
""",
"class": "",
},
],
}
@@ -20,11 +20,11 @@ Pre-built packages
| build-linux64_ | build-win32_ | build-win64_ | build-macos64_ | build-macosm164_ |
+----------------+----------------+----------------+----------------+------------------+
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-linux64.zip
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win32.zip
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-win64.zip
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macos64.zip
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-e38eafd-macosm164.zip
.. _build-linux64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-linux64.zip
.. _build-win32: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-win32.zip
.. _build-win64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-win64.zip
.. _build-macos64: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-macos64.zip
.. _build-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.0-f7c03db-macosm164.zip
2. Unzip the downloaded file and run IfcConvert using the command line.
@@ -1,14 +1,16 @@
Geometry tree
=============
IfcOpenShell includes a utility to build a unbalanced binary tree of geometry
and their bounding boxes. After a tree is built, you can efficiently select
geometry by specifying a point, radius, or bounding box.
IfcOpenShell includes a utility to build trees of geometry and their bounding
boxes. Geometry trees can be used to efficiently select geometry or collide
geometry with one another.
.. image:: images/geometry-tree.png
The most efficient way to build tree is by using the iterator, as shown in the
example below:
The most efficient way to build a tree is by using the iterator. If the native
OpenCASCADE shape is added to the tree, a **UB Tree** is built. Alternatively,
if triangulation is added to the tree, a **BVH Tree** is built. The type of
tree determines the type of operation you can perform.
.. code-block:: python
@@ -21,27 +23,208 @@ example below:
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count())
if iterator.initialize():
while True:
tree.add_element(iterator.get_native())
# Use triangulation to build a BVH tree
tree.add_element(iterator.get())
# Alternatively, use this code to build an unbalanced binary tree
# tree.add_element(iterator.get_native())
if not iterator.next():
break
Once built, there are three methods you can use to select elements in the tree:
``select_box``, ``select``, and ``select_ray``.
Clashing or selecting geometry from a geometry tree
---------------------------------------------------
``select_box`` lets you query for elements that contain a point or another
element. However, it only checks the bounding box of elements instead of their
exact geometry. This is the fastest approach and is recommended if you don't
need precise geometry selection.
With a **BVH Tree**, you can efficiently clash sets of elements with other
elements. You can find elements that intersect, collide, or are within a
clearance distance threshold of one another. There are three methods you can
use to clash elements in the tree. Each function collides one set of elements
with another set of elements.
``select`` lets you query for elements that contain a point, a sphere, or
another element. ``select`` is similar to select box, but additionally
considers the actual geometry of the object. This is slower but more precise.
- `Detecting intersection clashes between elements`_ detects when an element intersects with another
element. This is the most common type of clash detection used when
coordinating designs. For example, you might want to know if any pipes go
through structural columns or beams.
- `Detecting collision clashes between elements`_ detects when an element
touches another element. It is the fastest type of clash detection but does
not consider the distance that an element goes inside another element. This
considers surfaces only so it works on non-manifold geometry but will not
detect if an element is completely within another element.
- `Detecting clearance clashes between elements`_ detects when an element comes
near to another element within a clearance threshold. This is the slowest
type of clash detection. It works on non-manifold geometry and does not
consider inside vs outside. Elements like pipe and ducts with insulation,
structural openings, and equipment will typically require clearance checks.
``select_ray`` lets you query for elements that intersect with a ray.
With a **UB Tree**, you can efficiently select geometry by specifying a point,
radius, or bounding box. There are three methods you can use to select elements
in the tree.
- `Selecting elements using bounding boxes`_ lets you query for elements that
contain a point or another element. However, it only checks the bounding box
of elements instead of their exact geometry. This is the fastest approach and
is recommended if you don't need precise geometry selection.
- `Selecting elements using precise geometry`_ lets you query for elements that
contain a point, a sphere, or another element. This is similar to selecting
using bounding boxes, but additionally considers the actual geometry of the
element. This is slower but more precise.
- `Selecting elements using a ray`_ lets you query for elements that intersect
with a ray.
Detecting intersection clashes between elements
-----------------------------------------------
``clash_intersection_many`` detects when an element intersects with or is
contained within another element.
.. code-block:: python
clashes = tree.clash_intersection_many(
group_a_elements, # e.g. from model.by_type("IfcWall")
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
tolerance=0.002, # Any protrusions less than 2mm are ignored
check_all=True, # Keep on checking all potential intersections to find a worst case protrusion distance
)
for clash in clashes:
# Get the two elements that clash and their metadata
element1 = clash.a
element2 = clash.b
a_global_id = element1.get_argument(0)
b_global_id = element2.get_argument(0)
a_ifc_class = element1.is_a()
b_ifc_class = element2.is_a()
a_name = element1.get_argument(2)
b_name = element2.get_argument(2)
# Potential clash types that can be detected are protrusions, pierces, and collisions
clash_type = ["protrusion", "pierce", "collision", "clearance"][clash.clash_type],
# P1 and P2 represents two XYZ coordinates. The meaning of the coordinate depends on the clash type.
p1 = list(clash.p1)
p2 = list(clash.p2)
# This represents the protrusion or piercing distance in meters.
# It is also the distance between P1 and P2.
distance = clash.distance
If you specify a ``tolerance`` value, intersections with a protrusion distance
smaller than this tolerance are excluded. It is recommended to specify a
non-zero tolerance to distinguish between when elements merely touch (e.g. a
GPO on a wall) versus if they are truly intersecting (e.g. a pipe going through
a beam).
If ``check_all`` is ``False``, the clash check will return as soon as an
intersection is found. This is faster but may not return the worst-case
protrusion distance. If you are not interested in the protrusion distance, it
is recommended to set this to ``False``. If you want the protrusion distance,
such as to prioritise which clashes are more severe, set this to ``True``.
This includes:
1. When an element X protrudes inside element Y, where element Y is manifold.
In this case, a protrusion distance is calculated as the deepest point of
element X to the closest surface of element Y. ``P1`` is defined as the XYZ
coordinate on element X, and ``P2`` is defined as the nearest point on the
surface of element Y.
2. When an element X pierces element Y, such that an edge of element X enters
element Y and leaves through another face. In this case, a piercing distance
is calculated as the distance where that edge is inside element Y. ``P1`` is
defined as the point on an edge of element X which enters element Y, and
``P2`` is the point where that edge leaves element Y.
3. When neither X or Y is manifold, we cannot detect protrusion or piercing, so
instead when X and Y have any touching face. This is the same as the
``clash_collision_many`` check below. The distance is considered to be zero
and ignores your specified tolerance. ``P1`` and ``P2`` are equal and
represent an arbitrary XYZ point where the two elements touch.
Detecting collision clashes between elements
--------------------------------------------
``clash_collision_many`` detects when the surface of an element collides with
another element. The surfaces may either merely touch (e.g. are coplanar) or
intersect.
.. code-block:: python
clashes = tree.clash_collision_many(
group_a_elements, # e.g. from model.by_type("IfcWall")
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
allow_touching=True, # Include results where faces merely touch but do not intersect
)
for clash in clashes:
# Get the two elements that clash and their metadata
element1 = clash.a
element2 = clash.b
a_global_id = element1.get_argument(0)
b_global_id = element2.get_argument(0)
a_ifc_class = element1.is_a()
b_ifc_class = element2.is_a()
a_name = element1.get_argument(2)
b_name = element2.get_argument(2)
# P1 and P2 represents two possible arbitrary points where a collision is found.
# P1 may or may not be equal to P2.
p1 = list(clash.p1)
p2 = list(clash.p2)
A collision between two surface triangles may be "touching" or "intersecting".
Two touching triangles may be coplanar or merely have a single edge or vertex
touching the other triangle. An intersecting triangle will have at least one
edge that goes through the other triangle.
Detecting clearance clashes between elements
--------------------------------------------
``clash_clearance_many`` detects with the surface of an element comes within a
clearance distance threshold of another element.
.. code-block:: python
clashes = tree.clash_clearance_many(
group_a_elements, # e.g. from model.by_type("IfcWall")
group_b_elements, # Group b can be the same as group a if you want to clash within a single set
clearance=0.1, # Any surface closer than than 100mm is a clash
check_all=False, # Stop measuring distances once the first clearance violation is found per element.
)
for clash in clashes:
# Get the two elements that clash and their metadata
element1 = clash.a
element2 = clash.b
a_global_id = element1.get_argument(0)
b_global_id = element2.get_argument(0)
a_ifc_class = element1.is_a()
b_ifc_class = element2.is_a()
a_name = element1.get_argument(2)
b_name = element2.get_argument(2)
# P1 and P2 represents the two XYZ coordinates between element1 and element2.
p1 = list(clash.p1)
p2 = list(clash.p2)
# This represents the distance between element1 and element2 that is less than the clearance.
# It is the distance between P1 and P2. It cannot be less than 0.
distance = clash.distance
You cannot specify a ``clearance`` less than 0.
If ``check_all`` is ``False``, the clash check will return as soon as a
clearance violation is found. This is faster but may not return the worst-case
distance. If you only interested whether there is a clearance issue, it is
recommended to set this to ``False``. If you want the exact worst case
clearance distance, such as to prioritise which clashes are more severe, set
this to ``True``.
Selecting elements using bounding boxes
---------------------------------------
Elements may be queried using an axis aligned bounding box. An axis aligned
bounding box is the bounding box using global XYZ axes, not the element's local
XYZ axes. If you have a vertical construction project, this means that your
model should be oriented to project north to get the best results.
You may select all elements that have a bounding box containing the point with
XYZ coordinates of ``(0., 0., 0.)``.
@@ -128,11 +311,11 @@ geometry. It will return:
.. code-block:: python
elements = tree.select_box(wall, completely_within=True)
elements = tree.select(wall, completely_within=True)
# Alternatively, you may also specify an extension to dilate the geometry
# of the wall.
elements = tree.select_box(wall, completely_within=True, extend=5.)
elements = tree.select(wall, completely_within=True, extend=5.)
Selecting elements using a ray
------------------------------
@@ -30,10 +30,6 @@ the API.
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| | Linux 64bit | Windows 32bit | Windows 64bit | MacOS Intel 64bit | MacOS Silicon 64bit |
+=============+================+================+================+===================+=====================+
| Python 3.7 | py37-linux64_ | py37-win32_ | py37-win64_ | py37-macos64_ | py37-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| Python 3.8 | py38-linux64_ | py38-win32_ | py38-win64_ | py38-macos64_ | py38-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| Python 3.9 | py39-linux64_ | py39-win32_ | py39-win64_ | py39-macos64_ | py39-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
| Python 3.10 | py310-linux64_ | py310-win32_ | py310-win64_ | py310-macos64_ | py310-macosm164_ |
@@ -43,36 +39,26 @@ the API.
| Python 3.12 | py312-linux64_ | py312-win32_ | py312-win64_ | py312-macos64_ | py312-macosm164_ |
+-------------+----------------+----------------+----------------+-------------------+---------------------+
.. _py37-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-linux64.zip
.. _py38-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-linux64.zip
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-linux64.zip
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-linux64.zip
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-linux64.zip
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-linux64.zip
.. _py37-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win32.zip
.. _py38-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win32.zip
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win32.zip
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win32.zip
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win32.zip
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win32.zip
.. _py37-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-win64.zip
.. _py38-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-win64.zip
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-win64.zip
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-win64.zip
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-win64.zip
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-win64.zip
.. _py37-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macos64.zip
.. _py38-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macos64.zip
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macos64.zip
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macos64.zip
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macos64.zip
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macos64.zip
.. _py37-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-37-v0.7.0-e38eafd-macosm164.zip
.. _py38-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-38-v0.7.0-e38eafd-macosm164.zip
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-e38eafd-macosm164.zip
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-e38eafd-macosm164.zip
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-e38eafd-macosm164.zip
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-e38eafd-macosm164.zip
.. _py39-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-linux64.zip
.. _py310-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-linux64.zip
.. _py311-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-linux64.zip
.. _py312-linux64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-linux64.zip
.. _py39-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-win32.zip
.. _py310-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-win32.zip
.. _py311-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-win32.zip
.. _py312-win32: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-win32.zip
.. _py39-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-win64.zip
.. _py310-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-win64.zip
.. _py311-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-win64.zip
.. _py312-win64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-win64.zip
.. _py39-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-macos64.zip
.. _py310-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-macos64.zip
.. _py311-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-macos64.zip
.. _py312-macos64: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-macos64.zip
.. _py39-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-39-v0.7.0-f7c03db-macosm164.zip
.. _py310-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-310-v0.7.0-f7c03db-macosm164.zip
.. _py311-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.7.0-f7c03db-macosm164.zip
.. _py312-macosm164: https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-312-v0.7.0-f7c03db-macosm164.zip
2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your
Python path. If you're not sure where your Python path is, run the following
@@ -233,4 +233,4 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
"``concat({{value}}[, {{value2}}]*)``", "``concat(""foo"", ""bar"")``", "``foobar``", "Concatenates two or more strings."
"``round({{value}}, {{precision}})``", "``round(3.123, 0.1)``", "``3.1``", "Rounds ``{{value}}`` to the nearest ``{{precision}}``."
"``metric_length({{value}}, {{precision}}, {{decimals}})``", "``metric_length(3.123, 0.1, 2)``", "``3.10``", "Rounds ``{{value}}`` to the nearest ``{{precision}}`` then displays using a certain amount of decimal places."
"``imperial_length({{value}}, {{precision}}, {{unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "Rounds ``{{value}}`` to the nearest ``1/{{precision}}`` inch then displays using fractional feet and inches. The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{unit}}``."
"``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}})``", "``imperial_length(3.22, 4, ""foot"")``", "``3' - 3 3/4""``", "``The {{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot`` or just inches if ``{{output_unit}}`` is set to ``inch``."
@@ -61,7 +61,8 @@ operating systems. GCC (4.7 or newer) or Clang (any version) is required.
$ sudo apt-get install git cmake gcc g++ libboost-all-dev libcgal-dev
3. Install OpenCascade Technology (OCCT).
3. Install OpenCascade Technology (OCCT). Officially v7.5.0 is supported. Other
versions may have unexpected behaviour.
::

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