Compare commits

...

87 Commits

Author SHA1 Message Date
Andrej730 ecee3781e4 Fix more missing Blender descriptions leading to crashes #6420 2025-04-15 19:43:38 +05:00
Andrej730 52f949b2dc assign_class - avoid empty geometries and assigning shape reprs to structural items 2025-04-15 19:43:38 +05:00
Andrej730 cb7e04f61a assign_class - replace object with empty if no representation was added
Same goal as ffffcc576e - prevent user confusion when they reload IFC file and see the representation is gone.
2025-04-15 19:43:38 +05:00
Andrej730 b59e5b076d get_pset, get_psets - don't fail on ifc2x3 profile psets and partially handle ifc2x3 material psets #6538 2025-04-14 18:59:47 +05:00
Andrej730 1516237537 Allow Undefined topology representations for IfcStructuralPointConnection #6459
Because apparently it's not invalid IFC.
IfcStructuralPointConnection documentation says that they should have a topology representation with a IfcVertexPoint but it doesn't restict this representation from having Undefined type if it does have a IfcVertexPoint as it's item.
2025-04-14 17:49:17 +05:00
Andrej730 53cd5f834e add_mesh_representation - reuse shape builder + numpy input support 2025-04-14 16:17:59 +05:00
Andrej730 6dc6fddd0e add_mesh_representation - expand documentation and typing #6541 2025-04-14 16:17:59 +05:00
Dion Moult 52c8cc0a72 Fix #6140. You can configure the force snap setting.
I can't figure out a more elegant way of doing this. I.e. I don't know
how to detect whether or not the user has explicitly set their own snap
default.
2025-04-13 17:57:08 +10:00
Dion Moult 463473b959 Fix #6359. No longer sync materials at export sync we have a proper material / style manager now not Blender materials. 2025-04-13 16:09:36 +10:00
Dion Moult fd0c578995 No longer sync edited objects since we don't defer representation updates anymore due to item mode. 2025-04-13 16:06:15 +10:00
Dion Moult 74fbe94ed4 Update feature_request.yml 2025-04-13 12:10:28 +10:00
Dion Moult 38002e1ee2 Update and rename feature_request.md to feature_request.yml 2025-04-13 12:08:28 +10:00
Dion Moult 58b070d22d Update and rename bug_report.md to bug_report.yml 2025-04-13 12:06:46 +10:00
Dion Moult e2b119bf9b Update feature_request.md 2025-04-13 11:42:53 +10:00
Dion Moult 8cf3807c2c Update feature_request.md 2025-04-13 11:42:11 +10:00
Dion Moult ad3d0fecb2 Describe other channels in issue template 2025-04-13 11:38:33 +10:00
Dion Moult fcfe6f97c3 Update bug_report.md 2025-04-13 11:32:30 +10:00
Dion Moult c0190b5065 Fix #6452. Fix #6478. Disable camera clipping due to unpredictable Blender behaviour (reported upstream).
https://projects.blender.org/blender/blender/issues/137384
2025-04-13 11:20:57 +10:00
Andrej730 e596ac6b1b dev_environment.bat - ensure symlinks are loaded #6473 2025-04-11 19:55:27 +05:00
Andrej730 d9ed6a4950 Fix bim test after 1e3f1e7dfc 2025-04-11 19:55:26 +05:00
Andrej730 a49ccfcfc8 test_append_asset - fix skipped test
It wasn't appending library_material so test would never fail
2025-04-11 19:55:26 +05:00
Andrej730 7974d8a0d0 project.append_asset - test assume_asset_uniqueness_by_name=False 2025-04-11 19:55:26 +05:00
Andrej730 f00e77ef6c bim.validate_ifc_assets Bonsai operator #6243
There's now operator to check that all profiles, styles, materials in the current project have a unique name. Which is important since it's used when we append assets to ensure there are no duplicated assets.

Operator location - https://i.imgur.com/bvXjZ8Y.png

Example output:
```
IfcMaterial name 'KnownUnknown' is used by multiple assets:
- # 260=IfcMaterial('KnownUnknown',$,$)
- # 1220=IfcMaterial('KnownUnknown',$,$)

Found issues validating IfcProfileDef assets.
IfcProfileDef name 'DEMO-C' is used by multiple assets:
- # 266=IfcCShapeProfileDef(.AREA.,'DEMO-C',$,0.2,0.1,0.0015,0.03,0.005)
- # 261=IfcIShapeProfileDef(.AREA.,'DEMO-C',$,0.1,0.2,0.005,0.01,0.005,$,$)

Found issues validating IfcPresentationStyle assets.
IfcPresentationStyle name 'Frame' is used by multiple assets:
- # 363=IfcSurfaceStyle('Frame',.BOTH.,(# 362))
- # 1221=IfcSurfaceStyle('Frame',.BOTH.,(# 1222))
```
2025-04-11 19:55:26 +05:00
Andrej730 fc37cca1d0 ExtractElements - expose assume_asset_uniqueness_by_name option #6243
I'd much rather not expose this option to ifcpatch but I guess models are different and there should be a way to extract elements even if assets name uniqueness is not ensured (since it's not IFC requirement anyway). But it may cause duplicated assets.
2025-04-11 19:55:26 +05:00
Andrej730 53da00a5b7 append_asset - fix possible errors with assume_asset_uniqueness_by_name=False after f43e33b
If `assume_asset_uniqueness_by_name` is `False`, then we don't really need `SafeRemovalContext`.
2025-04-11 19:55:25 +05:00
Andrej730 b5bc53bdf6 Renaming style from UI not to add .xxx Blender suffix to style name
Set style name explicitly, this way what user does in UI is what they get in IFC.
Previously confusingly if you set style name to "Glass" and "Glass" already existed, you would get "Glass.001" style internally but "Glass" name in UI.
2025-04-11 19:55:25 +05:00
Andrej730 bc0fbf744a bim.search - set found object as active
As sometimes it's hard of find a selected object in outliner if it's not active.
2025-04-11 19:55:25 +05:00
Andrej730 53b26a9b66 typing 2025-04-11 19:55:25 +05:00
Thomas Krijnen 858e73c9d9 IfcPropertySetDefinitionSet in xml serializer #6330 2025-04-11 15:53:27 +02:00
Thomas Krijnen 952400bd58 Support defined types with instance references in parser (IfcPropertySetDefinitionSet; #6330) 2025-04-11 15:52:40 +02:00
Andrej730 ffffcc576e assign_class - reload representation after assignment
Just to make sure user will set what was actually saved to IFC right away. To prevent confusion when they see it only after model was reloaded completely.
2025-04-10 19:16:04 +05:00
Andrej730 d8102d848d Fix bug with the first object rename after class assignment
The problem was that during class assignment, it was setting the object name and `is_renaming` flag but only then linking to IFC. Since object wasn't linked to IFC, name callback wasn't triggered and `is_renaming` wasn't reset and first rename wouldn't work.
Now it's just not setting the flag if object is not linked to IFC, as there's no callback then and don't need to protect from it.
2025-04-10 17:44:06 +05:00
Andrej730 9407faf1f9 vs-cfg.md - support cmake 4.0.0 2025-04-10 17:44:06 +05:00
Andrej730 1e3f1e7dfc Show assign/reference container buttons if container has no elements #6514 2025-04-10 10:54:46 +05:00
Andrej730 8aad28c730 bim.assign_class - skip ifc objects to prevent errors 2025-04-09 18:26:55 +05:00
Andrej730 81943ec9e5 bim.assign_class - apply modifiers
Noticed working on #6500
2025-04-09 18:26:55 +05:00
Andrej730 5b15de50b6 bim.assign_class - use blender operators in a more safe way
Use temp_override or modify selection not to modify some object by accident, because this operator can also be called not from UI but by providing `object` argument.
2025-04-09 18:26:54 +05:00
Andrej730 4d50dce16d bim.assign_class - use get_selected_objects 2025-04-09 18:26:54 +05:00
Andrej730 69b5d0940f bim.assign_class - flip normals if matrix is negative #6500
object.transform_apply doesn't preserve normals (https://projects.blender.org/blender/blender/issues/102266)
2025-04-09 18:26:54 +05:00
Andrej730 273d6c1039 Experimental geo nodes integration 2025-04-09 18:26:54 +05:00
Andrej730 24ef03c96e bim.assign_container - skip aggregated elements #6514
New description - https://i.imgur.com/UeSPtEe.png
2025-04-09 18:26:54 +05:00
Andrej730 cc4553d3c9 bim.aggregate_assign_object - clarification 2025-04-09 18:26:54 +05:00
Andrej730 cf1f74e260 typing 2025-04-09 18:26:53 +05:00
Andrej730 3052c6e578 black . 2025-04-09 18:26:53 +05:00
Bruno Perdigão 0eb70b7fc3 See #6397 - Fix extrusion profile editing issue.
Profile transformations now correctly use the perpendicular value of the offset.
2025-04-08 20:07:46 -03:00
Bruno Perdigão eb863bb2aa Snap - Filter out 2D bounding boxes that are too far from the camera view. 2025-04-08 18:11:01 -03:00
Andrej730 345a6e72d9 Make referencing in structure more discoverable #6514
Didn't wanted to disrupt existing operators, but we probably should deprecate them.
2025-04-08 15:54:41 +05:00
Andrej730 8b775ea30f bim.assign_container - small clarification 2025-04-08 15:54:41 +05:00
Thomas Krijnen b712f30ad3 Don't assign material as style when it doesn't have any styles #6314 2025-04-07 21:25:33 +02:00
Andrej730 0bbe1aad5b Fix UI errors in Blender 4.2 (missing MOUSE_MMB_SCROLL icon)
Noticed #6515
2025-04-07 16:20:22 +05:00
Andrej730 29de264b43 ifc5d - remove Qto_BodyGeometryValidation from ifc4x3 base qtos (6c04669)
As it's not part of base quantities and it would be unexpected for user if some additional qto would start calculating for every IfcProduct.
2025-04-07 16:20:22 +05:00
Andrej730 f4714d2012 ifc5d - avoid overriding results from previous queries after f63d6b8 2025-04-07 16:20:21 +05:00
Andrej730 8bfdabab80 ifc5d IfcOpenShell.calculate - warn on missing formulas 2025-04-07 16:20:21 +05:00
Andrej730 50c1220730 typing 2025-04-07 16:20:21 +05:00
Bruno Perdigão c280550e30 Measure Tool - fix issue where the area highlight was incorrectly displayed in polyline mode. 2025-04-05 11:55:05 -03:00
Bruno Perdigão 2bf303686c Snap - Improve how viewport zoom affects snapping. 2025-04-05 11:45:56 -03:00
Bruno Perdigão bb91416c4e Snap - Fix creation of 2D bounding boxes
Previously, objects with 2D bounding boxes outside the view were still being added. This issue is now resolved.
Additionally, implement simple AABB detection to check if the bounding box is within the viewport.
2025-04-04 20:19:25 -03:00
Bruno Perdigão 47e8b978af Snap - filter visible objects before creating the 2d bouding box 2025-04-04 18:38:07 -03:00
Andrej730 50eae4ae19 Fix typo, expose Extend To Underside description 2025-04-04 19:04:03 +05:00
Andrej730 979183b64b Make adding openings more discoverable
1) It's back as Shift-O hotkey in BIM tool - https://i.imgur.com/XfmfqNY.png
2) It's always visible in Voids UI - https://i.imgur.com/rO6nOSw.png
2025-04-04 19:04:03 +05:00
Andrej730 dc2f788326 model/wall.py - poll messages 2025-04-04 19:04:03 +05:00
Andrej730 7d12880eb9 BIM Tool - add descriptions to "Align" section buttons 2025-04-04 19:03:08 +05:00
Andrej730 6676c7f62e typing 2025-04-04 19:03:08 +05:00
Thomas Krijnen c0e8c3d1b5 Update ci-ifcopenshell-docker.yml - action versions 2025-04-04 14:11:41 +02:00
Andrej730 e9245efd37 bim.browse_external_style - cache enum items #6496 #5673 2025-04-03 19:37:51 +05:00
Andrej730 de287395d2 Bump Blender build in tests to 4.4 2025-04-03 19:18:42 +05:00
Andrej730 938d72ebe3 Fix UI issue with props missing (65c99c5) 2025-04-03 18:06:49 +05:00
Andrej730 605c86afc5 Fix void bim tests after bim.add_potential_opening was removed 2025-04-03 18:06:48 +05:00
Andrej730 68cf8ee247 BIM tests - replace some class assignment with PanelSpy version 2025-04-03 18:06:48 +05:00
Andrej730 a452d1d361 Fix failing bim tests after 5b33c60 2025-04-03 18:06:48 +05:00
Andrej730 71067e4a14 Fix style test in Blender 4.4 2025-04-03 18:06:48 +05:00
Andrej730 7bacdbf8dd typing 2025-04-03 17:45:49 +05:00
Thomas Krijnen 54c1dfdb77 Update ci-ifcopenshell-docker.yml 2025-04-03 09:29:49 +02:00
Thomas Krijnen 834123ad14 Update release.yml - add workflow_dispatch 2025-04-03 09:27:32 +02:00
Andrej730 3cbab975d5 Fix tests after ea84f82 2025-04-02 19:29:43 +05:00
Andrej730 0315542237 geometry.remove_representation_item - add element argument
Use explicit element argument instead of props.representation_obj as representation_obj may not be there (e.g. removing rep items without item mode) or some other object might be in item mode and then booleans won't be unmarked.

Found the issue because of the failing test 🥳
2025-04-02 19:29:43 +05:00
Andrej730 156cca5932 representation_obj - add a note 2025-04-02 19:29:43 +05:00
Andrej730 4672fc341f Fix representation items / booleans UI prioritizing item mode object over active obj
It led to confusion when object A would be in Item Mode and you open properties for object B and try to show rep items for B and UI would be unresponsive.
2025-04-02 19:29:43 +05:00
Andrej730 f69b02cad9 bim.override_object_join - a bit more readable error message
Use name instead of bpy_struct repr, add a line split.
2025-04-02 19:29:42 +05:00
Andrej730 40af3f0495 Fix bug adding brick namespace in ed0b1c5
brick graph has bool evaluation too
2025-04-02 19:29:42 +05:00
Andrej730 0dd42c1fb2 fix core system tests after 5b9ef23 2025-04-02 19:29:42 +05:00
Andrej730 0990dfd372 biim.load_link - error if file does not exist
Noticed investigating #6403
2025-04-02 12:42:02 +05:00
Andrej730 9316e0bff0 Skip loading linked models in the drawing if they're unloaded #6403
As it's probably unintuitive (user can't see the linked model in viewport but we still load it) and user then have no option to keep model in Links but keep it unloaded.

Also producing traceback below if linked file is missing.
```
Error: Python: Traceback (most recent call last):
  File "\bonsai\bim\module\drawing\operator.py", line 299, in execute
    linework_svg = self.generate_linework(context)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\bonsai\bim\module\drawing\operator.py", line 828, in generate_linework
    IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "\ifcopenshell\__init__.py", line 162, in open
    raise FileNotFoundError(f"File does not exist: '{path}'.")
FileNotFoundError: File does not exist: 'D:\Dropbox\GitLab\OD_Library\BIM\OD_Revit_Template - 2020 - IFC4 - Reference View.ifc'.
```
2025-04-02 12:42:02 +05:00
Andrej730 bc709f06f7 Fix Blender 4.4 crashing on Mac (decorators issues) #6461 2025-04-01 19:16:16 +05:00
Andrej730 cc031a23cb black . 2025-04-01 19:16:16 +05:00
Andrej730 614b71e0c6 gitignore compiled ifcopenshell_wrapper files on mac/linux #4686 2025-04-01 19:10:05 +05:00
Andrej730 66329f07cb dev_environment.sh - validate input paths 2025-04-01 19:10:05 +05:00
135 changed files with 2330 additions and 938 deletions
-30
View File
@@ -1,30 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Error Description and Steps to Reproduce**
<!--
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
-->
**Attachments**
<!--
If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload). Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards.
-->
**Debug information**
<!--
If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc.
-->
+23
View File
@@ -0,0 +1,23 @@
name: Bug Report
description: Crashes, error messages, and broken features
type: bug
body:
- type: textarea
attributes:
label: Bug Description
placeholder: |
Describe what problem occurred and what you expected to happen instead.
1. To reproduce this, open file '...'
2. Click on '....'
3. See error
- type: textarea
attributes:
label: Attachments
description: "Private files can be uploaded to https://ifcopenshell.org/upload.html - only viewed by core developers and will be deleted afterwards. Please submit your report first then upload private files afterwards."
placeholder: "If applicable, add screenshots to help explain your problem. Please also drag-drop any files necessary to show the error (rename the file extension from .ifc to .txt to upload)."
- type: textarea
attributes:
label: Debug and Error Output
description: "If this is in Bonsai, paste the output from the Copy Debug Information option in Bonsai. It can be found under Quality and Coordination -> Quality Control -> Debug. If this is a general software issue, if relevant include details about IfcOpenShell version, operating system, Python version, etc."
render: yes
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Community Forums
url: https://community.osarch.org/
about: Have a question? Want to discuss an idea? Try the OSArch forums instead.
- name: Live Chat
url: https://osarch.org/chat
about: Have a really confusing issue? Want real-time support?
-15
View File
@@ -1,15 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Feature Description**
<!--
Describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable.
-->
@@ -0,0 +1,8 @@
name: Feature Request
description: Suggest a new feature or improvement
type: Feature
body:
- type: textarea
attributes:
label: Feature Description
placeholder: "Describe a feature you'd like us to add, or a change to the user interface, design, or workflow for usability. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable."
+1 -1
View File
@@ -104,7 +104,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
+6 -6
View File
@@ -9,7 +9,7 @@ on:
jobs:
activate:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
if: |
github.repository == 'IfcOpenShell/IfcOpenShell' &&
!startsWith(github.event.head_commit.message, 'Release ') &&
@@ -18,7 +18,7 @@ jobs:
- run: echo ok go
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v2
@@ -35,7 +35,7 @@ jobs:
-
name: ccache
uses: hendrikmuhs/ccache-action@v1
uses: hendrikmuhs/ccache-action@v1.2
-
name: Build ifcopenshell
@@ -75,7 +75,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
# Artifact name
name: ifcos-artifacts
@@ -83,7 +83,7 @@ jobs:
path: build/assets/Ifc*
deliver:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: build
name: Docker Build, Tag, Push
@@ -118,6 +118,6 @@ jobs:
repository: aecgeeks/ifcopenshell
# Since the dispatch is set to `tag`, `github.ref_name` should evaluate to the pushed tag
# On a workflow dispatch, `ref_name` will take on the value from the dispatch payload
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:latest' }}
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:22.04' }}
file: ./Dockerfile
push: true
+1
View File
@@ -4,6 +4,7 @@ on:
push:
tags:
- 'v[0-9].[0-9].[0-9]*'
workflow_dispatch:
jobs:
activate:
+1 -1
View File
@@ -91,7 +91,7 @@ src/bonsai/drawings
src/bonsai/layouts
# ifcopenshell swig and compiled files
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper.so
src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# apple
+6
View File
@@ -20,6 +20,7 @@ import os
import bpy
import bpy.utils.previews
import importlib
from bpy_extras.io_utils import ImportHelper, ExportHelper
from . import handler, ui, prop, operator
from typing import Callable, Union
@@ -221,7 +222,12 @@ def on_register(scene):
def register():
for cls in classes:
# Prevent crashes in Blender 4.4.0, see #6420.
if issubclass(cls, (ImportHelper, ExportHelper)):
assert getattr(cls, "bl_description", "") or cls.__doc__, cls
bpy.utils.register_class(cls)
bpy.app.handlers.depsgraph_update_post.append(on_register)
bpy.app.handlers.undo_post.append(handler.undo_post)
bpy.app.handlers.redo_post.append(handler.redo_post)
-30
View File
@@ -47,7 +47,6 @@ class IfcExporter:
self.set_header()
IfcStore.update_cache()
self.sync_all_objects()
self.sync_edited_objects()
tool.Project.save_linked_models_to_ifc()
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
if extension == "ifczip":
@@ -101,41 +100,12 @@ class IfcExporter:
result = self.sync_object_placement(obj)
if result:
results.append(result)
result = self.sync_object_material(obj)
# TODO: sync_object_material always returns None
# so it's never really appended
if result:
results.append(result)
except ReferenceError:
pass # The object is likely deleted
return results
def sync_edited_objects(self) -> list[ifcopenshell.entity_instance]:
results: list[ifcopenshell.entity_instance] = []
for obj in IfcStore.edited_objs.copy():
if not obj:
continue
if not tool.Blender.is_valid_data_block(obj):
continue
element = tool.Ifc.get_entity(obj)
if element:
results.append(element)
bpy.ops.bim.update_representation(obj=obj.name)
IfcStore.edited_objs.clear()
return results
def sync_object_material(self, obj: bpy.types.Object) -> None:
if not obj.data or not isinstance(obj.data, bpy.types.Mesh):
return
if not self.has_changed_materials(obj):
return
bpy.ops.bim.update_representation(obj=obj.name)
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
mprops = tool.Geometry.get_mesh_props(obj.data)
checksum = mprops.material_checksum
return checksum != tool.Geometry.get_material_checksum(obj)
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
if tool.Geometry.is_scaled(obj):
+3 -3
View File
@@ -347,8 +347,8 @@ def load_post(scene):
# Bonsai overlays
georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
aggregate_props = tool.Aggregate.get_aggregate_props()
nest_props = tool.Nest.get_nest_props()
model_props = tool.Model.get_model_props()
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
@@ -361,7 +361,7 @@ def load_post(scene):
if model_props.show_slab_direction:
SlabDirectionDecorator.install(bpy.context)
if scene := bpy.context.scene:
if preferences.should_use_snap and (scene := bpy.context.scene):
# Snapping is off by default in Blender, but in BIM, it's more useful to be on
scene.tool_settings.use_snap = True
# Match default Bonsai snaps
+9 -3
View File
@@ -717,9 +717,15 @@ class IfcImporter:
def create_structural_point_connections(self):
for product in self.file.by_type("IfcStructuralPointConnection"):
# TODO: make this based off ifcopenshell. See #1409
representation: ifcopenshell.entity_instance = next(
rep for rep in product.Representation.Representations if rep.RepresentationType == "Vertex"
)
representation = tool.Structural.get_vertex_representation(product)
if not representation:
print(
"WARNING. Skipping invalid IfcStructuralPointConnection - "
f"element has no valid representation:\n{product}."
)
continue
mesh = tool.Loader.create_structural_point_connection_mesh(representation)
if mesh is None:
continue
@@ -20,6 +20,7 @@ import blf
import bpy
import gpu
import ifcopenshell
import ifcopenshell.util.element
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
@@ -131,6 +132,9 @@ class AggregateDecorator:
return shader
def draw_custom_batch(self, coords, color):
indices = None
if not tool.Blender.validate_shader_batch_data(coords, indices):
return
shader = self.dotted_line_shader()
arc_lengths = [0]
@@ -141,6 +145,7 @@ class AggregateDecorator:
shader,
"LINE_STRIP",
{"position": coords, "arcLength": arc_lengths},
indices=indices,
)
matrix = bpy.context.region_data.perspective_matrix
@@ -150,13 +155,16 @@ class AggregateDecorator:
batch.draw(shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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_aggregate(self, context):
if context.scene.BIMAggregateProperties.in_aggregate_mode:
props = tool.Aggregate.get_aggregate_props()
if props.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
@@ -204,7 +212,7 @@ class AggregateDecorator:
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
if context.scene.BIMAggregateProperties.in_aggregate_mode:
if props.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
@@ -243,6 +251,8 @@ class AggregateModeDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -253,7 +263,7 @@ class AggregateModeDecorator:
return
region = context.region
rv3d = region.data
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
@@ -284,7 +294,7 @@ class AggregateModeDecorator:
def draw_aggregate_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
@@ -23,6 +23,7 @@ import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
import bonsai.core.spatial
from typing import TYPE_CHECKING
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
@@ -30,23 +31,30 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Object To Aggregation"
bl_description = (
"Assign object as an aggregate to the selected IFC elements.\n\n"
"If called from Bonsai UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called from Object Aggregates UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called directly, active object will be considered a relating whole"
)
bl_options = {"REGISTER", "UNDO"}
relating_object: bpy.props.IntProperty()
related_object: bpy.props.IntProperty()
if TYPE_CHECKING:
relating_object: int
related_object: int
def _execute(self, context):
relating_obj = None
if self.relating_object:
relating_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.relating_object))
assert relating_obj
elif self.related_object:
aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get().by_id(self.related_object))
if aggregate:
relating_obj = tool.Ifc.get_object(aggregate)
elif context.active_object:
assert relating_obj
else:
relating_obj = context.active_object
if not relating_obj:
self.report({"ERROR"}, "No relating object is provided.")
return
@@ -66,7 +74,7 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
relating_obj=relating_obj,
related_obj=obj,
)
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
new_editing_obj = props.editing_objects.add()
new_editing_obj.obj = obj
@@ -182,7 +190,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
)
core.assign_object(tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=aggregate, related_obj=obj)
def create_aggregate(self, context, ifc_class, aggregate_name):
def create_aggregate(self, context: bpy.types.Context, ifc_class: str, aggregate_name: str) -> bpy.types.Object:
aggregate = bpy.data.objects.new(aggregate_name, None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
@@ -388,7 +396,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
objs = [o.obj for o in props.editing_objects]
if props.in_aggregate_mode:
if context.space_data.local_view:
@@ -33,6 +33,7 @@ from bpy.props import (
CollectionProperty,
)
from bonsai.bim.module.aggregate.decorator import AggregateDecorator, AggregateModeDecorator
from typing import TYPE_CHECKING, Union
def can_aggregate(relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
@@ -98,12 +99,22 @@ class BIMObjectAggregateProperties(PropertyGroup):
poll=poll_related_object,
)
if TYPE_CHECKING:
is_editing: bool
relating_object: Union[bpy.types.Object, None]
related_object: Union[bpy.types.Object, None]
class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
previous_hide_select: bpy.props.BoolProperty(default=False)
if TYPE_CHECKING:
obj: Union[bpy.types.Object, None]
previous_display_type: str
previous_hide_select: bool
class BIMAggregateProperties(PropertyGroup):
in_aggregate_mode: BoolProperty(name="In Edit Mode", update=update_aggregate_mode_decorator)
@@ -115,3 +126,10 @@ class BIMAggregateProperties(PropertyGroup):
default=False,
update=update_aggregate_decorator,
)
if TYPE_CHECKING:
in_aggregate_mode: bool
editing_aggregate: Union[bpy.types.Object, None]
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
aggregate_decorator: bool
+6 -5
View File
@@ -49,11 +49,13 @@ class BIM_PT_aggregate(Panel):
layout = self.layout
row = layout.row()
row.label(text="Aggregate Decorator")
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
props = tool.Aggregate.get_aggregate_props()
row.prop(props, "aggregate_decorator", icon="HIDE_OFF", text="")
if not AggregateData.is_loaded:
AggregateData.load()
props = context.active_object.BIMObjectAggregateProperties
assert (obj := context.active_object)
props = tool.Aggregate.get_object_aggregate_props(obj)
if props.is_editing:
row = layout.row()
@@ -131,9 +133,8 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
row = layout.row(align=True)
if element.Decomposes:
@@ -45,6 +45,7 @@ from typing_extensions import assert_never
class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_alignment_csv"
bl_label = "Import Alignment CSV"
bl_description = " Import alignment from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@@ -26,30 +26,42 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
from typing import TYPE_CHECKING
def get_objs_for_operation(operator_properties, context):
def get_objs_for_operation(
operator_properties: "AttributesOperator", context: bpy.types.Context
) -> list[bpy.types.Object]:
if operator_properties.obj:
return [bpy.data.objects[operator_properties.obj]]
if operator_properties.mass_operation:
return context.selected_objects[:]
return [context.active_object]
obj = context.active_object
assert obj
return [obj]
class EnableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
class AttributesOperator:
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
if TYPE_CHECKING:
obj: str
mass_operation: bool
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def enable_editing_attribute_on_obj(self, obj):
props = obj.BIMAttributeProperties
class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
bl_description = "ALT + Left Click to enable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
def enable_editing_attribute_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
@@ -87,20 +99,14 @@ class EnableEditingAttributes(bpy.types.Operator):
return {"FINISHED"}
class DisableEditingAttributes(bpy.types.Operator):
class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes"
bl_description = "ALT + Left Click to disable editing attributes on all selected objects"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty(options={"SKIP_SAVE"})
mass_operation: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.mass_operation = event.alt
return self.execute(context)
def disable_editing_attributes_on_obj(self, obj):
props = obj.BIMAttributeProperties
def disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
props.is_editing_attributes = False
def execute(self, context):
@@ -118,7 +124,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.file = tool.Ifc.get()
obj = tool.Blender.get_active_object(is_selected=False)
if not (element := tool.Ifc.get_entity(obj)):
if not obj or not (element := tool.Ifc.get_entity(obj)):
return
def callback(attributes, prop):
@@ -130,7 +136,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
attributes[prop.name] = None
return True
props = obj.BIMAttributeProperties
props = tool.Blender.get_object_attribute_props(obj)
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
@@ -165,13 +171,12 @@ class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
if not obj or not (props := tool.Blender.get_object_attribute_props(obj)).is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element.is_a("IfcRoot"):
if not element or not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
@@ -191,7 +196,10 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
name: bpy.props.StringProperty()
def _execute(self, context):
value = tool.Blender.get_active_object().BIMAttributeProperties.attributes.get(self.name).get_value()
obj = tool.Blender.get_active_object()
assert obj
props = tool.Blender.get_object_attribute_props(obj)
value = props.attributes[self.name].get_value()
total = core.copy_attribute_to_selection(
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
)
@@ -29,8 +29,13 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING
class BIMAttributeProperties(PropertyGroup):
attributes: CollectionProperty(name="Attributes", type=Attribute)
is_editing_attributes: BoolProperty(name="Is Editing Attributes")
if TYPE_CHECKING:
attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
is_editing_attributes: bool
+4 -2
View File
@@ -17,14 +17,16 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.bim.helper
import bpy.types
from bpy.types import Panel
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
def draw_ui(context, layout, attributes):
def draw_ui(context: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
obj = context.active_object
props = obj.BIMAttributeProperties
assert obj
props = tool.Blender.get_object_attribute_props(obj)
if props.is_editing_attributes:
row = layout.row(align=True)
@@ -43,6 +43,8 @@ class BoundaryDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -49,6 +49,8 @@ class ClashDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -536,6 +536,7 @@ class SelectCostScheduleProducts(bpy.types.Operator):
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "bim.import_cost_schedule_csv"
bl_label = "Import Cost Schedule CSV"
bl_description = "Import cost schdule from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@@ -44,6 +44,7 @@ classes = (
operator.SelectHighPolygonMeshes,
operator.SelectHighestPolygonMeshes,
operator.ToggleDetailedIOSLogs,
operator.ValidateIfcAssets,
operator.ValidateIfcFile,
prop.BIMDebugProperties,
ui.BIM_PT_debug,
+63 -1
View File
@@ -37,6 +37,7 @@ import bonsai.core.profile
import bonsai.core.type
import bonsai.bim.handler
import bonsai.bim.import_ifc as import_ifc
from collections import defaultdict
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from bonsai import get_debug_info, format_debug_info
@@ -152,6 +153,67 @@ class ValidateIfcFile(bpy.types.Operator):
return {"FINISHED"}
class ValidateIfcAssets(bpy.types.Operator):
bl_idname = "bim.validate_ifc_assets"
bl_label = "Validate IFC Assets"
bl_description = (
"Run Bonsai validation for IFC assets.\n\n"
"There's an internal Bonsai convention to treat some IFC assets "
"as unique based on their name (e.g. profiles, materials, styles).\n"
"Though it's not required by IFC, it is a generally good practice "
"to keep asset names unique and it also helps with various issues.\n"
"If it's not conformed, it could lead to duplicated assets or "
"the opposite - different assets of the same name treated as one."
)
bl_options = set()
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("IFC file is not loaded.")
return False
return True
def execute(self, context):
ifc_file = tool.Ifc.get()
ifc_classes = {
"IfcMaterial": "Name",
"IfcProfileDef": "ProfileName",
"IfcPresentationStyle": "Name",
}
issues_found = False
unique_assets: defaultdict[str, list[ifcopenshell.entity_instance]]
for ifc_class, name_attr in ifc_classes.items():
unique_assets = defaultdict(list)
for asset in ifc_file.by_type(ifc_class):
asset_name: Union[str, None] = getattr(asset, name_attr)
if asset_name is None:
continue
unique_assets[asset_name].append(asset)
msg = ""
for asset_name, assets in unique_assets.items():
if len(assets) == 1:
continue
msg += f"{ifc_class} name '{asset_name}' is used by multiple assets:\n"
for asset in assets:
msg += f"- {asset}\n"
if msg:
issues_found = True
msg = f"Found issues validating {ifc_class} assets.\n" + msg
print(msg)
if issues_found:
self.report({"INFO"}, "Check asset validation results in the system console.")
else:
self.report({"INFO"}, "No asset validation issues found.")
return {"FINISHED"}
class ProfileImportIFC(bpy.types.Operator):
profile_filename = "blender.prof"
bl_idname = "bim.profile_import_ifc"
@@ -962,7 +1024,7 @@ class RestartBlender(bpy.types.Operator):
if not ms_store_app_id:
path = bpy.app.binary_path
if platform.system() == "Windows":
args = sys.argv[1:]
args = sys.argv[1:]
command_line = subprocess.list2cmdline([path] + args)
os.execv(path, [command_line])
else:
+2
View File
@@ -50,6 +50,8 @@ class BIM_PT_debug(Panel):
row.prop(props, "package_name", text="")
row.operator("bim.pip_install", icon="EVENT_PAGEDOWN").name = props.package_name
layout.operator("bim.validate_ifc_assets", icon="CHECKMARK")
row = layout.row()
row.operator("bim.reload_ifc_file", text="Incrementally Reload Changes")
@@ -318,6 +318,8 @@ class BaseDecorator:
color: tuple[float, float, float, float],
indices: Optional[list[tuple[int, int]]] = None,
) -> None:
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
shader = self.line_shader if shader_type == "LINES" else self.base_shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
@@ -1699,6 +1701,8 @@ class CutDecorator:
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -414,6 +414,8 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo):
shader_wrapper = ExtrusionGuidesShader()
verts = [Vector((0, 0, 0)), Vector((0, 0, 1))]
verts, edges = shader_wrapper.process_geometry(verts)
if not tool.Blender.validate_shader_batch_data(verts, edges):
verts, edges = [], []
self.custom_shape = shader_wrapper, shader_wrapper.batch(
pos=verts,
indices=edges,
@@ -799,7 +799,6 @@ class CreateDrawing(bpy.types.Operator):
exporter = bonsai.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
invalidated_elements = exporter.sync_all_objects()
invalidated_elements += exporter.sync_edited_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
if cache := IfcStore.get_cache():
[cache.remove(guid) for guid in invalidated_guids]
@@ -824,6 +823,8 @@ class CreateDrawing(bpy.types.Operator):
props = tool.Project.get_project_props()
for link in props.links:
if not link.is_loaded:
continue
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
@@ -2167,7 +2168,8 @@ class ActivateDrawingBase:
camera_props = camera.data.BIMCameraProperties
if camera_props.update_representation(camera):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
# See 6452 and 6478.
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
@@ -2221,6 +2223,7 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
class SelectDocIfcFile(bpy.types.Operator, ImportHelper):
bl_idname = "bim.select_doc_ifc_file"
bl_label = "Select Documentation IFC File"
bl_description = "Selection .ifc file for documentation."
bl_options = {"REGISTER", "UNDO"}
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filename_ext = ".ifc"
@@ -128,6 +128,8 @@ class ItemDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -1775,8 +1775,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
obj.select_set(False)
self.report(
{"ERROR"},
f"IFC join failed - object '{obj}' has a different representation type "
f"({obj_rep.RepresentationType}) than target '{self.target}' ({representation_type}).",
f"IFC join failed - object '{obj.name}' has a different representation type "
f"({obj_rep.RepresentationType})\nthan target '{self.target.name}' ({representation_type}).",
)
return
@@ -1929,7 +1929,7 @@ class OverrideEscape(bpy.types.Operator):
tool.Geometry.disable_item_mode()
elif tool.Model.get_model_props().openings:
bpy.ops.bim.hide_all_openings()
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
elif tool.Aggregate.get_aggregate_props().in_aggregate_mode:
bpy.ops.bim.disable_aggregate_mode()
elif active_object := context.active_object:
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
@@ -1953,24 +1953,26 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
selected_objs = context.selected_objects # Purposely exclude active object
aprops = tool.Aggregate.get_aggregate_props()
nprops = tool.Nest.get_nest_props()
if self.has_aggregates(selected_objs):
if not context.scene.BIMAggregateProperties.in_aggregate_mode:
if not aprops.in_aggregate_mode:
bonsai.core.aggregate.enable_aggregate_mode(tool.Aggregate, context.active_object)
return {"FINISHED"}
if self.has_nests(selected_objs):
if not context.scene.BIMNestProperties.in_nest_mode:
if not nprops.in_nest_mode:
bonsai.core.nest.enable_nest_mode(tool.Nest, context.active_object)
return {"FINISHED"}
if len(selected_objs) == 1 and context.active_object == selected_objs[0]:
self.handle_single_object(context, context.active_object)
elif len(selected_objs) == 0:
if context.scene.BIMAggregateProperties.in_aggregate_mode:
if aprops.in_aggregate_mode:
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
if context.scene.BIMNestProperties.in_nest_mode:
if nprops.in_nest_mode:
bonsai.core.nest.disable_nest_mode(tool.Nest)
return {"FINISHED"}
tool.Geometry.disable_item_mode()
@@ -1981,9 +1983,10 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
props = tool.Geometry.get_geometry_props()
pprops = tool.Project.get_project_props()
aprops = tool.Aggregate.get_aggregate_props()
if obj == props.representation_obj:
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
elif obj in [o.obj for o in aprops.not_editing_objects]:
obj.select_set(False)
self.report(
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
@@ -2099,7 +2102,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
parts = ifcopenshell.util.element.get_parts(element)
if (aggregate or parts) and not bpy.context.scene.BIMAggregateProperties.in_aggregate_mode:
props = tool.Aggregate.get_aggregate_props()
if (aggregate or parts) and not props.in_aggregate_mode:
return True
else:
return False
@@ -2111,7 +2115,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
continue
nest = ifcopenshell.util.element.get_nest(element)
components = ifcopenshell.util.element.get_components(element)
if (nest or components) and not bpy.context.scene.BIMNestProperties.in_nest_mode:
props = tool.Nest.get_nest_props()
if (nest or components) and not props.in_nest_mode:
return True
else:
return False
@@ -2509,11 +2514,12 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
assert (obj := tool.Geometry.get_active_or_representation_obj())
assert (element := tool.Ifc.get_entity(obj))
ifc_file = tool.Ifc.get()
representation_item = ifc_file.by_id(self.representation_item_id)
tool.Geometry.remove_representation_item(representation_item)
tool.Geometry.remove_representation_item(representation_item, element)
tool.Geometry.reload_representation(obj)
# reload items ui
@@ -3269,7 +3275,7 @@ class OverrideMove(bpy.types.Operator):
self.new_active_obj = obj
# Get aggregates
props = context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
not_editing_objs = [o.obj for o in props.not_editing_objects]
aggregates_to_move = []
for obj in context.selected_objects:
@@ -3304,7 +3310,7 @@ class OverrideMove(bpy.types.Operator):
return {"FINISHED"}
# Get nests
props = context.scene.BIMNestProperties
props = tool.Nest.get_nest_props()
not_editing_objs = [o.obj for o in props.not_editing_objects]
nests_to_move = []
for obj in context.selected_objects:
@@ -283,7 +283,14 @@ class BIMGeometryProperties(PropertyGroup):
is_changing_mode: BoolProperty(name="Is Changing Mode", default=False)
mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode)
representation_obj: PointerProperty(
name="Representation Object", type=bpy.types.Object, update=update_representation_obj
name="Representation Object",
description=(
"Only used for Item Mode. When element is in Item Mode, new objects are imported "
"for each element's representation item, original element's object is hidden and "
"representation_obj pointing to it. None if no object in Item Mode."
),
type=bpy.types.Object,
update=update_representation_obj,
)
item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject)
+1 -1
View File
@@ -225,7 +225,7 @@ class BIM_PT_representation_items(Panel):
RepresentationItemsData.load()
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or tool.Blender.get_active_object()
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
@@ -54,6 +54,8 @@ class GeoreferenceDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
props = tool.Georeference.get_georeference_props()
self.scale = props.visualization_scale
content_pos = [v * self.scale for v in content_pos]
@@ -52,6 +52,8 @@ class SolarDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -40,6 +40,7 @@ from . import (
railing,
roof,
mep,
external,
)
from typing import NamedTuple
@@ -138,6 +139,7 @@ classes = (
prop.BIMRoofProperties,
prop.BIMPolylineProperties,
prop.BIMProductPreviewProperties,
prop.BIMExternalParametricGeometryProperties,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
@@ -147,6 +149,7 @@ classes = (
ui.BIM_PT_roof,
ui.BIM_MT_type_manager_menu,
ui.BIM_MT_type_menu,
ui.BIM_PT_external_parametric_geometry,
ui.LaunchTypeMenu,
ui.LaunchTypeManager,
grid.BIM_OT_add_object,
@@ -198,6 +201,7 @@ classes = (
mep.MEPAddObstruction,
mep.MEPAddTransition,
mep.MEPAddBend,
external.ApplyExternalParametricGeometry,
)
addon_keymaps = []
@@ -255,6 +259,9 @@ def register():
bpy.types.Object.BIMDoorProperties = bpy.props.PointerProperty(type=prop.BIMDoorProperties)
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties)
bpy.types.Object.BIMExternalParametricGeometryProperties = bpy.props.PointerProperty(
type=prop.BIMExternalParametricGeometryProperties
)
bpy.types.VIEW3D_MT_add.prepend(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
@@ -277,6 +284,7 @@ def unregister():
del bpy.types.Object.BIMDoorProperties
del bpy.types.Object.BIMRailingProperties
del bpy.types.Object.BIMRoofProperties
del bpy.types.Object.BIMExternalParametricGeometryProperties
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
+33 -11
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import blf
import bpy
@@ -34,6 +35,7 @@ from gpu_extras.presets import draw_circle_2d
from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
from itertools import chain
from typing import Union, Any
def transparent_color(color, alpha=0.1):
@@ -72,6 +74,8 @@ class ProfileDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -346,21 +350,28 @@ class PolylineDecorator:
cls.is_installed = False
@classmethod
def update(cls, event, tool_state, input_ui, snapping_point):
def update(
cls,
event: bpy.types.Event,
tool_state: tool.Polyline.ToolState,
input_ui: tool.Polyline.PolylineUI,
snapping_point: Vector,
) -> None:
cls.event = event
cls.tool_state = tool_state
cls.input_ui = input_ui
# TODO: unused?
@classmethod
def set_input_ui(cls, input_ui):
def set_input_ui(cls, input_ui: tool.Polyline.PolylineUI) -> None:
cls.input_ui = input_ui
@classmethod
def set_angle_axis_line(cls, start, end):
def set_angle_axis_line(cls, start: Vector, end: Vector) -> None:
cls.axis_start = start
cls.axis_end = end
def calculate_measurement_x_y_and_z(self, context):
def calculate_measurement_x_y_and_z(self, context: bpy.types.Context) -> None:
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
@@ -383,7 +394,7 @@ class PolylineDecorator:
return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle)
@classmethod
def calculate_polygon(self, points):
def calculate_polygon(cls, points: list[Vector]) -> dict[str, Any]:
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
@@ -405,12 +416,14 @@ class PolylineDecorator:
return {"verts": verts, "edges": edges, "tris": tris}
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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_input_ui(self, context):
def draw_input_ui(self, context: bpy.types.Context) -> None:
texts = {
"D": "Distance: ",
"A": "Angle: ",
@@ -420,6 +433,7 @@ class PolylineDecorator:
"AREA": "Area:",
}
try:
assert self.event
mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y
except:
mouse_pos = (None, None)
@@ -534,7 +548,7 @@ class PolylineDecorator:
polyline_verts = [Vector((p.x, p.y, p.z)) for p in polyline_points]
# Area
if measure_type == "AREA" and polyline_data.area:
if measure_type == "POLY_AREA" and polyline_data.area:
if len(polyline_verts) < 3:
return
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
@@ -552,7 +566,7 @@ class PolylineDecorator:
blf.draw(self.font_id, text)
# Length
if measure_type in {"POLYLINE", "AREA"}:
if measure_type in {"POLYLINE", "POLY_AREA"}:
if len(polyline_verts) < 3:
return
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
@@ -722,8 +736,8 @@ class PolylineDecorator:
polyline_points = polyline_data.polyline_points
else:
polyline_points = []
polyline_verts = []
polyline_edges = []
polyline_verts: list[Vector] = []
polyline_edges: list[list[int]] = []
for point_prop in polyline_points:
point = Vector((point_prop.x, point_prop.y, point_prop.z))
polyline_verts.append(point)
@@ -761,7 +775,7 @@ class PolylineDecorator:
# Area highlight
if polyline_data:
area = polyline_data.area.split(" ")[0]
if area:
if polyline_data.measurement_type == "POLY_AREA" and area:
if float(area) > 0:
tris = self.calculate_polygon(polyline_verts)["tris"]
self.draw_batch("TRIS", polyline_verts, transparent_color(decorator_color_special), tris)
@@ -815,6 +829,8 @@ class ProductDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -882,6 +898,8 @@ class WallAxisDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -946,6 +964,8 @@ class SlabDirectionDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -1001,6 +1021,8 @@ class FaceAreaDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -0,0 +1,40 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021, 2022 Dion Moult <dion@thinkmoult.com>, @Andrej730
#
# This file is part of Bonsai.
#
# Bonsai 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.
#
# Bonsai 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 Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bmesh
import ifcopenshell
import bonsai.tool as tool
class ApplyExternalParametricGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.apply_external_parametric_geometry"
bl_label = "Apply External Parametric Geometry"
bl_description = "Apply external parametric geometry to the active object."
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
obj = context.active_object
assert obj
assert (active_representation := tool.Geometry.get_active_representation(obj))
ifc_context = active_representation.ContextOfItems
tool.Model.add_representation(obj, ifc_context)
props = tool.Model.get_epg_props(obj)
props.is_editing = False
return {"FINISHED"}
@@ -896,6 +896,8 @@ class DecorationsHandler:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
+10 -3
View File
@@ -550,6 +550,8 @@ class PolylineOperator:
# TODO Fill doc strings
""" """
objs_2d_bbox: list[tuple[bpy.types.Object, list[float]]]
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
return context.space_data.type == "VIEW_3D"
@@ -680,7 +682,10 @@ class PolylineOperator:
for action, settings in instructions.items():
if settings["icons"]:
for key in settings["keys"]:
self.layout.label(text="", icon=key)
if bpy.app.version < (4, 3, 0) and key == "MOUSE_MMB_SCROLL":
self.layout.label(text="MMB")
else:
self.layout.label(text="", icon=key)
self.layout.label(text=action)
else:
key = settings["keys"][0]
@@ -899,7 +904,8 @@ class PolylineOperator:
if self.mousemove_count == 2:
self.objs_2d_bbox = []
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
self.objs_2d_bbox.append(bbox_2d)
if self.mousemove_count > 3:
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
@@ -988,7 +994,8 @@ class PolylineOperator:
self.tool_state.mode = "Mouse"
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
self.objs_2d_bbox.append(bbox_2d)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
@@ -1142,3 +1142,43 @@ class BIMProductPreviewProperties(PropertyGroup):
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
if self.is_editing:
return
tool.Model.clean_up_parametric_geometry(self.id_data)
del self["is_editing"]
del self["geo_nodes"]
def update_geo_nodes(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
if self.geo_nodes:
tool.Model.setup_parametric_geometry(self.id_data)
return
modifier = tool.Model.get_epg_modifier(self.id_data)
assert modifier
modifier.show_viewport = False
del self["geo_nodes"]
class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
is_editing: bpy.props.BoolProperty(
name="Is Editing Paramteric Geometry",
description="Toggle editing parametric geometry.",
default=False,
update=update_is_editing,
)
geo_nodes: bpy.props.PointerProperty(
name="Geometry Nodes",
description="Geometry nodes tree to use as a source for representation.",
type=bpy.types.GeometryNodeTree,
update=update_geo_nodes,
poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"),
)
if TYPE_CHECKING:
is_editing: bool
geo_nodes: Union[bpy.types.GeometryNodeTree, None]
+6 -4
View File
@@ -677,8 +677,9 @@ class EnableEditingExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
# Restore the position to before it was changed by the offset and x_angle
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
offset = Vector((0.0, 0.0, -layer_params["offset"]))
rot_offset = offset @ rot_matrix
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
rot_offset = offset_vector @ rot_matrix
tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix
@@ -725,8 +726,9 @@ class EditExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
# Restore the position to after it was changed by the offset and x_angle
rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X")
offset = Vector((0.0, 0.0, -layer_params["offset"]))
rot_offset = offset @ rot_matrix
perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle))
offset_vector = Vector((0.0, 0.0, -perpendicular_offset))
rot_offset = offset_vector @ rot_matrix
tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix
+43
View File
@@ -701,6 +701,49 @@ class BIM_PT_roof(bpy.types.Panel):
row.operator("bim.add_roof", icon="ADD", text="")
class BIM_PT_external_parametric_geometry(bpy.types.Panel):
bl_label = "External Parametric Geometry"
bl_idname = "BIM_PT_external_parametric_geometry"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_options = {"DEFAULT_CLOSED"}
bl_parent_id = "BIM_PT_tab_parametric_geometry"
@classmethod
def poll(cls, context):
return (obj := context.active_object) and obj.type == "MESH"
def draw(self, context):
obj = context.active_object
assert obj
layout = self.layout
assert layout
props = tool.Model.get_epg_props(obj)
row = layout.row(align=True)
row.alignment = "RIGHT"
if not props.is_editing:
row.prop(props, "is_editing", icon="GREASEPENCIL", text="")
return
row.operator("bim.apply_external_parametric_geometry", icon="CHECKMARK", text="")
row.prop(props, "is_editing", icon="CANCEL", text="")
row = layout.row(align=True)
row.prop_search(props, "geo_nodes", bpy.data, "node_groups")
if props.geo_nodes:
assert (modifier := tool.Model.get_epg_modifier(obj))
inputs = tool.Model.get_parametric_geometry_inputs(modifier)
# NOTE: users won't be able to see inputs descriptions.
# If we add group node inputs as modifiers inputs, descriptions will be visible.
# But then we need to ensure inputs are up to date
# (e.g. probably just by adding a refresh button).
for input in inputs:
row = layout.row(align=True)
row.prop(input, "default_value", text=input.name)
def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
self.layout.operator_context = "INVOKE_REGION_WIN"
self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element")
+91 -48
View File
@@ -24,10 +24,12 @@ import math
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.geometry
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.type
import mathutils.geometry
import bonsai.core.type
@@ -52,7 +54,10 @@ class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
def _execute(self, context):
core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
@@ -66,7 +71,7 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
slab = None
walls = []
walls: list[bpy.types.Object] = []
if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)):
slab = obj
for obj in tool.Blender.get_selected_objects(include_active=False):
@@ -137,11 +142,17 @@ class AlignWall(bpy.types.Operator):
@classmethod
def poll(cls, context):
selected_valid_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
return context.active_object and len(selected_valid_objects) > 1
if not context.active_object:
cls.poll_message_set("No active object selected.")
return False
selected_valid_objects = tool.Model.get_selected_mesh_objects()
if len(selected_valid_objects) < 2:
cls.poll_message_set("Please select at least two mesh objects.")
return False
return True
def execute(self, context):
selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
selected_objects = tool.Model.get_selected_mesh_objects()
for obj in selected_objects:
if obj == context.active_object:
continue
@@ -165,10 +176,13 @@ class FlipWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
selected_objs = tool.Model.get_selected_mesh_objects()
joiner = DumbWallJoiner()
for obj in selected_objs:
joiner.flip(obj)
@@ -185,10 +199,13 @@ class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
selected_objs = tool.Model.get_selected_mesh_objects()
for obj in selected_objs:
DumbWallJoiner().split(obj, context.scene.cursor.location)
return {"FINISHED"}
@@ -202,12 +219,23 @@ class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not context.active_object:
cls.poll_message_set("No active object selected.")
return False
elif not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
mesh_objects = [o for o in tool.Model.get_selected_ifc_objects() if o.type == "MESH"]
if len(mesh_objects) != 2:
cls.poll_message_set("Please select exactly two mesh IFC objects.")
return False
return True
def _execute(self, context):
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
if len(selected_objs) == 2:
DumbWallJoiner().merge([o for o in selected_objs if o != context.active_object][0], context.active_object)
active_obj = context.active_object
assert active_obj
selected_objs = tool.Model.get_selected_mesh_objects()
DumbWallJoiner().merge(next(o for o in selected_objs if o != active_obj), active_obj)
return {"FINISHED"}
@@ -218,50 +246,59 @@ class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
DumbWallRecalculator().recalculate(context.selected_objects)
objects = tool.Model.get_selected_mesh_ifc_objects()
DumbWallRecalculator().recalculate(objects)
return {"FINISHED"}
class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_depth"
bl_label = "Update"
bl_description = "Update Height"
bl_description = "Update height for the selected objects."
bl_options = {"REGISTER", "UNDO"}
depth: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
layer2_objs = []
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
layer2_objs: list[bpy.types.Object] = []
ifc_file = tool.Ifc.get()
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs:
element = tool.Ifc.get_entity(obj)
if not element:
return
assert element
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
continue
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
continue
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
if tool.Model.get_usage_type(element) == "LAYER2":
for rel in element.ConnectedFrom:
if rel.is_a() == "IfcRelConnectsElements":
ifcopenshell.api.run(
"geometry.disconnect_element",
tool.Ifc.get(),
ifcopenshell.api.geometry.disconnect_element(
ifc_file,
relating_element=rel.RelatingElement,
related_element=element,
)
layer2_objs.append(obj)
if layer2_objs:
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
@@ -270,30 +307,33 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_x_angle"
bl_label = "Update"
bl_description = "Update Angle"
bl_description = "Update angle for the selected objects."
bl_options = {"REGISTER", "UNDO"}
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
layer2_objs = []
other_objs = []
layer2_objs: list[bpy.types.Object] = []
x_angle = 0 if tool.Cad.is_x(self.x_angle, 0, tolerance=0.001) else self.x_angle
x_angle = 0 if tool.Cad.is_x(self.x_angle, pi, tolerance=0.001) else self.x_angle
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs:
element = tool.Ifc.get_entity(obj)
if not element:
return
assert element
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
continue
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
return
continue
existing_x_angle = tool.Model.get_existing_x_angle(extrusion)
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle
existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle
@@ -380,17 +420,21 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_layer_length"
bl_label = "Update"
bl_description = "Update Length"
bl_description = "Update length for the selected objects."
bl_options = {"REGISTER", "UNDO"}
length: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
return context.selected_objects
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
def _execute(self, context):
joiner = DumbWallJoiner()
for obj in context.selected_objects:
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs:
joiner.set_length(obj, self.length)
return {"FINISHED"}
@@ -1078,7 +1122,7 @@ class DumbWallJoiner:
)
self.recreate_wall(element1, wall1)
def merge(self, wall1, wall2):
def merge(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
if tool.Ifc.is_moved(wall2):
@@ -1086,6 +1130,7 @@ class DumbWallJoiner:
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
assert element1 and element2
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p3, p4 = ifcopenshell.util.representation.get_reference_line(element2)
@@ -1192,14 +1237,13 @@ class DumbWallJoiner:
self.set_axis(element1, intersect, p2)
self.recreate_wall(element1, wall1)
def set_length(self, wall1, si_length):
def set_length(self, wall1: bpy.types.Object, si_length: float) -> None:
element1 = tool.Ifc.get_entity(wall1)
if not element1:
return
assert element1
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
ifcopenshell.api.geometry.disconnect_path(tool.Ifc.get(), element=element1, connection_type="ATEND")
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
@@ -1207,7 +1251,7 @@ class DumbWallJoiner:
self.set_axis(element1, p1, p2)
self.recreate_wall(element1, wall1)
def join_T(self, wall1, wall2):
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
axis1 = tool.Model.get_wall_axis(wall1)
@@ -1219,8 +1263,7 @@ class DumbWallJoiner:
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
ifcopenshell.api.geometry.connect_path(
tool.Ifc.get(),
related_element=element1,
relating_element=element2,
@@ -1231,7 +1274,7 @@ class DumbWallJoiner:
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
def connect(self, obj1, obj2):
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
wall1 = tool.Ifc.get_entity(obj1)
wall2 = tool.Ifc.get_entity(obj2)
if tool.Ifc.is_moved(obj1):
@@ -516,6 +516,7 @@ class CreateObjectUI:
@classmethod
def draw_type_manager_launcher(cls, context):
ui_context = context.region.type
props = tool.Model.get_model_props()
row = cls.layout.row(align=True)
box = cls.layout.box()
row1 = box.row(align=True)
@@ -716,12 +717,14 @@ class EditObjectUI:
AuthoringData.load(ifc_element_type)
if context.region.type == "TOOL_HEADER":
if context.scene.BIMAggregateProperties.in_aggregate_mode:
aprops = tool.Aggregate.get_aggregate_props()
if aprops.in_aggregate_mode:
layout.label(text=f"Aggregate Mode", icon="EMPTY_AXIS")
row = cls.layout.row(align=True)
op = row.operator("bim.disable_aggregate_mode", text="", icon="X")
op = row.operator("bim.toggle_aggregate_mode_local_view", text="", icon="ZOOM_SELECTED")
if context.scene.BIMNestProperties.in_nest_mode:
nprops = tool.Nest.get_nest_props()
if nprops.in_nest_mode:
layout.label(text=f"Nest Mode", icon="EMPTY_AXIS")
row = cls.layout.row(align=True)
op = row.operator("bim.disable_nest_mode", text="", icon="X")
@@ -909,7 +912,9 @@ class EditObjectUI:
else:
if "LAYER2" in AuthoringData.data["selected_material_usages"]:
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(cls.layout, "Extend To Undersideb", "S_E", "", ui_context)
add_layout_hotkey_operator(
cls.layout, "Extend To Underside", "S_E", bpy.ops.bim.extend_to_underside.__doc__, ui_context
)
if AuthoringData.data["is_flippable_element"]:
cls.draw_flip(ui_context, row)
@@ -959,6 +964,13 @@ class EditObjectUI:
op_text = "Add Void" if ui_context != "TOOL_HEADER" else ""
op_icon = custom_icon_previews["ADD_VOID"].icon_id
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
op = row.operator("bim.add_element", text=op_text, icon_value=op_icon)
op.ifc_product = "IfcFeatureElement"
op.ifc_class = "IfcOpeningElement"
op.skip_dialog = True
if ui_context != "TOOL_HEADER":
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if AuthoringData.data["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]:
@@ -981,12 +993,19 @@ class EditObjectUI:
row = cls.layout.row(align=True)
row.separator()
row.label(text="Align") if ui_context != "TOOL_HEADER" else row
description: str
if AuthoringData.data["active_material_usage"] == "LAYER2":
description = bpy.ops.bim.align_wall.__doc__
else:
description = bpy.ops.bim.align_product.__doc__
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Exterior", "S_X", "", ui_context)
add_layout_hotkey_operator(row, "Exterior", "S_X", description, ui_context)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Centreline", "S_C", "", ui_context)
add_layout_hotkey_operator(row, "Centreline", "S_C", description, ui_context)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Interior", "S_V", "", ui_context)
add_layout_hotkey_operator(row, "Interior", "S_V", description, ui_context)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__, ui_context)
@@ -1334,8 +1353,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.add_boundary()
def hotkey_S_O(self):
if len(bpy.context.selected_objects) == 2:
if len(bpy.context.selected_objects) > 1:
bpy.ops.bim.add_opening()
else:
bpy.ops.bim.add_element(
"INVOKE_DEFAULT", ifc_product="IfcFeatureElement", ifc_class="IfcOpeningElement", skip_dialog=True
)
def hotkey_S_L(self):
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
+15 -4
View File
@@ -20,6 +20,7 @@ import blf
import bpy
import gpu
import ifcopenshell
import ifcopenshell.util.element
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
@@ -131,6 +132,10 @@ class NestDecorator:
return shader
def draw_custom_batch(self, coords, color):
indices = None
if not tool.Blender.validate_shader_batch_data(coords, indices):
return
shader = self.dotted_line_shader()
arc_lengths = [0]
@@ -141,6 +146,7 @@ class NestDecorator:
shader,
"LINE_STRIP",
{"position": coords, "arcLength": arc_lengths},
indices=indices,
)
matrix = bpy.context.region_data.perspective_matrix
@@ -150,13 +156,16 @@ class NestDecorator:
batch.draw(shader)
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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_nest(self, context):
if context.scene.BIMNestProperties.in_nest_mode:
def draw_nest(self, context: bpy.types.Context) -> None:
props = tool.Nest.get_nest_props()
if props.in_nest_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
@@ -243,6 +252,8 @@ class NestModeDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -253,7 +264,7 @@ class NestModeDecorator:
return
region = context.region
rv3d = region.data
props = context.scene.BIMNestProperties
props = tool.Nest.get_nest_props()
aggregate_obj = props.editing_nest
if not aggregate_obj:
@@ -282,7 +293,7 @@ class NestModeDecorator:
def draw_nest_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = context.scene.BIMNestProperties
props = tool.Nest.get_nest_props()
nest_obj = props.editing_nest
if not nest_obj:
return
@@ -158,7 +158,7 @@ class BIM_OT_toggle_nest_mode_local_view(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMNestProperties
props = tool.Nest.get_nest_props()
objs = [o.obj for o in props.editing_objects]
if props.in_nest_mode:
if context.space_data.local_view:
+20 -4
View File
@@ -32,10 +32,11 @@ from bpy.props import (
CollectionProperty,
)
from bonsai.bim.module.nest.decorator import NestDecorator, NestModeDecorator
from typing import TYPE_CHECKING, Union
def update_relating_object(self, context):
def message(self, context):
def update_relating_object(self: "BIMObjectNestProperties", context: bpy.types.Context) -> None:
def message(self, context: bpy.types.Context) -> None:
self.layout.label(text="Please select a valid Ifc Element")
if self.relating_object is None:
@@ -49,15 +50,19 @@ class BIMObjectNestProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_object: PointerProperty(name="Nest Host", type=bpy.types.Object, update=update_relating_object)
if TYPE_CHECKING:
is_editing: bool
relating_object: bpy.types.Object
def update_nest_decorator(self, context):
def update_nest_decorator(self: "BIMNestProperties", context: bpy.types.Context) -> None:
if self.nest_decorator:
NestDecorator.install(bpy.context)
else:
NestDecorator.uninstall()
def update_nest_mode_decorator(self, context):
def update_nest_mode_decorator(self: "BIMNestProperties", context: bpy.types.Context) -> None:
if self.in_nest_mode:
NestModeDecorator.install(bpy.context)
else:
@@ -68,6 +73,10 @@ class Objects(bpy.types.PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
previous_display_type: bpy.props.StringProperty(default="TEXTURED")
if TYPE_CHECKING:
obj: Union[bpy.types.Object, None]
previous_display_type: str
class BIMNestProperties(PropertyGroup):
in_nest_mode: BoolProperty(name="In Edit Mode", update=update_nest_mode_decorator)
@@ -79,3 +88,10 @@ class BIMNestProperties(PropertyGroup):
default=False,
update=update_nest_decorator,
)
if TYPE_CHECKING:
in_nest_mode: bool
editing_nest: Union[bpy.types.Object, None]
editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
not_editing_objects: bpy.types.bpy_prop_collection_idprop[Objects]
nest_decorator: bool
+2 -1
View File
@@ -48,7 +48,8 @@ class BIM_PT_nest(Panel):
if not NestData.is_loaded:
NestData.load()
props = context.active_object.BIMObjectNestProperties
assert (obj := context.active_object)
props = tool.Nest.get_object_nest_props(obj)
if props.is_editing:
row = layout.row(align=True)
+7 -2
View File
@@ -16,10 +16,15 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.tool as tool
from bonsai.bim.helper import prop_with_search
from bonsai.bim.helper import draw_attributes
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.prop import Attribute
class BIM_PT_patch(bpy.types.Panel):
@@ -54,8 +59,8 @@ class BIM_PT_patch(bpy.types.Panel):
row.prop(props, "ifc_patch_output")
row.operator("bim.select_ifc_patch_output", icon="FILE_FOLDER", text="")
def draw_callback_(_, row: bpy.types.UILayout) -> None:
if props.ifc_patch_recipes == "ExtractElements":
def draw_callback_(attribute: Attribute, row: bpy.types.UILayout) -> None:
if props.ifc_patch_recipes == "ExtractElements" and attribute.name == "Query":
row.operator("bim.patch_query_from_selected", text="", icon="EYEDROPPER")
if props.ifc_patch_args_attr:
@@ -68,6 +68,8 @@ class ProjectDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -137,6 +139,8 @@ class ClippingPlaneDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -242,6 +246,8 @@ class MeasureDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -323,7 +329,7 @@ class MeasureDecorator:
all_positions = []
for i in range(len(polyline_points)):
if i < 1 and measure_type == "AREA":
if i < 1 and measure_type == "POLY_AREA":
continue
if i == 0:
continue
@@ -368,7 +374,7 @@ class MeasureDecorator:
polyline_verts = [Vector((p.x, p.y, p.z)) for p in polyline_points]
# Area
if measure_type == "AREA" and polyline_data.area:
if measure_type == "POLY_AREA" and polyline_data.area:
if len(polyline_verts) < 3:
continue
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
@@ -386,7 +392,7 @@ class MeasureDecorator:
blf.draw(self.font_id, text)
# Length
if measure_type in {"POLYLINE", "AREA"}:
if measure_type in {"POLYLINE", "POLY_AREA"}:
if len(polyline_verts) < 3:
continue
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
@@ -449,7 +455,7 @@ class MeasureDecorator:
# Area highlight
if polyline_data:
area = polyline_data.area.split(" ")[0]
if area:
if polyline_data.measurement_type == "POLY_AREA" and area:
if float(area) > 0:
tris = self.calculate_polygon(polyline_verts)["tris"]
self.draw_batch("TRIS", polyline_verts, transparent_color(decorator_color_special), tris)
@@ -1304,6 +1304,9 @@ class LoadLink(bpy.types.Operator):
def execute(self, context):
filepath = Path(tool.Ifc.resolve_uri(self.filepath))
if not filepath.exists():
self.report({"ERROR"}, f"File does not exist: '{filepath}'")
return {"CANCELLED"}
self.filepath_ = filepath
if filepath.suffix.lower().endswith(".blend"):
self.link_blend(filepath)
@@ -2346,7 +2349,9 @@ class RefreshClippingPlanes(bpy.types.Operator):
data = region.data
props = tool.Project.get_project_props()
if not len(props.clipping_planes) and not self.camera:
# See 6452 and 6478.
# if not len(props.clipping_planes) and not self.camera:
if not len(props.clipping_planes):
data.use_clip_planes = False
else:
with bpy.context.temp_override(area=area, region=region):
@@ -2375,13 +2380,14 @@ class RefreshClippingPlanes(bpy.types.Operator):
clip_planes.append(clip_plane)
bm.free()
if self.camera:
normal = self.camera.matrix_world.col[2].to_3d()
normal *= -1
center = self.camera.matrix_world.translation
distance = -center.dot(normal)
clip_plane = (normal.x, normal.y, normal.z, distance)
clip_planes.append(clip_plane)
# See 6452 and 6478.
# if self.camera:
# normal = self.camera.matrix_world.col[2].to_3d()
# normal *= -1
# center = self.camera.matrix_world.translation
# distance = -center.dot(normal)
# clip_plane = (normal.x, normal.y, normal.z, distance)
# clip_planes.append(clip_plane)
clip_planes = cycle(clip_planes)
data.clip_planes = [tuple(next(clip_planes)) for i in range(0, 6)]
+99 -14
View File
@@ -35,6 +35,7 @@ import bonsai.bim.module.root.prop as root_prop
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import get_enum_items, prop_with_search
from mathutils import Vector
from typing import TYPE_CHECKING
class EnableReassignClass(bpy.types.Operator):
@@ -173,24 +174,35 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_class"
bl_label = "Assign IFC Class"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Assign the IFC Class to the selected objects"
bl_description = "Assign the IFC Class to the selected non-ifc objects."
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty()
predefined_type: bpy.props.StringProperty()
userdefined_type: bpy.props.StringProperty()
context_id: bpy.props.IntProperty()
# TODO: is never used?
should_add_representation: bpy.props.BoolProperty(default=True)
ifc_representation_class: bpy.props.StringProperty()
if TYPE_CHECKING:
obj: str
ifc_class: str
predefined_type: str
userdefined_type: str
context_id: int
should_add_representation: bool
ifc_representation_class: str
def _execute(self, context):
ifc_file = tool.Ifc.get()
props = tool.Root.get_root_props()
objects: list[bpy.types.Object] = []
if self.obj:
objects = [bpy.data.objects[self.obj]]
elif objects := context.selected_objects:
pass
elif obj := context.active_object:
objects = [obj]
else:
objects = list(tool.Blender.get_selected_objects())
if not objects:
self.report({"INFO"}, "No objects selected.")
@@ -203,8 +215,20 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
ifc_context = int(props.contexts or "0") or None
if ifc_context:
ifc_context = tool.Ifc.get().by_id(ifc_context)
active_object = context.active_object
schema = ifcopenshell.schema_by_name(ifc_file.schema)
declaration = schema.declaration_by_name(ifc_class)
is_structural = ifcopenshell.util.schema.is_a(declaration, "IfcStructuralItem")
# Manage selection as operator can be called not from UI but using `object` argument.
current_selection = tool.Blender.get_objects_selection(context)
tool.Blender.clear_objects_selection()
for obj in objects:
element = tool.Ifc.get_entity(obj)
if element:
continue
if obj.mode != "OBJECT":
self.report({"ERROR"}, "Object must be in OBJECT mode to assign class")
continue
@@ -218,13 +242,41 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
)
continue
if self.should_add_representation and isinstance(obj.data, bpy.types.Mesh) and obj.data.polygons:
if (
self.should_add_representation
and not is_structural
and isinstance(obj.data, bpy.types.Mesh)
and obj.data.polygons
):
# Export mesh as tesselation.
def ensure_single_user_mesh(mesh: bpy.types.Mesh) -> None:
if mesh.users == 1:
return
obj.select_set(True)
# temp_override is not supported.
bpy.ops.object.make_single_user(
object=True, obdata=True, material=False, animation=False, obdata_animation=False
)
obj.select_set(False)
# Apply geometry.
if obj.modifiers:
ensure_single_user_mesh(obj.data)
with context.temp_override(selected_editable_objects=[obj]):
bpy.ops.object.convert(target="MESH")
# Apply scale.
if obj.scale != (1, 1, 1):
if obj.data.users > 1:
bpy.ops.object.make_single_user(
object=True, obdata=True, material=False, animation=False, obdata_animation=False
)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False)
ensure_single_user_mesh(obj.data)
is_negative = obj.matrix_world.is_negative
with context.temp_override(selected_editable_objects=[obj]):
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False)
# object.transform_apply is losing normals.
if is_negative:
for polygon in obj.data.polygons:
polygon.flip()
if tool.Geometry.mesh_has_loose_geometry(obj.data):
self.report(
{"WARNING"},
@@ -252,6 +304,21 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
should_sync_changes_first=False,
)
else:
def is_representation_supported() -> bool:
# We don't support much topological representations
# and need to prevent assigning IfcShapeRepresentations to structural items.
if is_structural:
return False
data = obj.data
# Is empty mesh.
if isinstance(data, bpy.types.Mesh) and not data.vertices:
return False
# Is empty curve.
if isinstance(data, bpy.types.Curve) and not data.splines:
return False
return True
element = core.assign_class(
tool.Ifc,
tool.Collector,
@@ -259,12 +326,27 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
obj=obj,
ifc_class=ifc_class,
predefined_type=predefined_type,
should_add_representation=self.should_add_representation,
should_add_representation=self.should_add_representation and is_representation_supported(),
context=ifc_context,
ifc_representation_class=self.ifc_representation_class,
)
representation = tool.Geometry.get_active_representation(obj)
if representation:
tool.Geometry.reload_representation(obj)
elif obj.data is not None:
new_obj = tool.Geometry.recreate_object_with_data(obj, None)
context.view_layer.objects.active = active_object
# TODO: reload representation might lead to the object being replaced by object of the other type.
# We probably should track it somehow and keep the original selection.
# Validation selection.
new_selected_objects = list(filter(tool.Blender.is_valid_data_block, current_selection[2]))
active_object = current_selection[1]
if active_object and not tool.Blender.is_valid_data_block(active_object):
active_object = None
current_selection = (current_selection[0], active_object, new_selected_objects)
tool.Blender.set_objects_selection(*current_selection)
class UnlinkObject(bpy.types.Operator, tool.Ifc.Operator):
@@ -360,6 +442,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
is_specific_tool: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
ifc_product: bpy.props.StringProperty(options={"SKIP_SAVE"})
ifc_class: bpy.props.StringProperty(options={"SKIP_SAVE"})
skip_dialog: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
@@ -380,6 +463,8 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
props.ifc_product = self.ifc_product
if self.ifc_class:
props.ifc_class = self.ifc_class
if self.skip_dialog:
return self.execute(context)
return context.window_manager.invoke_props_dialog(self)
def _execute(self, context):
@@ -190,6 +190,7 @@ class BIMRootProperties(PropertyGroup):
if TYPE_CHECKING:
contexts: str
name: str
description: str
ifc_product: str
ifc_class: str
@@ -40,6 +40,7 @@ from bpy.props import (
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, get_args
from typing_extensions import assert_never
class AddFilterGroup(Operator):
@@ -144,6 +145,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
class Search(Operator):
bl_idname = "bim.search"
bl_label = "Search"
bl_description = "Search IFC elements by the provided query and add them to the current selection."
PropertyGroupType = Literal["CsvProperties", "BIMSearchProperties"]
property_group: bpy.props.EnumProperty(
@@ -159,17 +161,18 @@ class Search(Operator):
elif self.property_group == "BIMSearchProperties":
props = tool.Search.get_search_props()
else:
raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.")
assert_never(self.property_group)
results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
)
total_selected = 0
for element in results:
if obj := tool.Ifc.get_object(element):
obj.select_set(True)
self.report({"INFO"}, f"{len(results)} Results")
objs = [obj for e in results if isinstance(obj := tool.Ifc.get_object(e), bpy.types.Object)]
for obj in objs:
tool.Blender.set_object_selection(obj)
if objs:
tool.Blender.set_active_object(objs[0])
self.report({"INFO"}, f"{len(results)} Results.")
return {"FINISHED"}
@@ -659,6 +659,7 @@ class DisableEditingWorkCalendar(bpy.types.Operator):
class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_csv"
bl_label = "Import CSV"
bl_description = "Import work schedule from the provided .csv file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@@ -686,6 +687,7 @@ class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class ImportP6(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_p6"
bl_label = "Import P6"
bl_description = "Import provided .xml P6 file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})
@@ -714,6 +716,7 @@ class ImportP6(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class ImportP6XER(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_p6xer"
bl_label = "Import P6 XER"
bl_description = "Import provided .xer P6 file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xer"
filter_glob: bpy.props.StringProperty(default="*.xer", options={"HIDDEN"})
@@ -742,6 +745,7 @@ class ImportP6XER(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class ImportPP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_pp"
bl_label = "Import Powerproject .pp"
bl_description = "Import provided .pp file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".pp"
filter_glob: bpy.props.StringProperty(default="*.pp", options={"HIDDEN"})
@@ -770,6 +774,7 @@ class ImportPP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_msp"
bl_label = "Import MSP"
bl_description = "Import provided .xml MSP file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})
@@ -798,6 +803,7 @@ class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
class ExportMSP(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_msp"
bl_label = "Export MSP"
bl_description = "Export work schedule as .xml MSP file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})
@@ -831,6 +837,7 @@ class ExportMSP(bpy.types.Operator, ExportHelper):
class ExportP6(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_p6"
bl_label = "Export P6"
bl_description = "Export work schedule as .xml P6 file."
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".xml"
filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})
@@ -24,11 +24,13 @@ classes = (
operator.ContractContainer,
operator.CopyToContainer,
operator.DeleteContainer,
operator.DereferenceFromProvidedStructure,
operator.DereferenceStructure,
operator.DisableEditingContainer,
operator.EnableEditingContainer,
operator.ExpandContainer,
operator.ImportSpatialDecomposition,
operator.ReferenceFromProvidedStructure,
operator.ReferenceStructure,
operator.RemoveContainer,
operator.SelectContainer,
+15 -3
View File
@@ -19,6 +19,7 @@
import bpy
import bonsai.tool as tool
import ifcopenshell.util.element
from typing import Any
def refresh():
@@ -79,9 +80,20 @@ class SpatialData:
return label
@classmethod
def references(cls):
results = ifcopenshell.util.element.get_referenced_structures(tool.Ifc.get_entity(bpy.context.active_object))
return sorted([f"{r.is_a()}/{r.Name or ''}" for r in results])
def references(cls) -> list[dict[str, Any]]:
assert (obj := bpy.context.active_object) and (element := tool.Ifc.get_entity(obj))
results: list[dict[str, Any]] = []
for structure in ifcopenshell.util.element.get_referenced_structures(element):
ifc_class = structure.is_a()
results.append(
{
"id": structure.id(),
"name": f"{ifc_class}/{structure.Name or ''}",
"type": ifc_class,
}
)
results.sort(key=lambda x: x["name"])
return results
@classmethod
def is_directly_contained(cls):
@@ -49,6 +49,8 @@ class GridDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
@@ -73,15 +73,85 @@ class DereferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
core.dereference_structure(tool.Ifc, tool.Spatial, structure=container, element=element)
class ReferenceFromProvidedStructure(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.reference_from_provided_structure"
bl_label = "Reference from Provided Structure"
bl_description = "Reference selected objects from the provided structure.\n\n" "ALT + Click to dereference instead."
bl_options = {"REGISTER", "UNDO"}
structure: bpy.props.IntProperty(options={"SKIP_SAVE"})
dereference: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
if not tool.Blender.get_selected_objects():
cls.poll_message_set("No objects selected.")
return False
return True
def invoke(self, context, event):
self.dereference = event.alt
return self.execute(context)
def _execute(self, context):
ifc_file = tool.Ifc.get()
structure = ifc_file.by_id(self.structure)
objs = tool.Spatial.get_selected_objects_without_containers()
if not objs:
self.report({"INFO"}, "No non-spatial objects are selected.")
return
elements = [e for o in objs if (e := tool.Ifc.get_entity(o))]
for element in elements:
if self.dereference:
core.dereference_structure(tool.Ifc, tool.Spatial, structure=structure, element=element)
else:
core.reference_structure(tool.Ifc, tool.Spatial, structure=structure, element=element)
msg = "dereferenced" if self.dereference else "referenced"
self.report({"INFO"}, f"{len(elements)} elements {msg} from the structure.")
class DereferenceFromProvidedStructure(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.dereference_from_provided_structure"
bl_label = "Dereference from Provided Structure"
bl_description = "Dereference selected objects from the provided structure."
bl_options = {"REGISTER", "UNDO"}
structure: bpy.props.IntProperty(options={"SKIP_SAVE"})
@classmethod
def poll(cls, context):
if not tool.Blender.get_selected_objects():
cls.poll_message_set("No objects selected.")
return False
return True
def _execute(self, context):
ifc_file = tool.Ifc.get()
structure = ifc_file.by_id(self.structure)
objs = tool.Spatial.get_selected_objects_without_containers()
if not objs:
self.report({"INFO"}, "No non-spatial objects are selected.")
return
elements = [e for o in objs if (e := tool.Ifc.get_entity(o))]
for element in elements:
core.dereference_structure(tool.Ifc, tool.Spatial, structure=structure, element=element)
self.report({"INFO"}, f"{len(elements)} elements dereferenced from the structure.")
class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_container"
bl_label = "Assign Container"
bl_description = "\n".join(
(
"Assign the selected objects to the selected container.",
"This will move objects to the container collection in the outliner.",
"ALT + Click to ensure objects are only linked in the container collection",
)
bl_description = (
"Assign the selected objects to the container selected in Spatial Manager.\n\n"
"All elements-parts of an aggregate will be skipped.\n"
"To assign a container, they should be unassigned from an aggregate first.\n\n"
"This will also move objects to the container collection in the outliner.\n"
"ALT + Click to ensure objects are only linked in the container collection"
)
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
@@ -103,12 +173,33 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
pass
else:
return
for element_obj in tool.Blender.get_selected_objects():
objs: list[bpy.types.Object] = []
# In IFC element can be either contained of aggregated,
# tehrefore we skip aggregated elements here to prevent confusion.
# Can't handle it in `poll` since user might just select bunch of elements
# and try to assign a container to them
# and excluding aggregates because of the `poll` failing might get awkward.
skipped_aggregates = 0
for obj in tool.Blender.get_selected_objects():
if not (element := tool.Ifc.get_entity(obj)):
continue
if ifcopenshell.util.element.get_aggregate(element):
skipped_aggregates += 1
continue
objs.append(obj)
for element_obj in objs:
if self.remove_from_other_containers:
for col in element_obj.users_collection[:]:
col.objects.unlink(element_obj)
core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj)
aggregates_msg = ""
if skipped_aggregates:
aggregates_msg = f" {skipped_aggregates} aggregated elements skipped."
self.report({"INFO"}, f"{len(objs)} elements assigned.{aggregates_msg}")
class EnableEditingContainer(bpy.types.Operator):
bl_idname = "bim.enable_editing_container"
+35 -17
View File
@@ -21,7 +21,7 @@ import bpy
from bpy.types import Panel, UIList
from bonsai.bim.module.spatial.data import SpatialData, SpatialDecompositionData
import bonsai.tool as tool
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast, Any
if TYPE_CHECKING:
from bonsai.bim.module.spatial.prop import BIMSpatialDecompositionProperties, BIMContainer, Element
@@ -56,6 +56,8 @@ class BIM_PT_spatial(Panel):
row.operator("bim.assign_container", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_container", icon="CANCEL", text="")
# TODO: deprecate as it's very hard to discover
# containers are not even selectable by default.
if SpatialData.data["selected_containers"]:
row = self.layout.row()
row.label(text=f"{len(SpatialData.data['selected_containers'])} Selected Containers")
@@ -86,14 +88,16 @@ class BIM_PT_spatial(Panel):
row.label(text="No Spatial Container")
row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="")
if references := SpatialData.data["references"]:
if references := cast(list[dict[str, Any]], SpatialData.data["references"]):
self.layout.label(text=f"{len(references)} References:")
else:
self.layout.label(text="No References Found")
for reference in references:
row = self.layout.row()
row.label(text=reference, icon="LINKED")
row.label(text=reference["name"], icon="LINKED")
op = row.operator("bim.dereference_from_provided_structure", icon="X", text="")
op.structure = reference["id"]
class BIM_PT_spatial_decomposition(Panel):
@@ -169,7 +173,14 @@ class BIM_PT_spatial_decomposition(Panel):
if not self.props.active_container:
return
if not self.props.total_elements:
container_has_elements = bool(self.props.total_elements)
if container_has_elements:
row = self.layout.row()
row.label(
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
icon="FILE_3D",
)
else:
row = self.layout.row(align=True)
row.label(text=f"{self.props.active_container.ifc_class} > No Elements", icon="FILE_3D")
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
@@ -177,37 +188,44 @@ class BIM_PT_spatial_decomposition(Panel):
row = self.layout.row(align=True)
row.prop(self.props, "element_mode", text="", icon="FILEBROWSER")
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
return
row = self.layout.row()
row.label(
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
icon="FILE_3D",
)
row = self.layout.row(align=True)
row.alignment = "RIGHT"
op = row.operator("bim.set_element_visibility", icon="FULLSCREEN_EXIT", text="Isolate")
col = row.column()
row_ = col.row(align=True)
row_.enabled = container_has_elements
op = row_.operator("bim.set_element_visibility", icon="FULLSCREEN_EXIT", text="")
op.mode = "ISOLATE"
op.container = ifc_definition_id
op = row.operator("bim.set_element_visibility", icon="HIDE_OFF", text="")
op = row_.operator("bim.set_element_visibility", icon="HIDE_OFF", text="")
op.mode = "SHOW"
op.container = ifc_definition_id
op = row.operator("bim.set_element_visibility", icon="HIDE_ON", text="")
op = row_.operator("bim.set_element_visibility", icon="HIDE_ON", text="")
op.mode = "HIDE"
op.container = ifc_definition_id
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
op = row.operator("bim.select_decomposed_element", icon="OBJECT_DATA", text="")
col = row.column()
row_ = col.row(align=True)
row_.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
row_.operator("bim.reference_from_provided_structure", icon="LINKED", text="").structure = ifc_definition_id
col = row.column()
row_ = col.row(align=True)
row_.enabled = container_has_elements
op = row_.operator("bim.select_decomposed_element", icon="OBJECT_DATA", text="")
if active_element := self.props.active_element:
op.element = active_element.ifc_definition_id
else:
op.element = 0
op = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
op = row_.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
op.container = ifc_definition_id
if not container_has_elements:
return
row = self.layout.row(align=True)
row.prop(self.props, "element_mode", text="", icon="FILEBROWSER")
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
@@ -21,6 +21,7 @@ import numpy as np
import bpy
import gpu
import blf
import bonsai.tool as tool
from bpy.types import SpaceView3D
from gpu_extras.batch import batch_for_shader
from typing import Iterable, Union
@@ -86,6 +87,8 @@ class LoadsDecorator:
shader = info["shader"]
args = info["args"]
indices = info["indices"]
if not tool.Blender.validate_shader_batch_data(args["coord"], indices):
continue
batch = batch_for_shader(shader, "TRIS", args, indices=indices)
matrix = bpy.context.region_data.perspective_matrix
shader.bind()
@@ -303,6 +303,7 @@ class BrowseExternalStyle(bpy.types.Operator, ImportHelper):
def get_data_blocks(self, context):
l = [("", "", "")]
BrowseExternalStyle.data_block_items = l
if self.data_block_type != "0" and os.path.exists(self.filepath) and self.filepath.endswith(".blend"):
with bpy.data.libraries.load(self.filepath) as (data_from, data_to):
objects = getattr(data_from, self.data_block_type)
@@ -73,6 +73,8 @@ class SystemDecorator:
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
return
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)
+7 -6
View File
@@ -118,23 +118,24 @@ class BooleansData:
@classmethod
def load(cls):
# Only called when some object is active.
cls.data = {}
cls.data["total_booleans"] = cls.booleans()
cls.data["manual_booleans"] = cls.manual_booleans()
cls.is_loaded = True
@classmethod
def booleans(cls):
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or bpy.context.active_object
def booleans(cls) -> list[ifcopenshell.entity_instance]:
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
if not (representation := tool.Geometry.get_active_representation(obj)):
return []
return tool.Model.get_booleans(representation=representation)
@classmethod
def manual_booleans(cls):
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or bpy.context.active_object
def manual_booleans(cls) -> list[ifcopenshell.entity_instance]:
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
if not (representation := tool.Geometry.get_active_representation(obj)):
return []
return tool.Model.get_manual_booleans(tool.Ifc.get_entity(obj), representation=representation)
@@ -32,14 +32,16 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Apply Opening"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Apply an Opening object on an Element. "
"The Element and the Opening to be applied should be selected. The order of selection is not important"
"Apply opening objects to an Element.\n\n"
"The Element and the openings to be applied should be selected. The order of selection is not important.\n"
"Opening can be just a Blender mesh object."
)
@classmethod
def poll(cls, context):
if len(context.selected_objects) < 2:
cls.poll_message_set("Select openings and a target element")
return False
return True
def _execute(self, context):
+2 -3
View File
@@ -52,9 +52,8 @@ class BIM_PT_voids(Panel):
if not VoidsData.is_loaded:
VoidsData.load()
if len(context.selected_objects) >= 2:
row = self.layout.row(align=True)
op = row.operator("bim.add_opening", icon="ADD", text="Add Opening")
row = self.layout.row(align=True)
op = row.operator("bim.add_opening", icon="ADD", text="Add Opening")
if VoidsData.data["active_opening"]:
row = self.layout.row()
+8 -1
View File
@@ -620,7 +620,14 @@ class BIMObjectProperties(PropertyGroup):
)
cartesian_point_offset: StringProperty(name="Cartesian Point Offset")
is_reassigning_class: BoolProperty(name="Is Reassigning Class")
is_renaming: BoolProperty(name="Is Renaming", default=False)
is_renaming: BoolProperty(
name="Is Renaming",
description=(
"Flag to ensure object name callback wouldn't write new name to IFC. "
"Automatically reset to `False` after the next callback."
),
default=False,
)
location_checksum: StringProperty(name="Location Checksum")
rotation_checksum: StringProperty(name="Rotation Checksum")
+15 -9
View File
@@ -247,6 +247,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
default=True,
description="If disabled, the toolbar will only load when an IFC model is active",
)
should_use_snap: BoolProperty(
name="Enable Snapping on Startup",
default=True,
description="If enabled, snapping will be enabled on new sessions.\nIt is recommended to keep this `Enabled`",
)
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
tmp_dir: StringProperty(
name="Temporary Directory",
@@ -349,13 +354,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.operator("bim.file_associate", icon="LOCKVIEW_ON")
row.operator("bim.file_unassociate", icon="LOCKVIEW_OFF")
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Interface", self.draw_interface_settings)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Command Paths", self.draw_commands)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Misc.", self.draw_misc_settings)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Model", self.draw_model_settings)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Colors", self.draw_decorator_colors)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Directories", self.draw_directories)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Drawing", self.draw_drawing_settings)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Openings", self.draw_openings_settings)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Other", self.draw_other_settings)
def draw_commands(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "svg2pdf_command")
@@ -365,16 +370,14 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "pdf_command")
layout.prop(self, "spreadsheet_command")
def draw_misc_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
def draw_interface_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "should_hide_empty_props")
layout.prop(self, "should_setup_workspace")
layout.prop(self, "activate_workspace")
layout.prop(self, "should_setup_toolbar")
layout.prop(self, "should_use_snap")
layout.prop(self, "should_play_chaching_sound")
layout.prop(self, "spatial_elements_unselectable")
props = tool.Project.get_project_props()
layout.prop(props, "should_disable_undo_on_save")
layout.prop(props, "should_stream")
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
props = tool.Model.get_model_props()
@@ -426,8 +429,11 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.row().prop(self, "decorator_color_error")
layout.row().prop(self, "decorator_color_background")
def draw_openings_settings(self, layout, context):
def draw_other_settings(self, layout, context):
layout.prop(self, "opening_focus_opacity")
props = tool.Project.get_project_props()
layout.prop(props, "should_disable_undo_on_save")
layout.prop(props, "should_stream")
# Scene panel groups
@@ -1262,8 +1268,8 @@ class BIM_PT_decorators_overlay(Panel):
overlay = view.overlay
georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
aggregate_props = tool.Aggregate.get_aggregate_props()
nest_props = tool.Nest.get_nest_props()
model_props = tool.Model.get_model_props()
display_all = overlay.show_overlays
+30 -7
View File
@@ -16,11 +16,22 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bonsai.core.tool as tool
from typing import Literal, Iterable
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Literal, Iterable
if TYPE_CHECKING:
import bpy
import ifcopenshell
import bonsai.tool as tool
from mathutils import Vector
from bonsai.bim.module.model.wall import DumbWallJoiner
def unjoin_walls(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None:
def unjoin_walls(
ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner: DumbWallJoiner, model: tool.Model
) -> None:
"""Unjoin selected walls."""
for obj in blender.get_selected_objects():
if not (element := ifc.get_entity(obj)) or model.get_usage_type(element) != "LAYER2":
continue
@@ -31,8 +42,14 @@ def unjoin_walls(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry,
def extend_walls(
ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model, target
ifc: tool.Ifc,
blender: tool.Blender,
geometry: tool.Geometry,
joiner: DumbWallJoiner,
model: tool.Model,
target: Vector,
) -> None:
"""Extend selected walls to the target."""
for obj in blender.get_selected_objects():
if not (element := ifc.get_entity(obj)) or model.get_usage_type(element) != "LAYER2":
continue
@@ -44,7 +61,7 @@ def join_walls_LV(
ifc: tool.Ifc,
blender: tool.Blender,
geometry: tool.Geometry,
joiner,
joiner: DumbWallJoiner,
model: tool.Model,
join_type: Literal["L", "V"] = "L",
) -> None:
@@ -66,7 +83,11 @@ def join_walls_LV(
def extend_wall_to_slab(
ifc: tool.Ifc, geometry: tool.Geometry, model: tool.Model, slab_obj, wall_objs: Iterable
ifc: tool.Ifc,
geometry: tool.Geometry,
model: tool.Model,
slab_obj: bpy.types.Object,
wall_objs: list[bpy.types.Object],
) -> None:
if not (clip := model.get_slab_clipping_bmesh(slab_obj)):
return # Nothing to clip?
@@ -80,7 +101,9 @@ def extend_wall_to_slab(
model.reload_body_representation(wall_objs)
def join_walls_TZ(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None:
def join_walls_TZ(
ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner: DumbWallJoiner, model: tool.Model
) -> None:
selected_objs = [
o
for o in blender.get_selected_objects()
+30 -9
View File
@@ -25,15 +25,21 @@ if TYPE_CHECKING:
import bonsai.tool as tool
def enable_editing_nest(nest, obj=None):
def enable_editing_nest(nest: tool.Nest, obj: bpy.types.Object) -> None:
nest.enable_editing(obj)
def disable_editing_nest(nest, obj=None):
def disable_editing_nest(nest: tool.Nest, obj: bpy.types.Object) -> None:
nest.disable_editing(obj)
def assign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
def assign_object(
ifc: tool.Ifc,
nest: tool.Nest,
collector: tool.Collector,
relating_obj: bpy.types.Object,
related_obj: bpy.types.Object,
) -> Union[ifcopenshell.entity_instance, None]:
if not nest.can_nest(relating_obj, related_obj):
return
rel = ifc.run(
@@ -47,14 +53,22 @@ def assign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
return rel
def unassign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
def unassign_object(
ifc: tool.Ifc,
nest: tool.Nest,
collector: tool.Collector,
relating_obj: bpy.types.Object,
related_obj: bpy.types.Object,
) -> Union[ifcopenshell.entity_instance, None]:
related_element = ifc.get_entity(related_obj)
assert related_element
container = nest.get_container(related_element)
# TODO: this branch is never used?
if not relating_obj:
relating_element = nest.get_relating_object(related_element)
if related_element:
relating_obj = ifc.get_object(relating_element)
if relating_obj:
else:
ifc.run("nest.unassign_object", related_objects=[related_element])
if container:
ifc.run("spatial.assign_container", products=[related_element], relating_structure=container)
@@ -62,21 +76,28 @@ def unassign_object(ifc, nest, collector, relating_obj=None, related_obj=None):
collector.assign(related_obj)
def add_part_to_object(ifc, nest, collector, blender, obj, part_class, part_name=None):
def add_part_to_object(
ifc: tool.Ifc,
nest: tool.Nest,
collector: tool.Collector,
blender: tool.Blender,
obj: bpy.types.Object,
part_class: str,
part_name: str,
) -> None:
part_obj = blender.create_ifc_object(ifc_class=part_class, name=part_name)
assign_object(ifc, nest, collector, relating_obj=obj, related_obj=part_obj)
blender.set_active_object(obj)
def enable_nest_mode(
nest: tool.Nest,
obj: bpy.types.Object,
):
) -> None:
if nest.get_nest_mode():
disable_nest_mode(nest)
nest.enable_nest_mode(obj)
def disable_nest_mode(nest: tool.Nest):
def disable_nest_mode(nest: tool.Nest) -> None:
nest.disable_nest_mode()
+1 -1
View File
@@ -558,7 +558,7 @@ class Misc:
def run_root_copy_class(cls, obj=None): pass
def scale_object_to_height(cls, obj, height): pass
def set_object_origin_to_bottom(cls, obj): pass
def split_objects_with_cutter(cls, objs, cutter): pass
def boolean_objects_with_cutter(cls, objs, cutter): pass
@interface
+23 -8
View File
@@ -16,15 +16,27 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.core.tool
import bonsai.tool as tool
import ifcopenshell.util.element
import ifcopenshell.util.representation
from typing import Union
from typing import Union, TYPE_CHECKING, Literal
if TYPE_CHECKING:
from bonsai.bim.module.aggregate.prop import BIMAggregateProperties, BIMObjectAggregateProperties
class Aggregate(bonsai.core.tool.Aggregate):
@classmethod
def get_aggregate_props(cls) -> BIMAggregateProperties:
return bpy.context.scene.BIMAggregateProperties
@classmethod
def get_object_aggregate_props(cls, obj: bpy.types.Object) -> BIMObjectAggregateProperties:
return obj.BIMObjectAggregateProperties
@classmethod
def can_aggregate(cls, relating_obj: bpy.types.Object, related_obj: bpy.types.Object) -> bool:
relating_object = tool.Ifc.get_entity(relating_obj)
@@ -56,11 +68,13 @@ class Aggregate(bonsai.core.tool.Aggregate):
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
obj.BIMObjectAggregateProperties.is_editing = False
props = cls.get_object_aggregate_props(obj)
props.is_editing = False
@classmethod
def enable_editing(cls, obj: bpy.types.Object) -> None:
obj.BIMObjectAggregateProperties.is_editing = True
props = cls.get_object_aggregate_props(obj)
props.is_editing = True
@classmethod
def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
@@ -86,13 +100,14 @@ class Aggregate(bonsai.core.tool.Aggregate):
return parts
@classmethod
def get_aggregate_mode(cls):
return bpy.context.scene.BIMAggregateProperties.in_aggregate_mode
def get_aggregate_mode(cls) -> bool:
props = cls.get_aggregate_props()
return props.in_aggregate_mode
@classmethod
def enable_aggregate_mode(cls, active_object: bpy.types.Object):
def enable_aggregate_mode(cls, active_object: bpy.types.Object) -> set[Literal["FINISHED"]]:
context = bpy.context
props = context.scene.BIMAggregateProperties
props = cls.get_aggregate_props()
element = tool.Ifc.get_entity(active_object)
if not element:
@@ -131,7 +146,7 @@ class Aggregate(bonsai.core.tool.Aggregate):
@classmethod
def disable_aggregate_mode(cls):
context = bpy.context
props = context.scene.BIMAggregateProperties
props = cls.get_aggregate_props()
for obj_prop in props.not_editing_objects:
obj = obj_prop.obj
if not obj:
+31
View File
@@ -43,6 +43,7 @@ from typing_extensions import assert_never
if TYPE_CHECKING:
from bonsai.bim.prop import BIMProperties, BIMObjectProperties
from bonsai.bim.module.attribute.prop import BIMAttributeProperties
from bonsai.bim.module.csv.prop import CsvProperties
from bonsai.bim.module.diff.prop import DiffProperties
@@ -113,11 +114,13 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def set_active_object(cls, obj: bpy.types.Object) -> None:
"""Set active object and select it."""
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
@classmethod
def clear_active_object(cls) -> None:
"""Clear active object, object is not unselected."""
bpy.context.view_layer.objects.active = None
@classmethod
@@ -655,6 +658,12 @@ class Blender(bonsai.core.tool.Blender):
if active_object:
active_object.select_set(True)
@classmethod
def clear_objects_selection(cls) -> None:
"""Clear objects selection and active object."""
bpy.ops.object.select_all(action="DESELECT")
cls.clear_active_object()
@classmethod
def get_enum_safe(cls, props: bpy.types.PropertyGroup, prop_name: str) -> Union[str, None]:
"""method created for readibility and to avoid console warnings like
@@ -1629,6 +1638,10 @@ class Blender(bonsai.core.tool.Blender):
def get_object_bim_props(cls, obj: bpy.types.Object) -> BIMObjectProperties:
return obj.BIMObjectProperties
@classmethod
def get_object_attribute_props(cls, obj: bpy.types.Object) -> BIMAttributeProperties:
return obj.BIMAttributeProperties
@classmethod
def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
if isinstance(obj, bpy.types.Object):
@@ -1668,3 +1681,21 @@ class Blender(bonsai.core.tool.Blender):
unit_scale = 0.3048
return unit_scale
@classmethod
def validate_shader_batch_data(cls, pos: Any, indices: Optional[Any]) -> bool:
"""Validate shader batch data.
If method returns ``False``, then drawing for this batch should be skipped.
Should be used always before running ``batch.draw(shader)``
Important because in Blender 4.4.0 on Mac passing an empty list
as ``indices`` is causing a crash.
See https://projects.blender.org/blender/blender/issues/136831
"""
# Checking `pos` is not critical but we keep it
# to ensure batch data is always validated to avoid crashes.
if len(pos) == 0 or (indices is not None and len(indices) == 0):
return False
return True
+1 -1
View File
@@ -456,7 +456,7 @@ class Brick(bonsai.core.tool.Brick):
@classmethod
def add_namespace(cls, alias: str, uri: str) -> None:
assert BrickStore.graph
assert BrickStore.graph is not None
BrickStore.graph.bind(alias, Namespace(uri))
BrickStore.load_namespaces()
+1 -1
View File
@@ -64,7 +64,7 @@ class Debug(bonsai.core.tool.Debug):
pass
@classmethod
def debug_bmesh(cls, bm: bpy.types.BMesh, name: str = "Debug") -> bpy.types.Object:
def debug_bmesh(cls, bm: bmesh.types.BMesh, name: str = "Debug") -> bpy.types.Object:
mesh = bpy.data.meshes.new("Debug")
bm.to_mesh(mesh)
obj = bpy.data.objects.new(name, mesh)
+16 -8
View File
@@ -202,6 +202,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def delete_ifc_item(cls, obj: bpy.types.Object) -> None:
"""Delete IfcRepresentationItem's Object."""
props = tool.Geometry.get_geometry_props()
if len(props.item_objs) == 1:
return
@@ -212,7 +213,9 @@ class Geometry(bonsai.core.tool.Geometry):
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
item = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id)
cls.remove_representation_item(item)
rep_obj = props.representation_obj
assert (rep_obj := props.representation_obj) and (rep_element := tool.Ifc.get_entity(rep_obj))
cls.remove_representation_item(item, rep_element)
cls.reload_representation(props.representation_obj)
bpy.data.objects.remove(obj)
@@ -1344,12 +1347,19 @@ class Geometry(bonsai.core.tool.Geometry):
)
@classmethod
def remove_representation_item(cls, representation_item: ifcopenshell.entity_instance) -> None:
def remove_representation_item(
cls, representation_item: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance
) -> None:
"""Remove IfcRepresentationItem.
:param representation_item: item to remove.
:param element: item's element. Is used to unmark manual booleans.
"""
# NOTE: we assume it's not the last representation item
# otherwise we probably would need to remove representation too
# NOTE: a lot of shared code with `geometry.remove_representation`
ifc_file = tool.Ifc.get()
shape_aspects = []
shape_aspects: list[ifcopenshell.entity_instance] = []
consider_inverses = []
styled_item, colour, texture, layer = None, None, None, None
@@ -1366,7 +1376,7 @@ class Geometry(bonsai.core.tool.Geometry):
[consider_inverses.append(texture := t) for t in getattr(representation_item, "HasTextures", [])]
representation = None
boolean_results_to_remove = set()
boolean_results_to_remove: set[ifcopenshell.entity_instance] = set()
for inverse in ifc_file.get_inverse(representation_item):
if inverse.is_a("IfcShapeRepresentation"):
if inverse.OfShapeAspect:
@@ -1409,11 +1419,9 @@ class Geometry(bonsai.core.tool.Geometry):
also_consider = list(consider_inverses)
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
props = tool.Geometry.get_geometry_props()
rep_element = tool.Ifc.get_entity(props.representation_obj)
tool.Model.unmark_manual_booleans(rep_element, [b.id() for b in boolean_results_to_remove])
tool.Model.unmark_manual_booleans(element, [b.id() for b in boolean_results_to_remove])
for boolean_result in boolean_results_to_remove:
cls.remove_representation_item(boolean_result)
cls.remove_representation_item(boolean_result, element)
@classmethod
def create_shape_aspect(
+1
View File
@@ -799,6 +799,7 @@ class Loader(bonsai.core.tool.Loader):
# TODO implement non cartesian point vertices.
if not point.is_a("IfcCartesianPoint"):
print(f"WARNING. Unsupported point type for IfcStructuralPointConnection: {point}.")
return
ifc_file = tool.Ifc.get()
+154 -9
View File
@@ -34,6 +34,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
import bonsai.core.geometry
import bonsai.core.tool
@@ -61,6 +62,7 @@ if TYPE_CHECKING:
BIMWindowProperties,
BIMStairProperties,
BIMRailingProperties,
BIMExternalParametricGeometryProperties,
)
@@ -93,6 +95,10 @@ class Model(bonsai.core.tool.Model):
def get_array_props(cls, obj: bpy.types.Object) -> BIMArrayProperties:
return obj.BIMArrayProperties
@classmethod
def get_epg_props(cls, obj: bpy.types.Object) -> BIMExternalParametricGeometryProperties:
return obj.BIMExternalParametricGeometryProperties
@classmethod
def convert_si_to_unit(cls, value: T) -> T:
if isinstance(value, (tuple, list)):
@@ -687,8 +693,12 @@ class Model(bonsai.core.tool.Model):
@classmethod
def unmark_manual_booleans(cls, element: ifcopenshell.entity_instance, boolean_ids: list[int]) -> None:
# NOTE: we use use boolean_ids instead of boolean entities
# so it will be possible to unmark manual booleans after they already was deleted
"""Remove boolean ids from ``element``'s 'BBIM_Boolean' pset.
:param boolean_ids: List of boolean ids to remove.
Ids are used instead of entities to make it possible to unmark already removed booleans.
Provided ids may not be marked as manual booleans previously.
"""
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Boolean")
if not pset:
return
@@ -1133,6 +1143,27 @@ class Model(bonsai.core.tool.Model):
AuthoringData.type_thumbnails[element.id()] = obj.preview.icon_id
@classmethod
def get_selected_ifc_objects(cls) -> list[bpy.types.Object]:
return [obj for obj in tool.Blender.get_selected_objects() if tool.Ifc.get_entity(obj)]
@classmethod
def has_selected_ifc_objects(cls) -> bool:
return any(tool.Ifc.get_entity(obj) for obj in tool.Blender.get_selected_objects())
@classmethod
def get_selected_mesh_objects(cls) -> list[bpy.types.Object]:
objects = tool.Blender.get_selected_objects()
return [obj for obj in objects if obj.type == "MESH"]
@classmethod
def get_selected_mesh_ifc_objects(cls) -> list[bpy.types.Object]:
return [obj for obj in tool.Model.get_selected_mesh_objects() if tool.Ifc.get_entity(obj)]
@classmethod
def has_selected_mesh_ifc_objects(cls) -> bool:
return any(tool.Ifc.get_entity(obj) for obj in tool.Model.get_selected_mesh_objects())
BBIM_PARAMETRIC_PSETS = (
"BBIM_Window",
"BBIM_Door",
@@ -1570,15 +1601,13 @@ class Model(bonsai.core.tool.Model):
return occurrences
@classmethod
def add_body_representation(cls, obj: bpy.types.Object) -> None:
def add_representation(cls, obj: bpy.types.Object, context: ifcopenshell.entity_instance) -> None:
ifc_file = tool.Ifc.get()
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
assert body
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
representation = ifcopenshell.api.geometry.add_representation(
ifc_file,
context=body,
context=context,
blender_object=obj,
geometry=mesh,
coordinate_offset=tool.Geometry.get_cartesian_point_offset(obj),
@@ -1590,7 +1619,14 @@ class Model(bonsai.core.tool.Model):
profile_set_usage=None,
)
assert representation
tool.Model.replace_object_ifc_representation(body, obj, representation)
tool.Model.replace_object_ifc_representation(context, obj, representation)
@classmethod
def add_body_representation(cls, obj: bpy.types.Object) -> None:
ifc_file = tool.Ifc.get()
body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
assert body
cls.add_representation(obj, body)
@classmethod
def auto_detect_annotation_fill_area(cls, obj: bpy.types.Object, mesh: bpy.types.Mesh) -> dict | None:
@@ -2100,7 +2136,7 @@ class Model(bonsai.core.tool.Model):
op.material_id = material_id_int
@classmethod
def get_slab_clipping_bmesh(cls, obj: bpy.types.Object) -> bpy.types.BMesh | None:
def get_slab_clipping_bmesh(cls, obj: bpy.types.Object) -> bmesh.types.BMesh | None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bm = bmesh.new()
@@ -2132,7 +2168,7 @@ class Model(bonsai.core.tool.Model):
return clipping_bm # clipping_bm is in project units
@classmethod
def clip_wall_to_slab(cls, wall: ifcopenshell.entity_instance, clipping_bm: bpy.types.BMesh) -> None:
def clip_wall_to_slab(cls, wall: ifcopenshell.entity_instance, clipping_bm: bmesh.types.BMesh) -> None:
matrix_i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall.ObjectPlacement))
bm = clipping_bm.copy()
bmesh.ops.transform(bm, matrix=Matrix(matrix_i.tolist()), verts=bm.verts)
@@ -2180,3 +2216,112 @@ class Model(bonsai.core.tool.Model):
ifcopenshell.api.geometry.connect_element(
tool.Ifc.get(), relating_element=slab, related_element=wall, description="TOP"
)
@classmethod
def get_epg_modifier(cls, obj: bpy.types.Object) -> Union[bpy.types.NodesModifier, None]:
for m in obj.modifiers:
if m.type == "NODES" and m.name.startswith("BBIM_EPG"):
assert isinstance(m, bpy.types.NodesModifier)
return m
return None
@classmethod
def setup_external_nodes(
cls, modifier: bpy.types.NodesModifier, external_nodes: bpy.types.GeometryNodeTree
) -> None:
bbim_nodes = modifier.node_group
if bbim_nodes is not None:
# Just assign modifier to existing node group.
assert isinstance(bbim_nodes, bpy.types.GeometryNodeTree)
group_node = next(n for n in bbim_nodes.nodes if n.type == "GROUP")
assert isinstance(group_node, bpy.types.GeometryNodeGroup)
group_node.node_tree = external_nodes
return
# Create a new node group.
bbim_nodes = bpy.data.node_groups.new(type="GeometryNodeTree", name="BBIM_EPG")
modifier.node_group = bbim_nodes
assert isinstance(bbim_nodes, bpy.types.GeometryNodeTree)
bbim_nodes_interface = bbim_nodes.interface
assert bbim_nodes_interface
geometry_socket_2 = bbim_nodes_interface.new_socket(
name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry"
)
geometry_socket_2.attribute_domain = "POINT"
# Socket Geometry
geometry_socket_3 = bbim_nodes_interface.new_socket(
name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry"
)
geometry_socket_3.attribute_domain = "POINT"
# Socket Socket
socket_socket = bbim_nodes_interface.new_socket(name="Socket", in_out="INPUT", socket_type="NodeSocketGeometry")
socket_socket.attribute_domain = "POINT"
# Initialize bbim_epg nodes.
# Node Group Input.
group_input_1 = bbim_nodes.nodes.new("NodeGroupInput")
assert isinstance(group_input_1, bpy.types.NodeGroupInput)
group_input_1.name = "Group Input"
# Node Group Output.
group_output_1 = bbim_nodes.nodes.new("NodeGroupOutput")
assert isinstance(group_output_1, bpy.types.NodeGroupOutput)
group_output_1.name = "Group Output"
group_output_1.is_active_output = True
# Node Group.
group = bbim_nodes.nodes.new("GeometryNodeGroup")
assert isinstance(group, bpy.types.GeometryNodeGroup)
group.name = "Group"
group.node_tree = external_nodes
# Set locations
group_input_1.location = (-345.0525817871094, 65.80108642578125)
group_output_1.location = (200.0, 0.0)
group.location = (-83.36784362792969, 80.47976684570312)
# Set dimensions
group_input_1.width, group_input_1.height = 140.0, 100.0
group_output_1.width, group_output_1.height = 140.0, 100.0
group.width, group.height = 179.41021728515625, 100.0
# Initialize bbim_epg links.
# group.Geometry -> group_output_1.Geometry
bbim_nodes.links.new(group.outputs[0], group_output_1.inputs[0])
# group_input_1.Geometry -> group.Geometry
bbim_nodes.links.new(group_input_1.outputs[0], group.inputs[0])
@classmethod
def setup_parametric_geometry(cls, obj: bpy.types.Object) -> None:
props = cls.get_epg_props(obj)
external_nodes = props.geo_nodes
assert external_nodes
if not (modifier := cls.get_epg_modifier(obj)):
modifier = obj.modifiers.new(type="NODES", name="BBIM_EPG")
assert isinstance(modifier, bpy.types.NodesModifier)
modifier.show_viewport = True
cls.setup_external_nodes(modifier, external_nodes)
@classmethod
def clean_up_parametric_geometry(cls, obj: bpy.types.Object) -> None:
modifier = tool.Model.get_epg_modifier(obj)
assert modifier
node_tree = modifier.node_group
assert node_tree
bpy.data.node_groups.remove(node_tree)
obj.modifiers.clear()
@classmethod
def get_parametric_geometry_inputs(cls, modifier: bpy.types.NodesModifier) -> list[bpy.types.NodeSocket]:
node_group = modifier.node_group
assert isinstance(node_group, bpy.types.GeometryNodeTree)
group_node = next(n for n in node_group.nodes if n.type == "GROUP")
assert isinstance(group_node, bpy.types.GeometryNodeGroup)
return [s for s in group_node.inputs if s.type != "GEOMETRY"]
+27 -9
View File
@@ -16,13 +16,26 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.core.tool
import bonsai.tool as tool
import ifcopenshell.util.element
from typing import TYPE_CHECKING, Union
if TYPE_CHECKING:
from bonsai.bim.module.nest.prop import BIMNestProperties, BIMObjectNestProperties
class Nest(bonsai.core.tool.Nest):
@classmethod
def get_nest_props(cls) -> BIMNestProperties:
return bpy.context.scene.BIMNestProperties
@classmethod
def get_object_nest_props(cls, obj: bpy.types.Object) -> BIMObjectNestProperties:
return obj.BIMObjectNestProperties
@classmethod
def can_nest(cls, relating_obj, related_obj):
relating_object = tool.Ifc.get_entity(relating_obj)
@@ -34,19 +47,23 @@ class Nest(bonsai.core.tool.Nest):
return False
@classmethod
def disable_editing(cls, obj):
obj.BIMObjectNestProperties.is_editing = False
def disable_editing(cls, obj: bpy.types.Object) -> None:
props = cls.get_object_nest_props(obj)
props.is_editing = False
@classmethod
def enable_editing(cls, obj):
obj.BIMObjectNestProperties.is_editing = True
def enable_editing(cls, obj: bpy.types.Object) -> None:
props = cls.get_object_nest_props(obj)
props.is_editing = True
@classmethod
def get_container(cls, element):
def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_container(element)
@classmethod
def get_relating_object(cls, related_element):
def get_relating_object(
cls, related_element: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_nest(related_element)
@classmethod
@@ -61,13 +78,14 @@ class Nest(bonsai.core.tool.Nest):
return components
@classmethod
def get_nest_mode(cls):
return bpy.context.scene.BIMNestProperties.in_nest_mode
def get_nest_mode(cls) -> bool:
props = cls.get_nest_props()
return props.in_nest_mode
@classmethod
def enable_nest_mode(cls, active_object: bpy.types.Object):
context = bpy.context
props = context.scene.BIMNestProperties
props = cls.get_nest_props()
element = tool.Ifc.get_entity(active_object)
if not element:
+51 -22
View File
@@ -52,15 +52,43 @@ class Raycast(bonsai.core.tool.Raycast):
else: # Usual object
obj = dup.object
all_objs.append(obj)
return all_objs
visible_objs = []
for obj in all_objs:
if obj.type in {"MESH", "EMPTY", "CURVE"} and (
obj.visible_in_viewport_get(bpy.context.space_data) or obj.library
): # Check for local view and local collections for this viewport and object
visible_objs.append(obj)
return visible_objs
@classmethod
def get_on_screen_2d_bounding_boxes(cls, context: bpy.types.Context, obj: bpy.types.Object):
def get_on_screen_2d_bounding_boxes(
cls, context: bpy.types.Context, obj: bpy.types.Object
) -> Union[tuple[bpy.types.Object, list[float]], None]:
rv3d = context.region_data
view_location = rv3d.view_matrix.inverted().translation
obj_matrix = obj.matrix_world.copy()
bbox = [obj_matrix @ Vector(v) for v in obj.bound_box]
transposed_bbox = []
bbox_2d = []
transposed_bbox: list[Vector] = []
bbox_2d: list[float] = []
assert context.region
assert isinstance(context.space_data, bpy.types.SpaceView3D)
assert context.space_data.region_3d
# Do not include objects too far from camera view
if rv3d.view_perspective == "PERSP":
threshold = 200
min_distance = float("inf")
closest_distance: float = None
for point in bbox:
distance = (view_location - point).length
if distance < min_distance:
min_distance = distance
closest_distance = distance
if closest_distance > threshold:
return None
for v in bbox:
coord_2d = view3d_utils.location_3d_to_region_2d(context.region, context.space_data.region_3d, v)
@@ -68,18 +96,24 @@ class Raycast(bonsai.core.tool.Raycast):
transposed_bbox.append(coord_2d)
region = context.region
borders = (region.width, region.height)
borders = (0, region.width, 0, region.height)
for i, axis in enumerate(zip(*transposed_bbox)):
axis: tuple[float, ...]
min_point = min(axis)
max_point = max(axis)
if min_point == max_point:
min_point = 0
if min_point < borders[i] and max_point > 0:
bbox_2d.extend([min_point, max_point])
else:
return (obj, None)
bbox_2d.extend([min_point, max_point])
return (obj, bbox_2d)
if len(bbox_2d) == 0:
return None
# AABB
if (
bbox_2d[0] <= borders[1]
and bbox_2d[1] >= borders[0]
and bbox_2d[2] <= borders[3]
and bbox_2d[3] >= borders[2]
):
return (obj, bbox_2d)
return None
@classmethod
def intersect_mouse_2d_bounding_box(cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float]):
@@ -180,9 +214,7 @@ class Raycast(bonsai.core.tool.Raycast):
# Makes the snapping point more or less sticky than others
# It changes the distance and affects how the snapping point are sorted
# We multiply by the increment snap which is based on the viewport zoom
snap_threshold = 10 * tool.Snap.get_increment_snap_value(bpy.context)
if face:
snap_threshold = tool.Snap.get_increment_snap_value(bpy.context)
snap_threshold = rv3d.view_distance / 100
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
@@ -275,7 +307,7 @@ class Raycast(bonsai.core.tool.Raycast):
rv3d = context.region_data
mouse_pos = event.mouse_region_x, event.mouse_region_y
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
snap_threshold = tool.Snap.get_increment_snap_value(bpy.context)
snap_threshold = rv3d.view_distance / 100
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
@@ -352,7 +384,7 @@ class Raycast(bonsai.core.tool.Raycast):
rv3d = context.region_data
mouse_pos = event.mouse_region_x, event.mouse_region_y
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
snap_threshold = tool.Snap.get_increment_snap_value(bpy.context)
snap_threshold = rv3d.view_distance / 100
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
@@ -386,12 +418,9 @@ class Raycast(bonsai.core.tool.Raycast):
mouse_pos = event.mouse_region_x, event.mouse_region_y
objs_to_raycast = []
for obj, bbox_2d in objs_2d_bbox:
if obj.type in {"MESH", "EMPTY", "CURVE"} and bbox_2d:
if bbox_2d:
if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d):
if (
obj.visible_in_viewport_get(bpy.context.space_data) or obj.library
): # Check for local view and local collections for this viewport and object
objs_to_raycast.append(obj)
objs_to_raycast.append(obj)
return objs_to_raycast
@classmethod
+6 -4
View File
@@ -238,13 +238,13 @@ class Root(bonsai.core.tool.Root):
@classmethod
def is_in_aggregate_mode(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
props = bpy.context.scene.BIMAggregateProperties
props = tool.Aggregate.get_aggregate_props()
if props.editing_aggregate and props.in_aggregate_mode:
return tool.Ifc.get_entity(props.editing_aggregate)
@classmethod
def is_in_nest_mode(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
props = bpy.context.scene.BIMNestProperties
props = tool.Nest.get_nest_props()
if props.editing_nest and props.in_nest_mode:
return tool.Ifc.get_entity(props.editing_nest)
@@ -419,7 +419,9 @@ class Root(bonsai.core.tool.Root):
name = tool.Loader.get_name(element)
if obj.name != name:
props = tool.Blender.get_object_bim_props(obj)
props.is_renaming = True
# If it's not an IFC object, then it doesn't have a name callback.
# So, we need to protect it writing to IFC.
props.is_renaming = bool(tool.Ifc.get_entity(obj))
obj.name = name # The handler will trigger, and reset is_renaming to False
@classmethod
@@ -428,7 +430,7 @@ class Root(bonsai.core.tool.Root):
if material.name == name:
return
msprops = tool.Style.get_material_style_props(material)
msprops.is_renaming = True
msprops.is_renaming = bool(tool.Ifc.get_entity(material))
material.name = name # The handler will trigger, and reset is_renaming to False.
@classmethod
+18 -12
View File
@@ -25,7 +25,7 @@ import math
import mathutils
from mathutils import Matrix, Vector
from lark import Lark, Transformer
from typing import Union
from typing import Union, Any
class Snap(bonsai.core.tool.Snap):
@@ -145,12 +145,12 @@ class Snap(bonsai.core.tool.Snap):
@classmethod
def snap_on_axis(cls, intersection, tool_state):
def create_axis_line_data(rot_mat, origin):
def create_axis_line_data(rot_mat: Matrix, origin: Vector) -> tuple[Vector, Vector]:
length = 1000
direction = Vector((1, 0, 0))
if tool_state.plane_method == "YZ" or (not tool_state.plane_method and tool_state.axis_method == "Z"):
direction = Vector((0, 0, 1))
rot_dir = rot_mat.inverted() @ direction
rot_dir: Vector = rot_mat.inverted() @ direction
start = origin + rot_dir * length
end = origin - rot_dir * length
@@ -280,10 +280,16 @@ class Snap(bonsai.core.tool.Snap):
return sorted_intersections
@classmethod
def detect_snapping_points(cls, context: bpy.types.Context, event: bpy.types.Event, objs_2d_bbox, tool_state):
def detect_snapping_points(
cls,
context: bpy.types.Context,
event: bpy.types.Event,
objs_2d_bbox: list[tuple[bpy.types.Object, list[float]]],
tool_state: tool.Polyline.ToolState,
) -> list[dict[str, Any]]:
rv3d = context.region_data
space = context.space_data
detected_snaps = []
detected_snaps: list[dict[str, Any]] = []
def select_plane_method():
if not last_polyline_point:
@@ -483,17 +489,17 @@ class Snap(bonsai.core.tool.Snap):
def sort_points_by_weighted_distance(snapping_points):
for snap in snapping_points:
weight_factor = 100 * cls.get_increment_snap_value(context)
zoom_factor = bpy.context.region_data.view_distance
if snap["type"] == "Vertex":
snap["distance"] *= weight_factor / 12
if snap["type"] == "Edge":
snap["distance"] *= weight_factor
snap["distance"] *= zoom_factor / 10
if snap["type"] == "Edge Center":
snap["distance"] *= weight_factor / 5
snap["distance"] *= zoom_factor / 8
if snap["type"] == "Edge Intersection":
snap["distance"] *= weight_factor / 10
snap["distance"] *= zoom_factor / 5
if snap["type"] == "Edge":
snap["distance"] *= zoom_factor
if snap["type"] in ["Plane", "Axis", "Face"]:
snap["distance"] = weight_factor / 50
snap["distance"] *= zoom_factor
return sorted(snapping_points, key=lambda x: x["distance"])
snaps_by_group = filter_snapping_points_by_group(detected_snaps)
+39
View File
@@ -172,3 +172,42 @@ class Structural(bonsai.core.tool.Structural):
new = props.structural_analysis_models.add()
new.ifc_definition_id = ifc_definition_id
new.name = model["Name"] or "Unnamed"
@classmethod
def get_vertex_representation(
cls, product: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""
:param product: IfcStructuralPointConnection
:return: IfcTopologyRepresentation if it's valid.
"""
vertex_representation, undefined_representation = None, None
# At least 1 representation is mandatory in IFC for IfcStructuralPointConnection.
for rep in product.Representation.Representations:
rep: ifcopenshell.entity_instance
rep_type: str = rep.RepresentationType
if rep_type == "Vertex":
vertex_representation = rep
break
# It's possible to have 'Undefined' or some other non-predefined type.
elif rep_type not in ("Edge", "Path", "Face", "Shell"):
undefined_representation = rep
if not vertex_representation and not undefined_representation:
return
# All other checks in this case are covered by IFC validation.
if vertex_representation:
items = vertex_representation.Items
if len(items) != 1:
return None
return vertex_representation
if undefined_representation is None:
return
items = undefined_representation.Items
if len(items) != 1 or not all(item.is_a("IfcVertex") for item in items):
return None
return undefined_representation
+10 -6
View File
@@ -28,7 +28,7 @@ import bonsai.core.tool
import bonsai.tool as tool
import bonsai.bim.helper
from mathutils import Color
from typing import Union, Any, Optional, Literal, TYPE_CHECKING
from typing import Union, Any, Optional, Literal, TYPE_CHECKING, Sequence
if TYPE_CHECKING:
from bonsai.bim.module.style.prop import BIMStylesProperties, BIMStyleProperties
@@ -291,7 +291,7 @@ class Style(bonsai.core.tool.Style):
def get_surface_rendering_attributes(cls, obj: bpy.types.Material, verbose: bool = False) -> dict[str, Any]:
report = (lambda *x: print(*x)) if verbose else (lambda *x: None)
def color_to_ifc_format(color):
def color_to_ifc_format(color: Sequence[float]) -> dict[str, Any]:
return {
"Name": None,
"Red": color[0],
@@ -324,6 +324,7 @@ class Style(bonsai.core.tool.Style):
report(f"{GREEN}Viewport color{R} saved as {GREEN}SurfaceColour{R}")
# TODO: make sure bsdf is connected to the output?
assert obj.node_tree
bsdfs = {n.type: n for n in obj.node_tree.nodes if n.outputs and n.outputs[0].is_linked}
if "BSDF_PRINCIPLED" not in bsdfs:
report(f"{GREEN}Viewport color alpha{R} saved as {GREEN}Transparency{R}")
@@ -386,10 +387,10 @@ class Style(bonsai.core.tool.Style):
report(f"BSDF {GREEN}Color{R} saved as {GREEN}DiffuseColour{R}")
diffuse_color = bsdf.inputs["Color"].default_value
elif "BSDF_DIFFUSE" in bsdfs:
elif "BSDF_DIFFUSE" in bsdfs or "DIFFUSE_BSDF" in bsdfs:
report(f"Because of {BLUE}BSDF_DIFFUSE{R} node reflectance method identified as {BLUE}MATT{R}")
attributes["ReflectanceMethod"] = "MATT"
bsdf = bsdfs["BSDF_DIFFUSE"]
bsdf = bsdfs["BSDF_DIFFUSE"] if "BSDF_DIFFUSE" in bsdfs else bsdfs["DIFFUSE_BSDF"]
report(f"BSDF {GREEN}Roughness{R} saved as {GREEN}IfcSpecularRoughness{R}")
attributes["SpecularHighlight"] = {"IfcSpecularRoughness": round(bsdf.inputs["Roughness"].default_value, 3)}
@@ -665,9 +666,12 @@ class Style(bonsai.core.tool.Style):
@classmethod
def rename_style(cls, style: ifcopenshell.entity_instance, name: str) -> None:
"""Rename style and related blender material."""
if style.Name == name:
return
style.Name = name
blender_material = tool.Ifc.get_object(style)
# Will implicitly update the style name using handler.
blender_material.name = name
assert isinstance(blender_material, bpy.types.Material)
tool.Root.set_material_name(blender_material, name)
@classmethod
def purge_unused_styles(cls) -> int:
@@ -30,6 +30,12 @@ pause
echo Changing to the Git repository directory...
cd %REPO_PATH%
:: Handle symlinks (they could be disabled by default on Windows).
git config --local core.symlinks true
:: Delete and checkout is the only way to ensure files are added as symlinks.
del /Q src\bonsai\bonsai\bim\data\templates\projects\*.ifc
git checkout -- src/bonsai/bonsai/bim/data/templates/projects/*.ifc
echo Copy over compiled IfcOpenShell files...
copy "%PACKAGE_PATH%\ifcopenshell\*_wrapper*" "%CD%\src\ifcopenshell-python\ifcopenshell\"
@@ -10,6 +10,10 @@ PACKAGE_PATH="${BLENDER_PATH}/extensions/.local/lib/python3.11/site-packages"
# BONSAI_PATH="${BLENDER_PATH}/extensions/user_default/bonsai"
BONSAI_PATH="${BLENDER_PATH}/extensions/raw_githubusercontent_com/bonsai"
# Validate inputs.
[ -z "$REPO_PATH" ] && echo "Error: REPO_PATH is not set." && exit 1
[ -z "$BLENDER_PATH" ] && echo "Error: BLENDER_PATH is not set." && exit 1
# Changing to the Git repository directory
cd "${REPO_PATH}"
@@ -38,9 +38,10 @@ Scenario: Add aggregate
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate"
Then the object "IfcElementAssembly/Default_Name" exists
@@ -51,9 +52,10 @@ Scenario: Add aggregate - add a nested aggregate
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate"
And the object "IfcWall/Cube" is selected
@@ -38,14 +38,16 @@ Scenario: Copy attribute to selected
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And additionally the object "IfcWall/Cube.001" is selected
And I press "bim.enable_editing_attributes(obj='IfcWall/Cube.001')"
+24 -18
View File
@@ -357,9 +357,10 @@ Scenario: Assign cost item quantity - count based
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
When I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
Then nothing happens
@@ -372,9 +373,10 @@ Scenario: Assign cost item quantity - quantity based
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')"
And I press "bim.perform_quantity_take_off"
@@ -390,9 +392,10 @@ Scenario: Unassign cost item quantity - selection based
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
When I press "bim.unassign_cost_item_quantity(cost_item={cost_item}, related_object=0)"
@@ -407,9 +410,10 @@ Scenario: Unassign cost item quantity - explicit object
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
And the variable "wall" is "{ifc}.by_type('IfcWall')[0].id()"
@@ -425,9 +429,10 @@ Scenario: Select cost item products
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
When I press "bim.select_cost_item_products(cost_item={cost_item})"
@@ -442,9 +447,10 @@ Scenario: Select Cost Schedule Products
And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')"
When I press "bim.select_cost_schedule_products(cost_schedule={cost_schedule})"
+8 -6
View File
@@ -88,9 +88,10 @@ Scenario: Assign document
And the variable "reference" is "{ifc}.by_type('IfcDocumentReference')[-1].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
When I press "bim.assign_document(document={reference})"
Then nothing happens
@@ -104,9 +105,10 @@ Scenario: Unassign document
And the variable "reference" is "{ifc}.by_type('IfcDocumentReference')[-1].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.assign_document(document={reference})"
When I press "bim.unassign_document(document={reference})"
Then nothing happens
+24 -18
View File
@@ -5,9 +5,10 @@ Scenario: Duplicate drawing
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
@@ -22,9 +23,10 @@ Scenario: Create drawing
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
@@ -39,9 +41,10 @@ Scenario: Create drawing after deleting a duplicated object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I duplicate the selected objects
And I save sample test files
And I look at the "Drawings" panel
@@ -89,9 +92,10 @@ Scenario: Remove drawing
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
@@ -107,9 +111,10 @@ Scenario: Remove drawing - via object deletion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_drawing"
And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()"
And the collection "IfcAnnotation/PLAN_VIEW" exists
@@ -121,9 +126,10 @@ Scenario: Remove drawing - deleting active drawing
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I save sample test files
And I look at the "Drawings" panel
And I click "IMPORT"
+68 -51
View File
@@ -5,9 +5,10 @@ Scenario: Edit object placement
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When I press "bim.edit_object_placement"
Then nothing happens
@@ -16,9 +17,10 @@ Scenario: Add representation
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
Then the object "IfcWall/Cube" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW"
When the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()"
@@ -71,9 +73,10 @@ Scenario: Switch representation
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()"
And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation})"
Then nothing happens
@@ -159,9 +162,10 @@ Scenario: Update representation - updating a tessellation
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.update_representation(obj='IfcWall/Cube')"
Then the object "IfcWall/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW"
@@ -169,9 +173,10 @@ Scenario: Update representation - updating a layered extrusion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -196,9 +201,10 @@ Scenario: Update representation - updating a profiled extrusion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -220,9 +226,10 @@ Scenario: Get representation IFC parameters
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.update_representation(ifc_representation_class='IfcExtrudedAreaSolid/IfcRectangleProfileDef')"
When I press "bim.get_representation_ifc_parameters"
Then nothing happens
@@ -231,9 +238,10 @@ Scenario: Copy representation
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add a cube
And the object "Cube" is selected
And I press "bim.assign_class"
@@ -253,9 +261,10 @@ Scenario: Override delete - with active IFC data
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When I press "bim.override_object_delete"
Then the object "IfcWall/Cube" does not exist
@@ -278,9 +287,10 @@ Scenario: Override duplicate move - with active IFC data
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And additionally the object "IfcBuildingStorey/My Storey" is selected
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
@@ -296,9 +306,10 @@ Scenario: Override duplicate move - with unlocked elements
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcBuildingStorey/My Storey" is selected
And I set "scene.BIMSpatialDecompositionProperties.is_locked" to "False"
When I duplicate the selected objects
@@ -309,9 +320,10 @@ Scenario: Override duplicate move - copying a coloured representation
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
And I press "bim.enable_adding_presentation_style"
@@ -348,9 +360,10 @@ Scenario: Override duplicate move - copying a layered extrusion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -378,9 +391,10 @@ Scenario: Override duplicate move - copying a profiled extrusion
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -405,9 +419,10 @@ Scenario: Override duplicate move - copying an aggregate
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When I press "bim.add_aggregate(aggregate_name='Assembly')"
When the object "IfcWall/Cube" is selected
@@ -488,9 +503,10 @@ Scenario: Override duplicate move linked - with active IFC data
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And additionally the object "IfcBuildingStorey/My Storey" is selected
When I press "object.duplicate_move_linked"
@@ -517,9 +533,10 @@ Scenario: Override paste buffer - with active IFC data
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And additionally the object "IfcBuildingStorey/My Storey" is selected
When I press "view3d.copybuffer"
+8 -6
View File
@@ -113,9 +113,10 @@ Scenario: Assign library reference
And the variable "reference" is "{ifc}.by_type('IfcLibraryReference')[-1].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When I press "bim.assign_library_reference(reference={reference})"
Then nothing happens
@@ -129,9 +130,10 @@ Scenario: Unassign library reference
And the variable "reference" is "{ifc}.by_type('IfcLibraryReference')[-1].id()"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I press "bim.assign_library_reference(reference={reference})"
When I press "bim.unassign_library_reference(reference={reference})"
+76 -57
View File
@@ -45,9 +45,10 @@ Scenario: Assign material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
And I press "bim.assign_material"
@@ -57,9 +58,10 @@ Scenario: Unassign material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
And I press "bim.assign_material"
@@ -70,9 +72,10 @@ Scenario: Enable editing assigned material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
And I press "bim.assign_material"
@@ -83,9 +86,10 @@ Scenario: Disable editing assigned material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
And I press "bim.assign_material"
@@ -97,9 +101,10 @@ Scenario: Edit assigned material - single material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterial"
And I press "bim.assign_material"
@@ -138,9 +143,10 @@ Scenario: Unassign material - removing inherited material
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
@@ -165,9 +171,10 @@ Scenario: Enable editing assigned material - material layer set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -183,9 +190,10 @@ Scenario: Disable editing assigned material - material layer set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -202,9 +210,10 @@ Scenario: Edit assigned material - material layer set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -248,9 +257,10 @@ Scenario: Enable editing assigned material - material profile set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -266,9 +276,10 @@ Scenario: Disable editing assigned material - material profile set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -285,9 +296,10 @@ Scenario: Edit assigned material - material profile set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -305,9 +317,10 @@ Scenario: Assign material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
And I press "bim.assign_material"
@@ -317,9 +330,10 @@ Scenario: Unassign material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
And I press "bim.assign_material"
@@ -330,9 +344,10 @@ Scenario: Enable editing assigned material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
And I press "bim.assign_material"
@@ -343,9 +358,10 @@ Scenario: Disable editing assigned material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
And I press "bim.assign_material"
@@ -357,9 +373,10 @@ Scenario: Edit assigned material - material constituent set
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I press "bim.add_material()"
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialConstituentSet"
And I press "bim.assign_material"
@@ -409,9 +426,10 @@ Scenario: Add material set layer
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
@@ -429,9 +447,10 @@ Scenario: Remove material set layer
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add an empty
And the object "Empty" is selected
+19 -13
View File
@@ -21,23 +21,28 @@ Scenario: Resize to storey
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()"
And I press "bim.set_default_container(container={storey})"
And I press "bim.assign_container(container={storey})"
When I press "bim.resize_to_storey(total_storeys=1)"
And I look at the "Spatial Decomposition" panel
And I select the "My Storey" item in the "BIM_UL_containers_manager" list
And I click "Set Default"
# Assign Container.
And I click "FOLDER_REDIRECT"
And I look at the "Miscellaneous" panel
When I click "Resize To Storey"
Then nothing happens
Scenario: Split along edge - boolean mode
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add a plane of size "4" at "0,0,0"
And the object "IfcWall/Cube" is selected
And additionally the object "Plane" is selected
@@ -50,9 +55,10 @@ Scenario: Split along edge - bisect mode
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add a plane of size "4" at "0,0,0"
And the object "IfcWall/Cube" is selected
And additionally the object "Plane" is selected
+8 -6
View File
@@ -284,9 +284,10 @@ Scenario: Assign actor
And I press "bim.add_actor"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()"
And the object "IfcWall/Cube" is selected
When I press "bim.assign_actor(actor={actor})"
@@ -298,9 +299,10 @@ Scenario: Unassign actor
And I press "bim.add_actor"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()"
And the object "IfcWall/Cube" is selected
And I press "bim.assign_actor(actor={actor})"
+4 -3
View File
@@ -901,9 +901,10 @@ Scenario: Export IFC - with changed object scale ignored
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
When the object "IfcWall/Cube" is scaled to "2"
And I press "bim.save_project(filepath='{cwd}/test/files/temp/export.ifc')"
+48 -36
View File
@@ -5,9 +5,10 @@ Scenario: Add pset - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
When I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
@@ -17,15 +18,17 @@ Scenario: Add pset - multiple objects
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And additionally the object "IfcWall/Cube.001" is selected
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
@@ -36,9 +39,10 @@ Scenario: Enable pset editing - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
And I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
@@ -128,9 +132,10 @@ Scenario: Disable pset editing - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
And I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
@@ -202,9 +207,10 @@ Scenario: Edit pset - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
And I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
@@ -215,9 +221,10 @@ Scenario: Edit qto - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I set "active_object.PsetProperties.qto_name" to "Qto_WallBaseQuantities"
When I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')"
@@ -290,14 +297,16 @@ Scenario: Copy property to selected - copy property
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And additionally the object "IfcWall/Cube.001" is selected
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
@@ -310,9 +319,10 @@ Scenario: Remove pset - object
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"
And I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
@@ -326,15 +336,17 @@ Scenario: Remove pset - multiple objects
Given an empty IFC project
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And I add a cube
And the object "Cube" is selected
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 look at the "Class" panel
And I set the "Products" property to "IfcElement"
And I set the "Class" property to "IfcWall"
And I click "Assign IFC Class"
And the object "IfcWall/Cube" is selected
And additionally the object "IfcWall/Cube.001" is selected
And I set "active_object.PsetProperties.pset_name" to "Pset_WallCommon"

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