Compare commits

..

2 Commits

Author SHA1 Message Date
Andrej730 05a3004ef0 test 2025-04-01 19:06:44 +05:00
Andrej730 fe5afaf172 black . 2025-04-01 18:42:47 +05:00
253 changed files with 2636 additions and 7211 deletions
+30
View File
@@ -0,0 +1,30 @@
---
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
@@ -1,23 +0,0 @@
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
@@ -1,8 +0,0 @@
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
@@ -0,0 +1,15 @@
---
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.
-->
@@ -1,8 +0,0 @@
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 -6
View File
@@ -118,10 +118,5 @@ jobs:
aws-region: us-east-1
- name: Upload .zip Archives to S3
env:
AWS_DEBUG: 1
AWS_RETRY_MODE: standard
AWS_MAX_ATTEMPTS: 3
run: |
dir "$env:USERPROFILE\output"
aws s3 cp "$env:USERPROFILE\output" s3://ifcopenshell-builds/ --recursive --debug
aws s3 cp $env:USERPROFILE\output s3://ifcopenshell-builds/ --recursive
+43 -22
View File
@@ -19,34 +19,55 @@ jobs:
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Install dependencies
- name: Step 1 - install dependencies
shell: bash
run: |
python3 -m pip install --upgrade pip
python3 -m pip install 'black>=24.10.0'
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
id: syntax-errors
run: |
ERROR=0
python3 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3 -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
continue-on-error: true
# NOTE: This would suffice, however it is less informative in terms of the 3 possible outcomes
# - name: QA Step - check linting
# shell: bash
# id: linting
# run: |
# python3 -m black .
- name: Black formatter
# QA STEP
- name: QA Step - check linting
shell: bash
id: linting
run: |
python3 -m black --check .
python3 -m black --check . \
&& exit 0 \
|| (echo "exit_code=$?" >> "$GITHUB_OUTPUT" && exit 1);
continue-on-error: true
- name: Final check
run: |
ERROR=0
if [ "${{ steps.syntax-errors.outcome }}" != "success" ]; then
echo "::error::Syntax errors check failed, see 'syntax-errors' step for the details." && ERROR=1
fi
if [ "${{ steps.linting.outcome }}" != "success" ]; then
echo "::error::Black formatting check failed, see 'linting' step for the details." && ERROR=1
fi
exit $ERROR
# OUTCOME 1 of QA STEP
- name: QA Step - no linting errors
if: steps.linting.outcome == 'success'
shell: bash
run: |-
echo "::notice::QA step linting succeeded"
exit 0;
# OUTCOME 2i of QA STEP
- name: QA Step - unprettified code with no syntax errors
if: steps.linting.outputs.exit_code == 1
shell: bash
run: |-
echo "::group::QA step succeeded with warnings"
echo "::warning::one or more files contains unformatted code but no syntax errors";
echo "::notice::please run the linter before pushing!";
echo "::endgroup::"
exit 0;
# OUTCOME 2ii of QA STEP
- name: QA Step - code contains syntax errors
if: steps.linting.outputs.exit_code == 123
shell: bash
run: |-
echo "::group::QA step failed"
echo "::error::one or more files contains syntax errors";
echo "::notice::please run the linter and fix syntax errors before pushing!";
echo "::endgroup::"
exit 1;
+1 -2
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.4/blender-4.4.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
@@ -175,7 +175,6 @@ jobs:
cd IfcOpenShell/src/bonsai
pip install pytest-blender
pip install pytest-bdd
blender --background --python scripts/setup_pytest.py
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
make test
+6 -6
View File
@@ -9,7 +9,7 @@ on:
jobs:
activate:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
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-22.04
runs-on: ubuntu-latest
needs: activate
steps:
- uses: actions/checkout@v2
@@ -35,7 +35,7 @@ jobs:
-
name: ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1
-
name: Build ifcopenshell
@@ -75,7 +75,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
# Artifact name
name: ifcos-artifacts
@@ -83,7 +83,7 @@ jobs:
path: build/assets/Ifc*
deliver:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
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:22.04' }}
tags: aecgeeks/ifcopenshell:${{ github.ref_name }}${{ github.ref_name == github.event.repository.default_branch && ',aecgeeks/ifcopenshell:latest' }}
file: ./Dockerfile
push: true
-1
View File
@@ -4,7 +4,6 @@ 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
+1 -1
View File
@@ -1 +1 @@
0.8.3
0.8.2
-1
View File
@@ -35,4 +35,3 @@ extend-exclude = '''
[tool.pyright]
reportInvalidTypeForm = false
disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
-1
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
SHELL := sh
PYTHON:=python3.11
PIP:=pip3.11
PATCH:=patch
+10 -25
View File
@@ -28,10 +28,6 @@ IN_BLENDER = sys.modules.get("bpy", None)
if IN_BLENDER:
import bpy
# This file is executed twice - first as a bonsai-extension
# and then as a bonsai-package.
IN_PACKAGE = __package__ == "bonsai"
import re
import platform
import traceback
@@ -222,21 +218,17 @@ if IN_BLENDER:
info["binary_python_version"] = version
return info
def update_commit_data() -> None:
try:
import git
try:
import git
global last_commit_hash
global last_commit_date
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
if IN_PACKAGE:
update_commit_data()
# We can't just use __file__ as bonsai/__init__.py is typically not symlinked
# as Blender have errors symlinking main addon package file.
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
except:
pass
try:
import ifcopenshell.api
@@ -252,12 +244,6 @@ if IN_BLENDER:
ifcopenshell.api.add_pre_listener("*", "action_logger", log_api)
def purge_cache():
"""Purge cache left from previous session (e.g. after reload or update)."""
import bonsai.tool as tool
tool.Blender.get_bonsai_version.cache_clear()
def register():
if platform.system() == "Windows":
clean_up_dlls_safe_links()
@@ -275,7 +261,6 @@ if IN_BLENDER:
bonsai.REINSTALLED_BBIM_VERSION = current_version
bonsai.bim.register()
purge_cache()
def unregister():
if platform.system() == "Windows":
-7
View File
@@ -20,7 +20,6 @@ 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
@@ -101,7 +100,6 @@ classes = [
operator.BIM_OT_delete_object,
operator.BIM_OT_remove_section_plane,
operator.BIM_OT_select_entity,
operator.BIM_OT_select_entity_by_guid,
operator.BIM_OT_select_object,
operator.BIM_OT_show_description,
operator.BIM_OT_multiple_file_selector,
@@ -223,12 +221,7 @@ 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)
@@ -1,11 +1,11 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION($,'2;1');
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_Drawing',$);
FILE_DESCRIPTION((),'2;1');
FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset_Drawing',$);
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15,#16,#17,#18,#19,#20,#21,#22,#23,#24,#25,#26,#27));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
@@ -32,6 +32,5 @@ DATA;
#25=IFCSIMPLEPROPERTYTEMPLATE('3LHwCrOcb6Y8ozfJZ7Ay$c',$,'LineworkMode','Method to use for line work',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#26=IFCSIMPLEPROPERTYTEMPLATE('2iwERDOW55Pf4hCbuFRe1Q',$,'FillMode','Method to fill areas seen in projection',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#27=IFCSIMPLEPROPERTYTEMPLATE('1YF$qLzBzF19Io8aB2N8cE',$,'CutMode','Method for cutting geometry',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
+30
View File
@@ -47,6 +47,7 @@ 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":
@@ -100,12 +101,41 @@ 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 = tool.Aggregate.get_aggregate_props()
nest_props = tool.Nest.get_nest_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
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 preferences.should_use_snap and (scene := bpy.context.scene):
if 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
-1
View File
@@ -392,7 +392,6 @@ def draw_filter(
row = layout.row(align=True)
row.label(text=f"{len(data.data['saved_searches'])} Saved Searches")
row.operator("bim.select_entity_by_guid", text="", icon="CON_OBJECTSOLVER")
if data.data["saved_searches"]:
row.operator("bim.load_search", text="", icon="IMPORT").module = module
row.operator("bim.save_search", text="", icon="EXPORT").module = module
+2 -31
View File
@@ -31,10 +31,9 @@ import ifcopenshell.ifcopenshell_wrapper
import bonsai
import bonsai.bim.handler
import bonsai.tool as tool
from ifcopenshell.file import UndoSystemError
from pathlib import Path
from bonsai.tool.brick import BrickStore
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, Literal
from typing import Set, Union, Optional, TypedDict, Callable, NotRequired
IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
@@ -418,32 +417,6 @@ class IfcStore:
props = tool.Blender.get_object_bim_props(obj)
props.ifc_definition_id = 0
@staticmethod
def get_ifc_file_undo_callback(callback_type: Literal["UNDO", "REDO"]):
def callback(_) -> None:
try:
if callback_type == "UNDO":
tool.Ifc.get().undo()
else:
tool.Ifc.get().redo()
except Exception as e:
# Persistent callbacks errors are not visible from UI and we set `last_error`
# to make it visible.
error_msg = ""
# In theory it should always be UndoSystemError, but just to be safe.
if isinstance(e, UndoSystemError):
error_msg += "Undo transaction operations:\n"
transaction = e.transaction
for operation in transaction.operations:
error_msg += f"- {str(operation)}\n"
# Show it in system console too, not just in last message.
print(error_msg)
error_msg += traceback.format_exc()
bonsai.last_error = error_msg
raise
return callback
@staticmethod
def execute_ifc_operator(
operator: tool.Ifc.Operator,
@@ -473,9 +446,7 @@ class IfcStore:
if tool.Ifc.get():
tool.Ifc.get().end_transaction()
IfcStore.add_transaction_operation(
operator,
rollback=IfcStore.get_ifc_file_undo_callback("UNDO"),
commit=IfcStore.get_ifc_file_undo_callback("REDO"),
operator, rollback=lambda d: tool.Ifc.get().undo(), commit=lambda d: tool.Ifc.get().redo()
)
if BrickStore.graph is not None: # `if BrickStore.graph` by itself takes ages.
BrickStore.end_transaction()
+20 -43
View File
@@ -38,7 +38,7 @@ import ifcopenshell.util.shape
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
from bonsai.tool.loader import OBJECT_DATA_TYPE
from typing import Dict, Union, Optional, Any, Literal, Iterable
from typing import Dict, Union, Optional, Any, Literal
from ifcopenshell.util.shape import MatrixType
@@ -198,16 +198,11 @@ class MaterialCreator:
class IfcImporter:
file: ifcopenshell.file
"""Either provided by user as an attribute or will be loaded from ``input_file`` during ``execute()``."""
elements: set[ifcopenshell.entity_instance]
"""Set of IfcElements to import. Excluding ``gross_elements`` and ```native_elements``."""
def __init__(self, ifc_import_settings: IfcImportSettings):
self.ifc_import_settings = ifc_import_settings
tool.Loader.set_settings(ifc_import_settings)
self.diff = None
self.file: ifcopenshell.file = None
self.project = None
self.has_existing_project = False
# element guids to blender collections mapping
@@ -305,7 +300,6 @@ class IfcImporter:
bpy.context.window_manager.progress_end()
def process_context_filter(self) -> None:
"""Setup contexts. Necessary for importing elements representations."""
contexts = self.file.by_type("IfcGeometricRepresentationContext")
if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto.
print("Warning! Excessive contexts were found and merged where applicable.")
@@ -325,15 +319,16 @@ class IfcImporter:
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
def process_element_filter(self) -> None:
elements: list[ifcopenshell.entity_instance]
if self.ifc_import_settings.has_filter:
elements = list(self.ifc_import_settings.elements)
self.elements = self.ifc_import_settings.elements
if isinstance(self.elements, set):
self.elements = list(self.elements)
# TODO: enable filtering for annotations
else:
if self.file.schema in ("IFC2X3", "IFC4"):
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
self.elements = self.file.by_type("IfcElement") + self.file.by_type("IfcProxy")
else:
elements = self.file.by_type("IfcElement")
self.elements = self.file.by_type("IfcElement")
drawing_groups = [g for g in self.file.by_type("IfcGroup") if g.ObjectType == "DRAWING"]
drawing_annotations = set()
@@ -343,16 +338,16 @@ class IfcImporter:
self.annotations = set([a for a in self.file.by_type("IfcAnnotation")])
self.annotations -= drawing_annotations
elements = [e for e in elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
self.elements = [e for e in self.elements if not e.is_a("IfcFeatureElement") or e.is_a("IfcSurfaceFeature")]
if self.ifc_import_settings.element_limit_mode == "UNLIMITED":
self.elements = set(elements)
self.elements = set(self.elements)
else:
offset = self.ifc_import_settings.element_offset
offset_limit = offset + self.ifc_import_settings.element_limit
self.elements = set(elements[offset:offset_limit])
self.elements = set(self.elements[offset:offset_limit])
if self.ifc_import_settings.has_filter or self.ifc_import_settings.element_limit_mode != "UNLIMITED":
self.element_types = {t for e in self.elements if (t := ifcopenshell.util.element.get_type(e))}
self.element_types = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
self.element_types = set(self.file.by_type("IfcTypeProduct"))
@@ -390,7 +385,6 @@ class IfcImporter:
return results
def parse_native_elements(self) -> None:
# TODO: move to `process_element_filter` to incapsulate all `self.elements` logic.
if not self.ifc_import_settings.should_load_geometry:
return
if not self.file.by_type("IfcSweptDiskSolid"):
@@ -723,15 +717,9 @@ 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 = tool.Structural.get_vertex_representation(product)
if not representation:
print(
"WARNING. Skipping invalid IfcStructuralPointConnection - "
f"element has no valid representation:\n{product}."
)
continue
representation: ifcopenshell.entity_instance = next(
rep for rep in product.Representation.Representations if rep.RepresentationType == "Vertex"
)
mesh = tool.Loader.create_structural_point_connection_mesh(representation)
if mesh is None:
continue
@@ -1136,7 +1124,7 @@ class IfcImporter:
bpy.ops.view3d.view_selected()
bpy.ops.object.select_all(action="DESELECT")
def setup_arrays(self, annotations_to_import: Optional[set[ifcopenshell.entity_instance]] = None) -> None:
def setup_arrays(self):
for pset in self.file.by_type("IfcPropertySet"):
if pset.Name != "BBIM_Array":
continue
@@ -1144,25 +1132,16 @@ class IfcImporter:
continue
data = json.loads(data)
for rel in pset.DefinesOccurrence:
element: ifcopenshell.entity_instance
for element in rel.RelatedObjects:
if annotations_to_import is None:
# IfcAnnotations are not loaded during base import.
if element.is_a("IfcAnnotation"):
continue
else:
if element not in annotations_to_import:
continue
for i in range(len(data)):
tool.Blender.Modifier.Array.set_children_lock_state(element, i, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
class IfcImportSettings:
input_file: Union[str, None] = None
logger: Union[logging.Logger, None] = None
def __init__(self):
self.logger: logging.Logger = None
self.input_file = None
self.diff_file = None
self.geometry_library = "opencascade"
self.should_use_cpu_multiprocessing = True
@@ -1183,19 +1162,17 @@ class IfcImportSettings:
self.element_limit_mode = "UNLIMITED"
self.element_offset = 0
self.element_limit = 30000
self.has_filter = False
self.has_filter = None
self.should_filter_spatial_elements = True
self.should_setup_viewport_camera = True
self.contexts: list[ifcopenshell.entity_instance] = []
self.context_settings: list[ifcopenshell.geom.main.settings] = []
self.gross_context_settings: list[ifcopenshell.geom.main.settings] = []
self.elements: Iterable[ifcopenshell.entity_instance] = set()
self.elements: set[ifcopenshell.entity_instance] = set()
self.load_indexed_maps = False
@staticmethod
def factory(
context=None, input_file: Optional[str] = None, logger: Optional[logging.Logger] = None
) -> IfcImportSettings:
def factory(context=None, input_file=None, logger=None):
scene_diff = tool.Blender.get_diff_props()
props = tool.Project.get_project_props()
settings = IfcImportSettings()
@@ -20,7 +20,6 @@ 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
@@ -163,8 +162,7 @@ class AggregateDecorator:
batch.draw(shader)
def draw_aggregate(self, context):
props = tool.Aggregate.get_aggregate_props()
if props.in_aggregate_mode:
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
@@ -212,7 +210,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 props.in_aggregate_mode:
if context.scene.BIMAggregateProperties.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]
@@ -263,7 +261,7 @@ class AggregateModeDecorator:
return
region = context.region
rv3d = region.data
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
@@ -294,7 +292,7 @@ class AggregateModeDecorator:
def draw_aggregate_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
aggregate_obj = props.editing_aggregate
if not aggregate_obj:
return
@@ -23,7 +23,6 @@ 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):
@@ -31,30 +30,23 @@ 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 Object Aggregates UI, then either 'Relating Whole' or 'Related Part' must be provided.\n"
"If called from Bonsai 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)
assert relating_obj
else:
elif context.active_object:
relating_obj = context.active_object
if not relating_obj:
self.report({"ERROR"}, "No relating object is provided.")
return
@@ -74,7 +66,7 @@ class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
relating_obj=relating_obj,
related_obj=obj,
)
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
if relating_obj == props.editing_aggregate and props.in_aggregate_mode:
new_editing_obj = props.editing_objects.add()
new_editing_obj.obj = obj
@@ -140,11 +132,12 @@ class BIM_OT_disable_editing_aggregate(bpy.types.Operator):
class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
"""Add aggregate to selected IFC elements."""
"""Add aggregate to IFC element"""
bl_idname = "bim.add_aggregate"
bl_label = "Add Aggregate"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
ifc_class: bpy.props.StringProperty(name="IFC Class", default="IfcElementAssembly")
aggregate_name: bpy.props.StringProperty(name="Name", default="Default_Name")
@@ -164,7 +157,7 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
return
aggregate = self.create_aggregate(context, ifc_class, self.aggregate_name)
for obj in tool.Blender.get_selected_objects():
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
@@ -189,7 +182,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: bpy.types.Context, ifc_class: str, aggregate_name: str) -> bpy.types.Object:
def create_aggregate(self, context, ifc_class, aggregate_name):
aggregate = bpy.data.objects.new(aggregate_name, None)
aggregate.location = context.scene.cursor.location
bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class=ifc_class)
@@ -385,7 +378,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
bonsai.core.aggregate.exit_aggregate_mode(tool.Aggregate)
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
return {"FINISHED"}
@@ -395,7 +388,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Aggregate.get_aggregate_props()
props = context.scene.BIMAggregateProperties
objs = [o.obj for o in props.editing_objects]
if props.in_aggregate_mode:
if context.space_data.local_view:
@@ -33,7 +33,6 @@ 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:
@@ -99,27 +98,16 @@ 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)
editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
previous_editing_aggregate: PointerProperty(name="Editing Aggregate", type=bpy.types.Object)
editing_objects: CollectionProperty(type=Objects)
not_editing_objects: CollectionProperty(type=Objects)
aggregate_decorator: BoolProperty(
@@ -127,16 +115,3 @@ class BIMAggregateProperties(PropertyGroup):
default=False,
update=update_aggregate_decorator,
)
previous_state: BoolProperty(
name="True if it was previously in aggregate mode",
default=False,
)
if TYPE_CHECKING:
in_aggregate_mode: bool
editing_aggregate: Union[bpy.types.Object, None]
previous_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
previous_state: bool
+7 -7
View File
@@ -49,14 +49,11 @@ class BIM_PT_aggregate(Panel):
layout = self.layout
row = layout.row()
row.label(text="Aggregate Decorator")
props = tool.Aggregate.get_aggregate_props()
icon = "HIDE_OFF" if props.aggregate_decorator else "HIDE_ON"
row.prop(props, "aggregate_decorator", icon=icon, text="")
row.prop(context.scene.BIMAggregateProperties, "aggregate_decorator", icon="HIDE_OFF", text="")
if not AggregateData.is_loaded:
AggregateData.load()
assert (obj := context.active_object)
props = tool.Aggregate.get_object_aggregate_props(obj)
props = context.active_object.BIMObjectAggregateProperties
if props.is_editing:
row = layout.row()
@@ -79,8 +76,10 @@ class BIM_PT_aggregate(Panel):
if AggregateData.data["has_relating_object"]:
row.label(text=AggregateData.data["relating_object_label"], icon="TRIA_UP")
op = row.operator("bim.select_aggregate", icon="OBJECT_DATA", text="")
op.obj = context.active_object.name
op.select_parts = False
op = row.operator("bim.select_aggregate", icon="RESTRICT_SELECT_OFF", text="")
op.obj = context.active_object.name
op.select_parts = True
row.operator("bim.enable_editing_aggregate", icon="GREASEPENCIL", text="")
row.operator("bim.add_aggregate", icon="ADD", text="")
@@ -132,8 +131,9 @@ class BIM_PT_linked_aggregate(Panel):
if not AggregateData.is_loaded:
AggregateData.load()
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMObjectAggregateProperties
row = layout.row(align=True)
if element.Decomposes:
@@ -45,7 +45,6 @@ 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,42 +26,30 @@ 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: "AttributesOperator", context: bpy.types.Context
) -> list[bpy.types.Object]:
def get_objs_for_operation(operator_properties, context):
if operator_properties.obj:
return [bpy.data.objects[operator_properties.obj]]
if operator_properties.mass_operation:
return context.selected_objects[:]
obj = context.active_object
assert obj
return [obj]
return [context.active_object]
class AttributesOperator:
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"}
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)
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)
def enable_editing_attribute_on_obj(self, obj):
props = obj.BIMAttributeProperties
props.attributes.clear()
element = tool.Ifc.get_entity(obj)
@@ -99,14 +87,20 @@ class EnableEditingAttributes(bpy.types.Operator, AttributesOperator):
return {"FINISHED"}
class DisableEditingAttributes(bpy.types.Operator, AttributesOperator):
class DisableEditingAttributes(bpy.types.Operator):
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 disable_editing_attributes_on_obj(self, obj: bpy.types.Object) -> None:
props = tool.Blender.get_object_attribute_props(obj)
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
props.is_editing_attributes = False
def execute(self, context):
@@ -124,7 +118,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 obj or not (element := tool.Ifc.get_entity(obj)):
if not (element := tool.Ifc.get_entity(obj)):
return
def callback(attributes, prop):
@@ -136,7 +130,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
attributes[prop.name] = None
return True
props = tool.Blender.get_object_attribute_props(obj)
props = obj.BIMAttributeProperties
attributes = bonsai.bim.helper.export_attributes(props.attributes, callback=callback)
ifcopenshell.api.attribute.edit_attributes(self.file, product=element, attributes=attributes)
@@ -171,12 +165,13 @@ class GenerateGlobalId(bpy.types.Operator, tool.Ifc.Operator):
element.GlobalId = ifcopenshell.guid.new()
obj = context.active_object
if not obj or not (props := tool.Blender.get_object_attribute_props(obj)).is_editing_attributes:
if not obj or not obj.BIMAttributeProperties.is_editing_attributes:
return {"FINISHED"}
props = obj.BIMAttributeProperties
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcRoot"):
if not element.is_a("IfcRoot"):
return {"FINISHED"}
if self.use_selected and obj in context.selected_objects:
@@ -196,10 +191,7 @@ class CopyAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
name: bpy.props.StringProperty()
def _execute(self, context):
obj = tool.Blender.get_active_object()
assert obj
props = tool.Blender.get_object_attribute_props(obj)
value = props.attributes[self.name].get_value()
value = tool.Blender.get_active_object().BIMAttributeProperties.attributes.get(self.name).get_value()
total = core.copy_attribute_to_selection(
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
)
@@ -29,13 +29,8 @@ 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
+2 -4
View File
@@ -17,16 +17,14 @@
# 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: bpy.types.Context, layout: bpy.types.UILayout, attributes) -> None:
def draw_ui(context, layout, attributes):
obj = context.active_object
assert obj
props = tool.Blender.get_object_attribute_props(obj)
props = obj.BIMAttributeProperties
if props.is_editing_attributes:
row = layout.row(align=True)
@@ -1283,7 +1283,6 @@ class ActivateBcfViewpoint(bpy.types.Operator):
cam_aspect: float,
context: bpy.types.Context,
) -> None:
assert isinstance(obj.data, bpy.types.Camera)
if viewpoint.visualization_info.orthogonal_camera:
camera = viewpoint.visualization_info.orthogonal_camera
obj.data.type = "ORTHO"
@@ -61,7 +61,6 @@ classes = (
operator.ExportCostSchedules,
operator.HighlightProductCostItem,
operator.ImportCostScheduleCsv,
operator.RefreshCostScheduleCsv,
operator.LoadCostItemElementQuantities,
operator.LoadCostItemQuantities,
operator.LoadCostItemResourceQuantities,
@@ -83,7 +82,6 @@ classes = (
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
prop.CostItemsMapping,
prop.ScheduleColumn,
prop.BIMCostProperties,
ui.BIM_PT_cost_schedules,
+26 -42
View File
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.tool as tool
from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
from typing import Any, Union
from typing import Any
def refresh():
@@ -33,19 +33,13 @@ def refresh():
CostItemQuantitiesData.is_loaded = False
CostItem = dict[str, Any]
CostQuantity = dict[str, Any]
CostSchedule = dict[str, Any]
Currency = dict[str, Any]
class CostSchedulesData:
data = {}
is_loaded = False
_cost_values: dict[int, dict[str, Any]]
@classmethod
def load(cls) -> None:
def load(cls):
cls.data = {
"predefined_types": cls.get_cost_schedule_types(),
"total_cost_schedules": cls.total_cost_schedules(),
@@ -60,18 +54,18 @@ class CostSchedulesData:
cls.is_loaded = True
@classmethod
def currency(cls) -> Union[Currency, None]:
def currency(cls):
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
@classmethod
def total_cost_schedules(cls) -> int:
def total_cost_schedules(cls):
return len(tool.Ifc.get().by_type("IfcCostSchedule"))
@classmethod
def schedules(cls) -> list[CostSchedule]:
results: list[CostSchedule] = []
def schedules(cls):
results = []
props = tool.Cost.get_cost_props()
if props.active_cost_schedule_id:
schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id)
@@ -94,19 +88,19 @@ class CostSchedulesData:
return results
@classmethod
def is_editing_rates(cls) -> bool:
def is_editing_rates(cls):
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_schedule_id
if not ifc_id:
return False
return
return tool.Ifc.get().by_id(ifc_id).PredefinedType == "SCHEDULEOFRATES"
@classmethod
def cost_items(cls) -> dict[int, CostItem]:
def cost_items(cls):
cls._cost_values = {}
results: dict[int, CostItem] = {}
results = {}
for cost_item in tool.Ifc.get().by_type("IfcCostItem"):
data: CostItem = {}
data = {}
cls._load_cost_item_quantities(cost_item, data)
cls._load_cost_values(cost_item, data)
cls._load_nesting_index(cost_item, data)
@@ -114,13 +108,13 @@ class CostSchedulesData:
return results
@classmethod
def _load_nesting_index(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_nesting_index(cls, cost_item, data):
data["NestingIndex"] = None
for rel in cost_item.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(cost_item)
@classmethod
def _load_cost_values(cls, root_element: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_cost_values(cls, root_element, data):
# data["CostValues"] = []
data["CategoryValues"] = {}
data["UnitBasisValueComponent"] = None
@@ -129,7 +123,6 @@ class CostSchedulesData:
data["TotalCost"] = 0.0
has_unit_basis = False
is_sum = False
values: list[ifcopenshell.entity_instance]
if root_element.is_a("IfcCostItem"):
values = root_element.CostValues
elif root_element.is_a("IfcConstructionResource"):
@@ -157,7 +150,7 @@ class CostSchedulesData:
data["TotalAppliedValue"] = None
@classmethod
def _load_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance, data: CostItem) -> None:
def _load_cost_item_quantities(cls, cost_item, data):
# parametric_quantities = []
# for rel in cost_item.Controls:
# for related_object in rel.RelatedObjects or []:
@@ -165,9 +158,8 @@ class CostSchedulesData:
# parametric_quantities.extend(quantities)
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
data["UnitSymbol"] = "-"
quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
if quantities:
quantity = quantities[0]
if cost_item.CostQuantities:
quantity = cost_item.CostQuantities[0]
data["QuantityType"] = quantity.is_a()
unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get())
if unit:
@@ -221,31 +213,23 @@ class CostSchedulesData:
) -> list[int]:
if not element.is_a("IfcObject"):
return []
cost_quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
cost_quantities = cost_item.CostQuantities
if not cost_quantities:
return []
results: list[int] = []
relationship: ifcopenshell.entity_instance
results = []
for relationship in element.IsDefinedBy:
if not relationship.is_a("IfcRelDefinesByProperties"):
continue
qto: ifcopenshell.entity_instance = relationship.RelatingPropertyDefinition
qto = relationship.RelatingPropertyDefinition
if not qto.is_a("IfcElementQuantity"):
continue
prop: ifcopenshell.entity_instance
for prop in qto.Quantities:
if prop in cost_quantities:
results.append(prop.id())
return results
@classmethod
def _load_cost_value(
cls,
root_element: ifcopenshell.entity_instance,
root_element_data: CostItem,
cost_value: ifcopenshell.entity_instance,
) -> None:
def _load_cost_value(cls, root_element, root_element_data, cost_value):
value_data = cost_value.get_info()
del value_data["AppliedValue"]
if value_data["UnitBasis"]:
@@ -272,8 +256,8 @@ class CostSchedulesData:
cls._load_cost_value(root_element, root_element_data, component)
@classmethod
def cost_quantities(cls) -> list[CostQuantity]:
results: list[CostQuantity] = []
def cost_quantities(cls):
results = []
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
@@ -283,7 +267,7 @@ class CostSchedulesData:
return results
@classmethod
def cost_values(cls) -> list[dict[str, str]]:
def cost_values(cls):
props = tool.Cost.get_cost_props()
ifc_id = props.active_cost_item_id
if not ifc_id:
@@ -291,14 +275,14 @@ class CostSchedulesData:
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@classmethod
def quantity_types(cls) -> list[tuple[str, str, str]]:
def quantity_types(cls):
return [
(t.name(), t.name(), "")
for t in tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes()
]
@classmethod
def get_cost_schedule_types(cls) -> list[tuple[str, str, str]]:
def get_cost_schedule_types(cls):
types = ifcopenshell.util.cost.get_cost_schedule_types(tool.Ifc.get())
return [(t["name"], t["name"], t["description"]) for t in types]
@@ -315,7 +299,7 @@ class CostItemRatesData:
cls.is_loaded = True
@classmethod
def schedule_of_rates(cls) -> list[tuple[str, str, str]]:
def schedule_of_rates(cls):
return [
(str(s.id()), s.Name or "Unnamed", "")
for s in tool.Ifc.get().by_type("IfcCostSchedule")
+7 -27
View File
@@ -24,7 +24,6 @@ import bonsai.tool as tool
from bpy_extras.io_utils import ImportHelper, ExportHelper
import bonsai.tool as tool
import bonsai.core.cost as core
from pathlib import Path
from typing import get_args, TYPE_CHECKING, Literal
@@ -537,7 +536,6 @@ 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"})
@@ -552,19 +550,7 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
return True
def _execute(self, context):
cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule)
return {"FINISHED"}
class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.refresh_cost_schedule_csv"
bl_label = "Refresh Cost Schedule CSV"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def _execute(self, context):
core.refresh_cost_schedule_csv(tool.Ifc, tool.Cost)
core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates)
return {"FINISHED"}
@@ -700,9 +686,8 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
bl_idname = "bim.export_cost_schedules"
bl_label = "Export Cost Schedule"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Export current/all cost schedules as CSV, XSLX or ODS files to the provided directory."
cost_schedule: bpy.props.IntProperty(options={"SKIP_SAVE"})
bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file"
cost_schedule: bpy.props.IntProperty()
format: bpy.props.EnumProperty(name="Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", "")))
directory: bpy.props.StringProperty(subtype="FILE_PATH")
filter_folder: bpy.props.BoolProperty(
@@ -711,15 +696,7 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
)
if TYPE_CHECKING:
cost_schedule: int
format: Literal["CSV", "XLSX", "ODS"]
directory: str
def check(self, context):
if self.filepath != self.directory:
self.filepath = self.directory
return True
return False
@property
def filename_ext(self) -> str:
@@ -728,12 +705,15 @@ class ExportCostSchedules(bpy.types.Operator, ExportHelper):
def execute(self, context):
cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None
r = core.export_cost_schedules(
tool.Cost, dirpath=self.directory, format=self.format, cost_schedule=cost_schedule
tool.Cost, filepath=self.directory, format=self.format, cost_schedule=cost_schedule
)
if isinstance(r, str):
self.report({"ERROR"}, r)
return {"FINISHED"}
def invoke(self, context, event):
return ExportHelper.invoke(self, context, event)
def draw(self, context):
self.layout.label(text="Choose a format")
self.layout.prop(self, "format")
-11
View File
@@ -167,15 +167,6 @@ class CostItemQuantity(PropertyGroup):
total_cost_quantity: float
class CostItemsMapping(PropertyGroup):
cost_item_id: IntProperty(name="cost_item_id")
csv_filepath: StringProperty(name="filepath")
if TYPE_CHECKING:
cost_item_id: int
csv_filepath: str
class CostItemType(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -281,7 +272,6 @@ class BIMCostProperties(PropertyGroup):
custom_currency: StringProperty(
name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format"
)
cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostItemsMapping)
if TYPE_CHECKING:
cost_schedule_predefined_types: str
@@ -336,4 +326,3 @@ class BIMCostProperties(PropertyGroup):
show_cost_item_operators: bool
currency: str
custom_currency: str
cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostItemsMapping]
+1 -42
View File
@@ -71,26 +71,6 @@ class BIM_PT_cost_schedules(Panel):
text="Currently editing: {}[{}]".format(cost_schedule["name"], cost_schedule["predefined_type"]),
icon="LINENUMBERS_ON",
)
row0 = self.layout.row(align=True)
col = row0.column()
col.label(text="Linked CSV:")
row_1 = col.row(align=True)
if self.props.active_cost_schedule_id in [item.cost_item_id for item in self.props.cost_schedule_files]:
file = next(
(
item.csv_filepath
for item in self.props.cost_schedule_files
if item.cost_item_id == self.props.active_cost_schedule_id
),
None,
)
row_1.label(text=file)
row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="")
else:
row_1.label(text="No CSV file found")
row_1.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col = grid.column()
row1 = col.row(align=True)
@@ -384,29 +364,8 @@ class BIM_PT_cost_item_types(Panel):
# TODO
col = grid.column()
has_quantity_names = CostProp.get_resource_quantity_names(self, context)
row2 = col.row(align=True)
# row2.label(text="Resources")
total_cost_item_resources = len(self.props.cost_item_resources)
row2.label(text="Resources({})".format(total_cost_item_resources))
op = row2.operator("bim.calculate_cost_item_resource_value", text="", icon="DISC")
op.cost_item = cost_item.ifc_definition_id
rtprops = context.scene.BIMResourceTreeProperties
rprops = context.scene.BIMResourceProperties
if rtprops.resources and rprops.active_resource_index < len(rtprops.resources):
if has_quantity_names:
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES")
op.related_object_type = "RESOURCE"
op.cost_item = cost_item.ifc_definition_id
op.prop_name = self.props.resource_quantity_names
op = row2.operator("bim.assign_cost_item_quantity", text="", icon="ADD")
op.related_object_type = "RESOURCE"
op.cost_item = cost_item.ifc_definition_id
op.prop_name = ""
row2.label(text="Resources")
row2 = col.row()
row2.template_list(
@@ -44,7 +44,6 @@ classes = (
operator.SelectHighPolygonMeshes,
operator.SelectHighestPolygonMeshes,
operator.ToggleDetailedIOSLogs,
operator.ValidateIfcAssets,
operator.ValidateIfcFile,
prop.BIMDebugProperties,
ui.BIM_PT_debug,
@@ -37,7 +37,6 @@ 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
@@ -153,67 +152,6 @@ 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"
-2
View File
@@ -50,8 +50,6 @@ 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")
@@ -132,14 +132,13 @@ class RemoveDocument(bpy.types.Operator, tool.Ifc.Operator):
class AssignDocument(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_document"
bl_label = "Assign Document"
bl_description = "Assign active document to the selected objects."
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
document: bpy.props.IntProperty()
def _execute(self, context):
document = tool.Ifc.get().by_id(self.document)
objs = [bpy.data.objects[self.obj]] if self.obj else tool.Blender.get_selected_objects()
objs = [bpy.data.objects.get(self.obj)] if self.obj else tool.Blender.get_selected_objects()
for obj in objs:
element = tool.Ifc.get_entity(obj)
if element:
@@ -30,7 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
@@ -79,7 +79,3 @@ class BIMDocumentProperties(PropertyGroup):
breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
active_document_index: int
is_editing: bool
@property
def active_document(self) -> Union[Document, None]:
return tool.Blender.get_active_uilist_element(self.documents, self.active_document_index)
+2 -5
View File
@@ -61,17 +61,14 @@ class BIM_PT_documents(Panel):
if self.props.breadcrumbs:
row.operator("bim.add_document_reference", text="", icon="FILE_HIDDEN")
active_document = self.props.active_document
if self.props.active_document_id:
row.operator("bim.edit_document", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
elif active_document:
ifc_definition_id = active_document.ifc_definition_id
elif self.props.documents and self.props.active_document_index < len(self.props.documents):
ifc_definition_id = self.props.documents[self.props.active_document_index].ifc_definition_id
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
ifc_definition_id
)
row.operator("bim.assign_document", text="", icon="BRUSH_DATA").document = ifc_definition_id
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
+29 -23
View File
@@ -34,7 +34,9 @@ def refresh():
DrawingsData.is_loaded = False
ElementFiltersData.is_loaded = False
AnnotationData.is_loaded = False
DecoratorData.is_loaded = False
DecoratorData.data = {}
DecoratorData.cut_cache = {}
DecoratorData.layerset_cache = {}
class ProductAssignmentsData:
@@ -237,25 +239,6 @@ class DecoratorData:
slice_cache = {}
fill_cache = {}
@classmethod
def load(cls):
cls.is_loaded = True
cls.cut_cache = {}
cls.layerset_cache = {}
text = {}
dimension = {}
for obj in bpy.context.visible_objects:
if not (element := tool.Ifc.get_entity(obj)):
continue
if tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
text[obj.name] = cls.get_ifc_text_data(obj)
elif tool.Drawing.is_annotation_object_type(
element, ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
):
dimension[obj.name] = cls.get_dimension_data(obj)
cls.data = {"text": text, "dimension": dimension}
@classmethod
def get_batting_thickness(cls, obj):
"""used by IfcAnnotations with ObjectType = "BATTING" """
@@ -313,10 +296,17 @@ class DecoratorData:
return display_data
@classmethod
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict:
def get_ifc_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
returns font size in mm for current ifc text object"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]):
return None
props = tool.Drawing.get_text_props(obj)
# getting font size
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
@@ -354,7 +344,9 @@ class DecoratorData:
literals_data.append(literal_data)
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
cls.data[obj.name] = text_data
return text_data
@classmethod
def get_symbol(cls, obj: bpy.types.Object) -> Union[str, None]:
@@ -367,7 +359,19 @@ class DecoratorData:
DIMENSION / DIAMETER / SECTION_LEVEL / PLAN_LEVEL / RADIUS
"""
result = cls.data.get(obj.name, None)
if result is not None:
return result
element = tool.Ifc.get_entity(obj)
supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
if (
not element
or not element.is_a("IfcAnnotation")
or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types
):
return None
dimension_style = "arrow"
fill_bg = False
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
@@ -386,7 +390,7 @@ class DecoratorData:
custom_unit_list = pset_data.get("CustomUnit", None) or ""
custom_unit = custom_unit_list[0] if custom_unit_list else ""
return {
dimension_data = {
"dimension_style": dimension_style,
"show_description_only": show_description_only,
"suppress_zero_inches": suppress_zero_inches,
@@ -395,6 +399,8 @@ class DecoratorData:
"fill_bg": fill_bg,
"custom_unit": custom_unit,
}
cls.data[obj.name] = dimension_data
return dimension_data
class AnnotationData:
@@ -590,7 +590,7 @@ class BaseDecorator:
"""if `text_world_position` is not provided, the object's location will be used"""
if not text_world_position:
text_world_position = obj.matrix_world.translation
text_world_position = obj.location
region = context.region
region3d = context.region_data
@@ -599,7 +599,7 @@ class BaseDecorator:
if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)):
return
props = tool.Drawing.get_text_props(obj)
text_data = DecoratorData.data["text"].get(obj.name, None)
text_data = DecoratorData.get_ifc_text_data(obj)
if props.is_editing:
text_data = text_data | props.get_text_edited_data()
literals_data = text_data["Literals"]
@@ -656,10 +656,7 @@ class DimensionDecorator(BaseDecorator):
viewportDrawingScale = self.get_viewport_drawing_scale(context)
# setup geometry parameters
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_style = dimension_data["dimension_style"]
dimension_style = DecoratorData.get_dimension_data(obj)["dimension_style"]
if dimension_style == "oblique":
size = viewportDrawingScale * 10 # OLBIQUE_SYMBOL_SIZE
angle = radians(45)
@@ -721,9 +718,7 @@ class DimensionDecorator(BaseDecorator):
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_data = DecoratorData.get_dimension_data(obj)
show_description_only = dimension_data["show_description_only"]
text_prefix = dimension_data["text_prefix"]
text_suffix = dimension_data["text_suffix"]
@@ -957,9 +952,7 @@ class RadiusDecorator(BaseDecorator):
return
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_data = DecoratorData.get_dimension_data(obj)
viewportDrawingScale = self.get_viewport_drawing_scale(context)
text_offset = 20 * viewportDrawingScale
@@ -1186,9 +1179,7 @@ class PlanLevelDecorator(BaseDecorator):
element = tool.Ifc.get_entity(obj)
description = element.Description
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_data = DecoratorData.get_dimension_data(obj)
for verts in splines:
p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]]
@@ -1271,9 +1262,7 @@ class SectionLevelDecorator(BaseDecorator):
storey = tool.Drawing.get_annotation_element(element)
tag = storey.Name if storey else element.Description
dimension_data = DecoratorData.data["dimension"].get(obj.name, None)
if not dimension_data:
return
dimension_data = DecoratorData.get_dimension_data(obj)
for verts in splines:
@@ -1732,11 +1721,11 @@ class CutDecorator:
self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
if tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.cut_cache[element.id()] = (False, False)
else:
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
DecoratorData.cut_cache[element.id()] = (verts, edges)
else:
DecoratorData.cut_cache[element.id()] = (False, False)
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
element_id = element.id()
@@ -1920,8 +1909,6 @@ class DecorationsHandler:
def install(cls, context):
if cls.installed:
cls.uninstall()
if not DecoratorData.is_loaded:
DecoratorData.load()
handler = cls()
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
+23 -17
View File
@@ -37,23 +37,29 @@ def depsgraph_update_pre_handler(scene):
def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
"""Sync scene render resolution with the active drawing
and prevent user from manually changing ``ortho_scale`` on IFC camera."""
props = tool.Drawing.get_document_props()
camera_obj = scene.camera
if not camera_obj or "/" not in camera_obj.name or not props.drawings:
if not scene.camera or "/" not in scene.camera.name or not props.drawings:
return
assert isinstance((camera := camera_obj.data), bpy.types.Camera)
props = tool.Drawing.get_camera_props(camera)
if camera.type != props.camera_type:
camera.type = props.camera_type
ortho_scale, aspect_ratio = props.get_scale_and_aspect_ratio()
scene_render = scene.render
if (camera.ortho_scale != ortho_scale) or not tool.Cad.is_x(
scene_render.resolution_x / scene_render.resolution_y, aspect_ratio
assert isinstance(scene.camera.data, bpy.types.Camera)
props = scene.camera.data.BIMCameraProperties
ortho_scale = max((props.width, props.height))
aspect_ratio = props.width / props.height
if (scene.camera.data.ortho_scale != ortho_scale) or (
scene.render.resolution_x / scene.render.resolution_y != aspect_ratio
):
raster_x, raster_y = props.update_camera_resolution()
scene_render.resolution_x = raster_x
scene_render.resolution_y = raster_y
scene.camera.data.ortho_scale = ortho_scale
diagram_scale = tool.Drawing.get_diagram_scale(scene.camera)
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
if props.width > props.height:
aspect_ratio = props.height / props.width
raster_x = ortho_scale * scale_ratio * props.dpi / 0.0254
raster_y = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
else:
aspect_ratio = props.width / props.height
raster_x = ortho_scale * aspect_ratio * scale_ratio * props.dpi / 0.0254
raster_y = ortho_scale * scale_ratio * props.dpi / 0.0254
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
@@ -413,15 +413,13 @@ def get_project_collection(scene):
return colls[0]
def parse_diagram_scale(camera: bpy.types.Camera) -> float:
def parse_diagram_scale(camera):
"""Returns numeric value of scale"""
props = tool.Drawing.get_camera_props(camera)
if props.diagram_scale == "CUSTOM":
numerator = props.custom_scale_numerator
denominator = props.custom_scale_denominator
if camera.BIMCameraProperties.diagram_scale == "CUSTOM":
_, fraction = camera.BIMCameraProperties.custom_diagram_scale.split("|")
else:
_, fraction = props.diagram_scale.split("|")
numerator, denominator = fraction.split("/")
_, fraction = camera.BIMCameraProperties.diagram_scale.split("|")
numerator, denominator = fraction.split("/")
return float(numerator) / float(denominator)
@@ -433,11 +431,12 @@ def ortho_view_frame(
Similar to `bpy.types.Camera.view_frame`
:arg camera: camera of drawing
:type camera: bpy.types.Camera + BIMCameraProperties
:arg margin: margins, in scene units
:type margin: float
:return: (xmin, xmax, ymin, ymax, zmin, zmax) in local camera coordinates
"""
props = tool.Drawing.get_camera_props(camera)
aspect = props.raster_y / props.raster_x
aspect = camera.BIMCameraProperties.raster_y / camera.BIMCameraProperties.raster_x
size = camera.ortho_scale
hwidth = size * 0.5
hheight = size * 0.5 * aspect
+126 -155
View File
@@ -46,7 +46,7 @@ import bonsai.bim.export_ifc
from bpy_extras.io_utils import ImportHelper
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional, Literal, TYPE_CHECKING, Any
from typing import NamedTuple, List, Union, Optional, Literal
from lxml import etree
from math import radians
from mathutils import Vector, Color, Matrix
@@ -56,10 +56,6 @@ from bonsai.bim.ifc import IfcStore
from pathlib import Path
from bpy_extras.image_utils import load_image
if TYPE_CHECKING:
from bonsai.bim.module.project.prop import Link
from bpy._typing import rna_enums
cwd = os.path.dirname(os.path.realpath(__file__))
@@ -184,9 +180,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Drawing.get_document_props()
core.duplicate_drawing(
tool.Ifc,
tool.Blender,
tool.Drawing,
tool.Geometry,
drawing=tool.Ifc.get().by_id(self.drawing),
should_duplicate_annotations=self.should_duplicate_annotations,
)
@@ -224,7 +218,6 @@ class CreateDrawing(bpy.types.Operator):
)
drawing_name: str
is_manifold_cache: dict[str, bool]
@classmethod
def poll(cls, context):
@@ -233,9 +226,7 @@ class CreateDrawing(bpy.types.Operator):
if not tool.Drawing.is_drawing_active():
cls.poll_message_set("No active drawing.")
return False
assert context.scene
assert (camera_obj := context.scene.camera)
if tool.Drawing.get_camera_props(camera_obj).linework_mode == "FREESTYLE" and not hasattr(
if context.scene.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE" and not hasattr(
context.scene, "svg_export"
):
cls.poll_message_set(
@@ -255,7 +246,6 @@ class CreateDrawing(bpy.types.Operator):
def execute(self, context):
self.props = tool.Drawing.get_document_props()
assert context.scene and context.scene.camera
active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera)
if self.print_all:
@@ -270,22 +260,19 @@ class CreateDrawing(bpy.types.Operator):
bpy.ops.bim.activate_drawing(drawing=drawing_id, should_view_from_camera=False)
self.camera = context.scene.camera
assert (camera_element := tool.Ifc.get_entity(self.camera))
self.camera_element = camera_element
self.camera_element = tool.Ifc.get_entity(self.camera)
self.camera_document = tool.Drawing.get_drawing_document(self.camera_element)
self.file = tool.Ifc.get()
with profile("Drawing generation process"):
with profile("Initialize drawing generation process"):
self.cprops = tool.Drawing.get_camera_props(self.camera)
self.cprops = self.camera.data.BIMCameraProperties
self.drawing = self.file.by_id(drawing_id)
self.drawing_name = self.drawing.Name
self.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
self.get_scale(context)
if self.cprops.update_representation(self.camera.matrix_world):
if self.cprops.update_representation(self.camera):
bpy.ops.bim.update_representation(obj=self.camera.name, ifc_representation_class="")
# Reassign props as data is recreated during the update.
self.cprops = tool.Drawing.get_camera_props(self.camera)
self.svg_writer = svgwriter.SvgWriter()
self.svg_writer.human_scale = self.human_scale
@@ -308,11 +295,11 @@ class CreateDrawing(bpy.types.Operator):
with profile("Generate linework"):
if tool.Drawing.is_camera_orthographic():
if self.cprops.linework_mode == "OPENCASCADE":
if self.camera.data.BIMCameraProperties.linework_mode == "OPENCASCADE":
linework_svg = self.generate_linework(context)
elif self.cprops.linework_mode == "FREESTYLE":
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
elif self.cprops.linework_mode == "FREESTYLE":
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
with profile("Generate annotation"):
@@ -812,6 +799,7 @@ 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]
@@ -835,8 +823,10 @@ class CreateDrawing(bpy.types.Operator):
files = {bim_props.ifc_file: tool.Ifc.get()}
props = tool.Project.get_project_props()
for link in props.get_loaded_links():
files[link.name] = self.get_linked_file(link)
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
self.setup_serialiser(target_view)
@@ -883,31 +873,36 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
if self.cprops.cut_mode == "BISECT":
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
elif self.cprops.cut_mode == "OPENCASCADE":
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
self.generate_wall_layers(context, root)
self.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root)
if self.cprops.fill_mode == "SHAPELY":
if self.camera.data.BIMCameraProperties.fill_mode == "SHAPELY":
# shapely variant
group = root.find("{http://www.w3.org/2000/svg}g")
nm = group.attrib["{http://www.ifcopenshell.org/ns}name"]
m4 = np.array(json.loads(group.attrib["{http://www.ifcopenshell.org/ns}plane"]))
m3 = np.array(json.loads(group.attrib["{http://www.ifcopenshell.org/ns}matrix3"]))
m44 = np.eye(4)
m44[0][0:2] = m3[0][0:2]
m44[1][0:2] = m3[1][0:2]
m44[0][3] = m3[0][2]
m44[1][3] = m3[1][2]
m44 = np.linalg.inv(m44)
raycast_objs = set()
elements_with_faces = set()
for element in drawing_elements.copy():
if element.is_a("IfcAnnotation"):
continue
obj = tool.Ifc.get_object(element)
if obj and obj.type == "MESH" and len(obj.data.polygons):
elements_with_faces.add(element.GlobalId)
raycast_objs.add(obj)
projections = root.xpath(
".//svg:g[contains(@class, 'projection')]", namespaces={"svg": "http://www.w3.org/2000/svg"}
@@ -934,21 +929,23 @@ class CreateDrawing(bpy.types.Operator):
if polygon.area < 1:
continue
centroid = polygon.centroid
centroid = centroid if polygon.contains(centroid) else polygon.representative_point()
if centroid:
centroid3d = self.drawing_to_model_co(centroid.x, centroid.y)
inside_elements = [
e for e in tree.select(self.pythonize(centroid3d)) if not e.is_a("IfcAnnotation")
]
internal_point = centroid if polygon.contains(centroid) else polygon.representative_point()
if internal_point:
internal_point = [internal_point.x, internal_point.y]
a, b = self.drawing_to_model_co(m44, m4, internal_point, 0.0), self.drawing_to_model_co(
m44, m4, internal_point, -100.0
)
inside_elements = [e for e in tree.select(self.pythonize(a)) if not e.is_a("IfcAnnotation")]
if not inside_elements:
camera_dir = self.camera.matrix_world.col[2].to_3d() * -1
# We previously used tree.select_ray, but raycasting in Blender is 100x faster.
raycast_results = self.cast_rays_and_get_best_object(raycast_objs, centroid3d, camera_dir)
raycast_element = None
if raycast_obj := raycast_results[0]:
raycast_element = tool.Ifc.get_entity(raycast_obj)
if raycast_element:
elements = [
e
for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a))
if not e.instance.is_a("IfcAnnotation")
and tool.Cad.is_point_on_edge(
Vector(list(e.position)), (Vector(self.pythonize(a)), Vector(self.pythonize(b)))
)
]
if elements:
path = etree.Element("path")
d = (
"M"
@@ -962,12 +959,18 @@ class CreateDrawing(bpy.types.Operator):
+ " Z"
)
path.attrib["d"] = d
classes = self.get_svg_classes(raycast_element)
classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id()))
classes.append(f"intpoint-{internal_point}")
classes.append(f"ab-{a}, {b}")
for i, ray_result in enumerate(elements):
classes.append(f"el{i}-{ray_result.instance.id()}")
classes.append(f"el{i}-pos-{list(ray_result.position)}")
classes.append(f"el{i}-dst-{ray_result.distance}")
classes.append("surface")
path.set("class", " ".join(list(classes)))
group.insert(0, path)
if self.cprops.fill_mode == "SVGFILL":
if self.camera.data.BIMCameraProperties.fill_mode == "SVGFILL":
results = etree.tostring(root).decode("utf8")
svg_data_1 = results
from xml.dom.minidom import parseString
@@ -1022,6 +1025,18 @@ class CreateDrawing(bpy.types.Operator):
g2 = list(yield_groups(svg2))[0]
# These are attributes on the original group that we can use to reconstruct
# a 4x4 matrix of the projection used in the SVG generation process
nm = g1.getAttribute("ifc:name")
m4 = np.array(json.loads(g1.getAttribute("ifc:plane")))
m3 = np.array(json.loads(g1.getAttribute("ifc:matrix3")))
m44 = np.eye(4)
m44[0][0:2] = m3[0][0:2]
m44[1][0:2] = m3[1][0:2]
m44[0][3] = m3[0][2]
m44[1][3] = m3[1][2]
m44 = np.linalg.inv(m44)
# Loop over the cell paths
for pi, p in enumerate(g2.getElementsByTagName("path")):
d = p.getAttribute("d")
@@ -1038,34 +1053,30 @@ class CreateDrawing(bpy.types.Operator):
xy = list(map(float, p.getAttribute("ifc:pointInside").split(",")))
centroid3d = self.drawing_to_model_co(*xy)
a, b = self.drawing_to_model_co(m44, m4, xy, 0.0), self.drawing_to_model_co(m44, m4, xy, -100.0)
inside_elements = [
e for e in tree.select(self.pythonize(centroid3d)) if not e.is_a("IfcAnnotation")
]
inside_elements = [e for e in tree.select(self.pythonize(a)) if not e.is_a("IfcAnnotation")]
if inside_elements:
elements = None
if iteration != num_passes:
semantics[pi] = (inside_elements[0], -1)
else:
camera_dir = self.camera.matrix_world.col[2].to_3d() * -1
elements = [
e
for e in tree.select_ray(self.pythonize(centroid3d), self.pythonize(camera_dir))
for e in tree.select_ray(self.pythonize(a), self.pythonize(b - a))
if not e.instance.is_a("IfcAnnotation")
]
if elements:
classes = self.get_svg_classes(ifc.by_id(elements[0].instance.id()))
classes.append("projection")
classes.append("surface")
if iteration != num_passes:
semantics[pi] = elements[0]
else:
classes = ["projection"]
p.setAttribute("style", "foo")
p.setAttribute("style", "")
p.setAttribute("class", " ".join(classes))
if iteration != num_passes:
@@ -1227,7 +1238,7 @@ class CreateDrawing(bpy.types.Operator):
)
return classes
def is_manifold(self, obj) -> bool:
def is_manifold(self, obj):
result = self.is_manifold_cache.get(obj.data.name, None)
if result is not None:
return result
@@ -1243,41 +1254,34 @@ class CreateDrawing(bpy.types.Operator):
self.is_manifold_cache[obj.data.name] = True
return True
def get_linked_file(self, link: "Link") -> ifcopenshell.file:
link_path = link.name
ifc_file = IfcStore.session_files.get(link_path, None)
if ifc_file is not None:
return ifc_file
resolved_path = tool.Ifc.resolve_uri(link_path)
ifc_file = IfcStore.session_files[link_path] = ifcopenshell.open(resolved_path)
return ifc_file
def get_element_by_guid(self, guid: str) -> Union[ifcopenshell.entity_instance, None]:
def get_element_by_guid(self, guid):
try:
return tool.Ifc.get().by_guid(guid)
except RuntimeError:
except:
props = tool.Project.get_project_props()
for link in props.get_loaded_links():
ifc_file = self.get_linked_file(link)
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
return ifc_file.by_guid(guid)
except RuntimeError:
return IfcStore.session_files[link.name].by_guid(guid)
except:
continue
def get_element_by_id(self, step_id: Any) -> Union[ifcopenshell.entity_instance, None]:
def get_element_by_id(self, step_id):
try:
step_id = int(step_id)
except:
return
try:
return tool.Ifc.get().by_id(step_id)
except RuntimeError:
except:
props = tool.Project.get_project_props()
for link in props.get_loaded_links():
ifc_file = self.get_linked_file(link)
for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
return ifc_file.by_id(step_id)
except RuntimeError:
return IfcStore.session_files[link.name].by_id(step_id)
except:
continue
def remove_cut_linework(self, root):
@@ -1409,6 +1413,7 @@ class CreateDrawing(bpy.types.Operator):
if layer:
for query in join_criteria:
key = ifcopenshell.util.selector.get_element_value(layer, query)
print("got layer key", query, key)
if isinstance(key, (list, tuple)):
keys.extend(key)
else:
@@ -1463,45 +1468,14 @@ class CreateDrawing(bpy.types.Operator):
g.set("class", " ".join(list(classes)))
group.append(g)
def drawing_to_model_co(self, x: float, y: float) -> Vector:
camera_xy = np.array((x, -y)) / self.scale / 1000
camera_xy += np.array((self.cprops.width / -2, self.cprops.height / 2)) # top left offset
return self.camera.matrix_world @ Vector(camera_xy).to_3d()
def drawing_to_model_co(self, m44, m4, xy, z=0.0):
xyzw = m44 @ np.array(xy + [z, 1.0])
xyzw[1] *= -1.0
return (m4 @ xyzw)[0:3]
def pythonize(self, arr):
return tuple(map(float, arr))
def cast_rays_and_get_best_object(
self, objs_to_raycast: list[bpy.types.Object], ray_origin, ray_direction
) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]:
# This could be optimised even further with 2D box culling
best_length_squared = 1.0
best_obj = None
best_hit = None
best_face_index = None
for obj in objs_to_raycast:
matrix_inv = obj.matrix_world.inverted()
ray_origin_obj = matrix_inv @ ray_origin
ray_direction_obj = ray_direction.to_4d()
ray_direction_obj[3] = 0.0
ray_direction_obj = (matrix_inv @ ray_direction_obj).to_3d()
success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
if success:
hit = obj.matrix_world @ location
length_squared = (hit - ray_origin).length_squared
if best_obj is None or length_squared < best_length_squared:
best_length_squared = length_squared
best_obj = obj
best_hit = hit
best_face_index = face_index
if best_obj is not None:
return best_obj, best_hit, best_face_index
return None, None, None
def move_projection_to_bottom(self, root):
# IfcConvert puts the projection afterwards which is not correct since
# projection should be drawn underneath the cut.
@@ -1515,11 +1489,12 @@ class CreateDrawing(bpy.types.Operator):
def move_elements_to_top(self, root):
group = root.find("{http://www.w3.org/2000/svg}g")
bringtofront = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "BringToFront") or ""
bringtofront = [item.strip() for item in bringtofront.split(",") if item.strip()]
# TODO: Make this an assignable preference
classes_to_move = ["IfcColumn", "IfcBeam", "EPsetStatusStatus-NEW"]
# Iterate through classes in order of preference
for class_name in bringtofront:
for class_name in classes_to_move:
xpath_query = f".//svg:g[contains(@class, '{class_name}')]"
elements_to_move = root.xpath(xpath_query, namespaces={"svg": "http://www.w3.org/2000/svg"})
@@ -1935,6 +1910,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
sheet_builder = sheeter.SheetBuilder()
references = sheet_builder.build(sheet)
raster_references = [tool.Ifc.get_uri(r, use_relative_path=True) for r in references["RASTER"]]
# These variables will be made available to the evaluated commands
svg = references["SHEET"]
@@ -1953,6 +1929,11 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
reference_description = tool.Drawing.get_reference_description(reference)
if reference_description == "SHEET":
has_sheet_reference = True
elif reference_description == "RASTER":
if reference.Location in raster_references:
raster_references.remove(reference.Location)
else:
tool.Ifc.run("document.remove_reference", reference=reference)
if not has_sheet_reference:
reference = tool.Ifc.run("document.add_reference", information=sheet)
@@ -1964,6 +1945,18 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
),
)
for raster_reference in raster_references:
reference = tool.Ifc.run("document.add_reference", information=sheet)
tool.Ifc.run(
"document.edit_reference",
reference=reference,
attributes=tool.Drawing.generate_reference_attributes(
reference,
Location=tool.Ifc.get_uri(raster_reference, use_relative_path=True),
Description="RASTER",
),
)
if svg2pdf_command:
# With great power comes great responsibility. Example:
# [["inkscape", "svg", "-o", "pdf"]]
@@ -2128,17 +2121,15 @@ class ActivateModel(bpy.types.Operator):
return {"FINISHED"}
class ActivateDrawingBase(tool.Ifc.Operator):
# Ifc Operator is necessary, because sync_references may create or remove IFC elements.
def invoke(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
class ActivateDrawingBase:
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
self.should_view_from_camera = False
if event.type == "LEFTMOUSE" and event.shift:
self.use_quick_preview = True
return self.execute(context)
def _execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
def execute(self, context):
props = tool.Drawing.get_document_props()
if props.is_editing_drawings == False:
bpy.ops.bim.load_drawings()
@@ -2173,11 +2164,10 @@ class ActivateDrawingBase(tool.Ifc.Operator):
# Save drawing bounds to the .ifc file
camera = context.scene.camera
camera_props = tool.Drawing.get_camera_props(camera)
if camera_props.update_representation(camera.matrix_world):
camera_props = camera.data.BIMCameraProperties
if camera_props.update_representation(camera):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
# See 6452 and 6478.
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
@@ -2231,7 +2221,6 @@ 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"
@@ -2313,8 +2302,7 @@ class ReloadDrawingStyles(bpy.types.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
assert context.scene and (camera := context.scene.camera)
camera_props = tool.Drawing.get_camera_props(camera)
camera_props = context.scene.camera.data.BIMCameraProperties
# added this part as a temporary fallback
# TODO: should remove it a bit later when projects get more accommodated
@@ -2381,14 +2369,12 @@ class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
assert context.scene and (camera_obj := context.scene.camera)
props = tool.Drawing.get_document_props()
drawing_styles = props.drawing_styles
new = drawing_styles.add()
# drawing style is saved to ifc on rename
new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles)
camera_props = tool.Drawing.get_camera_props(camera_obj)
camera_props.active_drawing_style_index = len(drawing_styles) - 1
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = len(drawing_styles) - 1
return {"FINISHED"}
@@ -2399,11 +2385,9 @@ class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
assert context.scene and (camera_obj := context.scene.camera)
props = tool.Drawing.get_document_props()
props.drawing_styles.remove(self.index)
camera_props = tool.Drawing.get_camera_props(camera_obj)
camera_props.active_drawing_style_index = max(self.index - 1, 0)
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = max(self.index - 1, 0)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
@@ -2420,7 +2404,6 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def execute(self, context):
space = self.get_view_3d(context) # Do not remove. It is used later in eval
scene = context.scene
assert scene
style = {}
eval_namespace = {"context": context, "scene": scene, "space": space}
@@ -2458,9 +2441,7 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if self.index:
index = int(self.index)
else:
assert (camera := scene.camera)
props = tool.Drawing.get_camera_props(camera)
index = props.active_drawing_style_index
index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index
props = tool.Drawing.get_document_props()
props.drawing_styles[index].raster_style = json.dumps(style)
@@ -2535,10 +2516,8 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def execute(self, context):
scene = context.scene
assert scene and (camera := scene.camera)
camera_props = tool.Drawing.get_camera_props(camera)
ifc_file = tool.Ifc.get()
active_drawing_style_index = camera_props.active_drawing_style_index
active_drawing_style_index = scene.camera.data.BIMCameraProperties.active_drawing_style_index
props = tool.Drawing.get_document_props()
if active_drawing_style_index >= len(props.drawing_styles):
@@ -2858,8 +2837,7 @@ class AddDrawingStyleAttribute(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
assert context.scene and (camera := context.scene.camera)
props = tool.Drawing.get_camera_props(camera)
props = context.scene.camera.data.BIMCameraProperties
dprops = tool.Drawing.get_document_props()
dprops.drawing_styles[props.active_drawing_style_index].attributes.add()
return {"FINISHED"}
@@ -2873,8 +2851,7 @@ class RemoveDrawingStyleAttribute(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
assert context.scene and (camera := context.scene.camera)
props = tool.Drawing.get_camera_props(camera)
props = context.scene.camera.data.BIMCameraProperties
dprops = tool.Drawing.get_document_props()
dprops.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index)
return {"FINISHED"}
@@ -3310,7 +3287,7 @@ class DisableEditingDrawings(bpy.types.Operator, tool.Ifc.Operator):
class ExpandTargetView(bpy.types.Operator):
bl_idname = "bim.expand_target_view"
bl_label = "Expand Target View"
bl_description = "\nSHIFT+CLICK to expand all view categories"
bl_description = "\nSHIFT+CLICK to expand all view categories "
bl_options = {"REGISTER", "UNDO"}
target_view: bpy.props.StringProperty()
@@ -3421,15 +3398,11 @@ class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator):
filter_mode: bpy.props.StringProperty()
def _execute(self, context):
assert context.scene
obj = context.scene.camera
obj = bpy.context.scene.camera
if not obj:
return
assert (camera := context.scene.camera)
props = tool.Drawing.get_camera_props(camera)
props.filter_mode = self.filter_mode
obj.data.BIMCameraProperties.filter_mode = self.filter_mode
element = tool.Ifc.get_entity(obj)
assert element
if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
try:
@@ -3445,12 +3418,10 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
filter_mode: bpy.props.StringProperty()
def _execute(self, context):
assert context.scene
obj = context.scene.camera
obj = bpy.context.scene.camera
assert obj
props = tool.Drawing.get_camera_props(obj)
props = obj.data.BIMCameraProperties
element = tool.Ifc.get_entity(obj)
assert element
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
if self.filter_mode == "INCLUDE":
query = tool.Search.export_filter_query(props.include_filter_groups) or None
@@ -3458,7 +3429,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
elif self.filter_mode == "EXCLUDE":
query = tool.Search.export_filter_query(props.exclude_filter_groups) or None
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Exclude": query})
props.filter_mode = "NONE"
obj.data.BIMCameraProperties.filter_mode = "NONE"
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
+39 -192
View File
@@ -22,13 +22,11 @@ import json
import enum
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.drawing as core
import bonsai.bim.module.drawing.annotation as annotation
import bonsai.bim.module.drawing.decoration as decoration
from mathutils import Matrix
from bonsai.bim.prop import BIMFilterGroup
from bonsai.bim.module.drawing.data import DrawingsData, DecoratorData, SheetsData, AnnotationData
from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
@@ -46,7 +44,7 @@ from bpy.props import (
CollectionProperty,
BoolVectorProperty,
)
from typing import TYPE_CHECKING, Literal, Any, Callable, get_args
from typing import TYPE_CHECKING, Literal, Any
diagram_scales_enum = []
@@ -57,23 +55,23 @@ def purge():
diagram_scales_enum = []
def update_target_view(self: "DocProperties", context: bpy.types.Context) -> None:
def update_target_view(self, context):
DrawingsData.data["location_hint"] = DrawingsData.location_hint()
def get_location_hint(self: "DocProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
def get_location_hint(self, context):
if not DrawingsData.is_loaded:
DrawingsData.load()
return DrawingsData.data["location_hint"]
def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
def update_diagram_scale(self, context):
if not self.update_props:
return
assert context.scene
if not (camera := context.scene.camera) or camera.data != self.id_data:
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
if not (element := tool.Ifc.get_entity(camera)):
element = tool.Ifc.get_entity(context.scene.camera)
if not element:
return
try:
element = (
@@ -91,15 +89,13 @@ def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=diagram_scale)
self.update_camera_resolution()
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Drawing")
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale)
def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
assert context.scene
if not context.scene.camera or context.scene.camera.data != self.id_data:
return
element = tool.Ifc.get_entity(context.scene.camera)
@@ -122,9 +118,8 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"IsNTS": self.is_nts})
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
def get_diagram_scales(self, context):
global diagram_scales_enum
assert context.scene
if (
len(diagram_scales_enum) < 1
or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
@@ -205,57 +200,44 @@ def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None:
bpy.ops.bim.save_drawing_styles_data(rename_style=True, rename_style_from=old_value, rename_style_to=new_value)
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
def update_document_name(self, context):
document = tool.Ifc.get().by_id(self.ifc_definition_id)
core.update_document_name(tool.Ifc, tool.Drawing, document=document, name=self.name)
def update_has_underlay(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
def update_has_underlay(self, context):
update_layer(self, context, "HasUnderlay", self.has_underlay)
assert context.scene
# making sure that camera is active
if self.has_underlay and (context.scene.camera and context.scene.camera.data == self.id_data):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
def get_update_layer_callback(
camera_prop_name: str, pset_prop_name: str
) -> Callable[["BIMCameraProperties", bpy.types.Context], None]:
def update_layer_callback(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
update_layer(self, context, pset_prop_name, getattr(self, camera_prop_name))
return update_layer_callback
def update_has_linework(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
def update_has_linework(self, context):
update_layer(self, context, "HasLinework", self.has_linework)
def update_target_view(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if self.target_view != "MODEL_VIEW":
self.camera_type = "ORTHO"
update_layer(self, context, "TargetView", self.target_view)
def update_has_annotation(self, context):
update_layer(self, context, "HasAnnotation", self.has_annotation)
def update_dpi(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
def update_dpi(self, context):
update_layer(self, context, "DPI", self.dpi)
def update_linework_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
def update_linework_mode(self, context):
update_layer(self, context, "LineworkMode", self.linework_mode)
def update_fill_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
def update_fill_mode(self, context):
update_layer(self, context, "FillMode", self.fill_mode)
def update_cut_mode(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
def update_cut_mode(self, context):
update_layer(self, context, "CutMode", self.cut_mode)
def update_layer(self: "BIMCameraProperties", context: bpy.types.Context, name: str, value: Any) -> None:
assert context.scene
def update_layer(self, context, name, value):
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
@@ -305,24 +287,10 @@ class Variable(PropertyGroup):
prop_key: StringProperty(name="Property Key")
TargetView = Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
TARGET_VIEW_ITEMS: list[tuple[TargetView, str, str]] = [
("PLAN_VIEW", "Plan", ""),
("ELEVATION_VIEW", "Elevation", ""),
("SECTION_VIEW", "Section", ""),
("REFLECTED_PLAN_VIEW", "RCP", ""),
("MODEL_VIEW", "Model", ""),
]
class Drawing(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
name: StringProperty(name="Name", update=update_drawing_name)
target_view: EnumProperty(
name="Target View",
default="PLAN_VIEW",
items=TARGET_VIEW_ITEMS,
)
target_view: StringProperty(name="Target View")
is_selected: BoolProperty(name="Is Selected", default=True)
is_drawing: BoolProperty(name="Is Drawing", default=False)
is_expanded: BoolProperty(name="Is Expanded", default=True)
@@ -330,7 +298,7 @@ class Drawing(PropertyGroup):
if TYPE_CHECKING:
ifc_definition_id: int
name: str
target_view: TargetView
target_view: str
is_selected: bool
is_drawing: bool
is_expanded: bool
@@ -407,7 +375,13 @@ class DocProperties(PropertyGroup):
is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
is_editing_references: BoolProperty(name="Is Editing References", default=False)
target_view: EnumProperty(
items=TARGET_VIEW_ITEMS,
items=[
("PLAN_VIEW", "Plan", ""),
("ELEVATION_VIEW", "Elevation", ""),
("SECTION_VIEW", "Section", ""),
("REFLECTED_PLAN_VIEW", "RCP", ""),
("MODEL_VIEW", "Model", ""),
],
name="Target View",
default="PLAN_VIEW",
update=update_target_view,
@@ -499,22 +473,6 @@ class DocProperties(PropertyGroup):
classes_to_wireframe: str
def update_width_height(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
self.update_camera_resolution()
def update_camera_type(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
assert isinstance(camera := self.id_data, bpy.types.Camera)
camera.type = self.camera_type
CameraType = Literal["PERSP", "ORTHO"]
CAMERA_TYPE_ENUM_ITEMS: dict[CameraType, tuple[str, str]] = {
"ORTHO": ("Ortographic", "Most common camera for the drawings, supporting all of the features."),
"PERSP": ("Perspective", "The only avilable features for perspective camera: freestyle linework, underlay."),
}
class BIMCameraProperties(PropertyGroup):
linework_mode: EnumProperty(
items=[
@@ -544,30 +502,9 @@ class BIMCameraProperties(PropertyGroup):
name="Cut Mode",
update=update_cut_mode,
)
# EPset_Drawing.
has_underlay: BoolProperty(
name="Underlay",
default=False,
update=get_update_layer_callback("has_underlay", "HasUnderlay"),
)
has_linework: BoolProperty(
name="Linework",
default=True,
update=get_update_layer_callback("has_linework", "HasLinework"),
)
has_annotation: BoolProperty(
name="Annotation",
default=True,
update=get_update_layer_callback("has_annotation", "HasAnnotation"),
)
target_view: EnumProperty(
name="Target View",
default="PLAN_VIEW",
items=TARGET_VIEW_ITEMS,
update=update_target_view,
)
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
representation: StringProperty(name="Representation")
view_name: StringProperty(name="View Name")
diagram_scale: EnumProperty(items=get_diagram_scales, name="Drawing Scale", update=update_diagram_scale)
@@ -576,80 +513,25 @@ class BIMCameraProperties(PropertyGroup):
raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000)
dpi: IntProperty(name="DPI", default=75, update=update_dpi)
width: FloatProperty(name="Width", default=50, subtype="DISTANCE", update=update_width_height)
height: FloatProperty(name="Height", default=50, subtype="DISTANCE", update=update_width_height)
# Bonsai property is needed to prevent user from using unsupported panoramic camera.
camera_type: EnumProperty(
name="Camera Type",
default="ORTHO",
items=[(k, *v) for k, v in CAMERA_TYPE_ENUM_ITEMS.items()],
update=update_camera_type,
)
width: FloatProperty(name="Width", default=50, subtype="DISTANCE")
height: FloatProperty(name="Height", default=50, subtype="DISTANCE")
is_nts: BoolProperty(name="Is NTS", update=update_is_nts)
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
filter_mode: StringProperty(name="Filter Mode", default="NONE")
include_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Include Filter")
exclude_filter_groups: CollectionProperty(type=BIMFilterGroup, name="Exclude Filter")
update_props: BoolProperty(
name="Enable Props Auto Update",
description="Update related EPset_Drawing pset on any change in camera properties.",
default=True,
)
if TYPE_CHECKING:
linework_mode: Literal["OPENCASCADE", "FREESTYLE"]
fill_mode: Literal["NONE", "SHAPELY", "SVGFILL"]
cut_mode: Literal["BISECT", "OPENCASCADE"]
has_underlay: bool
has_linework: bool
has_annotation: bool
target_view: TargetView
representation: str
view_name: str
diagram_scale: str
custom_scale_numerator: str
custom_scale_denominator: str
raster_x: int
raster_y: int
dpi: int
width: float
height: float
camera_type: CameraType
is_nts: bool
active_drawing_style_index: int
filter_mode: str
include_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
exclude_filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
update_props: bool
update_props: BoolProperty(name="Enable Props Auto Update", default=True)
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
# By checking this, you will know whether or not the camera IFC representation needs to be refreshed
def update_representation(self, matrix_world: Matrix) -> bool:
"""Update ``representation`` based on current camera properties and the provided world matrix.
:return: ``True`` if ``representation`` was updated and
representation should also be updated in IFC.
"""
# Matrix is used instead of Object so this works before the Object exists,
# allowing all camera initialization to stay encapsulated in `create_camera`.
camera = self.id_data
assert isinstance(camera, bpy.types.Camera)
# Rounding is necessary to avoid float garbage differences
# forcing unnecessary representation update.
def round_(f: float) -> float:
return round(f, 6)
def update_representation(self, obj):
representation = json.dumps(
{
"type": self.camera_type,
"matrix": [[round_(v) for v in row] for row in matrix_world],
"matrix": [list(x) for x in obj.matrix_world],
"raster_x": self.raster_x,
"raster_y": self.raster_y,
"ortho_scale": round_(camera.ortho_scale),
"clip_end": round_(camera.clip_end),
"ortho_scale": obj.data.ortho_scale,
"clip_end": obj.data.clip_end,
}
)
if self.representation != representation:
@@ -657,41 +539,6 @@ class BIMCameraProperties(PropertyGroup):
return True
return False
def update_camera_resolution(self) -> tuple[int, int]:
"""Update ``camera.ortho_scale``, ``raster_x`` and ``raster_y``
based on current ``width`` and ``height`` and diagram scale props.
:return: tuple[resolution_x, resolution_y]
"""
assert isinstance(camera := self.id_data, bpy.types.Camera)
ortho_scale, aspect_ratio = self.get_scale_and_aspect_ratio()
aspect_ratio = self.width / self.height
camera.ortho_scale = ortho_scale
diagram_scale = tool.Drawing.get_diagram_scale(camera)
scale_ratio = tool.Drawing.get_scale_ratio(diagram_scale["Scale"])
if self.width > self.height:
aspect_ratio = self.height / self.width
raster_x = ortho_scale * scale_ratio * self.dpi / 0.0254
raster_y = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254
else:
aspect_ratio = self.width / self.height
raster_x = ortho_scale * aspect_ratio * scale_ratio * self.dpi / 0.0254
raster_y = ortho_scale * scale_ratio * self.dpi / 0.0254
raster_x, raster_y = int(raster_x), int(raster_y)
self.raster_x, self.raster_y = raster_x, raster_y
return raster_x, raster_y
def get_scale_and_aspect_ratio(self) -> tuple[float, float]:
"""
:return: A tuple of calculated ortho scale and aspect ratio values.
"""
ortho_scale = max(self.width, self.height)
aspect_ratio = self.width / self.height
return ortho_scale, aspect_ratio
DEFAULT_BOX_ALIGNMENT = [False] * 6 + [True] + [False] * 2
BOX_ALIGNMENT_POSITIONS = [
@@ -90,6 +90,7 @@ class SheetBuilder:
layout_dir = os.path.dirname(layout_path)
drawing_path = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_reference(drawing))
underlay_path = os.path.splitext(drawing_path)[0] + "-underlay.png"
if not os.path.exists(layout_path) or not os.path.exists(drawing_path):
raise FileNotFoundError
@@ -115,6 +116,16 @@ class SheetBuilder:
x, y = self.next_drawing_location(layout_root, view_width)
# add background
if os.path.isfile(underlay_path):
background = ET.SubElement(view, "image")
background.attrib["data-type"] = "background"
background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir)
background.attrib["x"] = str(x)
background.attrib["y"] = str(y)
background.attrib["width"] = str(view_width)
background.attrib["height"] = str(view_height)
# add foreground
if os.path.isfile(drawing_path):
foreground = ET.SubElement(view, "image")
@@ -395,11 +406,14 @@ class SheetBuilder:
images = view.findall("{http://www.w3.org/2000/svg}image")
background = None
foreground = None
view_title = None
for image in images:
if image.attrib["data-type"] == "foreground":
if image.attrib["data-type"] == "background":
background = image
elif image.attrib["data-type"] == "foreground":
foreground = image
elif image.attrib["data-type"] == "view-title":
view_title = image
@@ -409,6 +423,12 @@ class SheetBuilder:
svg = self.ensure_drawing_unique_styles(svg, drawing_id)
view.append(svg)
if background is not None:
background_path = os.path.join(self.layout_dir, self.get_href(background))
raster_path = os.path.join(self.sheets_dir, os.path.basename(background_path))
shutil.copy(background_path, raster_path)
self.references["RASTER"].append(raster_path)
if view_title is not None:
foreground_path = self.get_href(foreground)
data = reference.get_info()
@@ -501,14 +521,8 @@ class SheetBuilder:
self.scale = embedded.attrib.get("data-scale")
images = embedded.findall("{http://www.w3.org/2000/svg}image")
for image in images:
old_href = Path(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
if not os.path.isabs(old_href):
template_dir = Path(os.path.join(self.layout_dir, svg_path)).resolve().parent
old_href = Path(os.path.join(template_dir, old_href))
old_href = old_href.absolute().resolve().as_posix()
new_href = Path(os.path.join(self.sheets_dir, Path(old_href).name)).absolute().resolve().as_posix()
shutil.copy(old_href, new_href)
image.attrib["{http://www.w3.org/1999/xlink}href"] = Path(old_href).name
new_href = ntpath.basename(image.attrib.get("{http://www.w3.org/1999/xlink}href"))
image.attrib["{http://www.w3.org/1999/xlink}href"] = new_href
for child in embedded:
if "namedview" in child.tag:
continue
+9 -24
View File
@@ -16,7 +16,6 @@
# 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.bim.helper
import bonsai.tool as tool
@@ -29,10 +28,6 @@ from bonsai.bim.module.drawing.data import (
ElementFiltersData,
DecoratorData,
)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.drawing.prop import DocProperties, Drawing
class BIM_PT_camera(Panel):
@@ -44,21 +39,18 @@ class BIM_PT_camera(Panel):
bl_parent_id = "BIM_PT_tab_drawings"
def draw(self, context):
assert context.scene and self.layout
camera = context.scene.camera
if not camera:
if not (context.scene.camera and hasattr(context.scene.camera.data, "BIMCameraProperties")):
row = self.layout.row()
row.label(text="No Active Drawing", icon="ERROR")
return
if not tool.Ifc.get_entity(camera):
if "/" not in context.scene.camera.name:
self.layout.label(text="This is not a BIM camera.")
return
assert isinstance(camera_data := camera.data, bpy.types.Camera)
props = tool.Drawing.get_camera_props(camera)
self.layout.use_property_split = True
dprops = tool.Drawing.get_document_props()
props = context.scene.camera.data.BIMCameraProperties
col = self.layout.column(align=True)
row = col.row(align=True)
@@ -71,13 +63,6 @@ class BIM_PT_camera(Panel):
row.prop(props, "has_annotation", icon="MOD_EDGESPLIT")
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = self.layout.row(align=True)
row.prop(props, "target_view")
if props.target_view == "MODEL_VIEW":
row = self.layout.row()
row.prop(props, "camera_type")
row = self.layout.row()
row.prop(props, "linework_mode")
if props.linework_mode == "OPENCASCADE":
@@ -92,7 +77,7 @@ class BIM_PT_camera(Panel):
row.prop(props, "height")
row = self.layout.row()
row.prop(camera_data, "clip_end", text="Depth")
row.prop(context.scene.camera.data, "clip_end", text="Depth")
row = self.layout.row(align=True)
row.prop(props, "diagram_scale", text="Scale")
@@ -125,8 +110,7 @@ class BIM_PT_element_filters(Panel):
if not ElementFiltersData.is_loaded:
ElementFiltersData.load()
assert context.scene and (camera := context.scene.camera)
props = tool.Drawing.get_camera_props(camera)
props = context.scene.camera.data.BIMCameraProperties
if props.filter_mode == "INCLUDE":
bonsai.bim.helper.draw_filter(
@@ -175,9 +159,10 @@ class BIM_PT_drawing_underlay(Panel):
def draw(self, context):
layout = self.layout
layout.use_property_split = True
assert context.scene and (camera := context.scene.camera)
camera = context.scene.camera
assert camera
dprops = tool.Drawing.get_document_props()
props = tool.Drawing.get_camera_props(camera)
props = camera.data.BIMCameraProperties
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
if not DrawingsData.is_loaded:
@@ -622,7 +607,7 @@ class BIM_PT_text(Panel):
class BIM_UL_drawinglist(bpy.types.UIList):
def draw_item(self, context, layout, data: DocProperties, item: Drawing, icon, active_data, active_propname):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if not item:
layout.label(text="", translate=False)
return
@@ -56,7 +56,7 @@ classes = (
operator.OverrideMeshSeparate,
operator.OverrideModeSetEdit,
operator.OverrideModeSetObject,
operator.OverrideMoveSelect,
operator.OverrideMove,
operator.OverrideMoveMacro,
operator.OverrideOriginSet,
operator.OverrideOutlinerDelete,
@@ -95,7 +95,7 @@ classes = (
)
addon_keymaps: list[tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
addon_keymaps = []
@persistent
@@ -124,9 +124,9 @@ def register():
operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked")
operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate")
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate")
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move_select")
operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move")
operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate")
operator.OverrideMoveMacro.define("BIM_OT_override_move_select")
operator.OverrideMoveMacro.define("BIM_OT_override_move")
operator.OverrideMoveMacro.define("TRANSFORM_OT_translate")
bpy.types.Object.BIMGeometryProperties = bpy.props.PointerProperty(type=prop.BIMObjectGeometryProperties)
@@ -156,19 +156,11 @@ def register():
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_mode_set_edit", "TAB", "PRESS")
addon_keymaps.append((km, kmi))
# Deletion.
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS")
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS", shift=True)
kmi.properties.use_global = True
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS")
kmi.properties.confirm = False
addon_keymaps.append((km, kmi))
kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS", shift=True)
kmi.properties.confirm = False
kmi.properties.use_global = True
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new("bim.override_mode_set_object", "TAB", "PRESS")
File diff suppressed because it is too large Load Diff
+1 -21
View File
@@ -162,11 +162,6 @@ class RepresentationItemObject(PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
ifc_definition_id: IntProperty()
if TYPE_CHECKING:
name: str
obj: Union[bpy.types.Object, None]
ifc_definition_id: int
class ShapeAspect(PropertyGroup):
name: StringProperty(
@@ -288,14 +283,7 @@ 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",
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,
name="Representation Object", type=bpy.types.Object, update=update_representation_obj
)
item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject)
@@ -309,14 +297,6 @@ class BIMGeometryProperties(PropertyGroup):
blender_item.name = name
return blender_item
def remove_item_object_by_entity(self, item: ifcopenshell.entity_instance) -> None:
ifc_id = item.id()
for i, item_obj in enumerate(self.item_objs):
if item_obj.ifc_definition_id == ifc_id:
self.item_objs.remove(i)
return
assert False
def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool:
return bool(obj != bpy.context.active_object and obj.data)
+27 -11
View File
@@ -64,7 +64,6 @@ def object_menu(self, context):
self.layout.operator("bim.override_object_delete", icon="PLUGIN")
self.layout.operator("bim.override_paste_buffer", icon="PLUGIN")
self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN")
self.layout.menu("BIM_MT_separate", icon="PLUGIN")
def edit_mesh_menu(self, context):
@@ -77,20 +76,25 @@ class BIM_MT_separate(Menu):
bl_label = "IFC Separate"
def draw(self, context):
assert self.layout
self.layout.label(text="IFC Separate", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator_enum("bim.override_mesh_separate", "type")
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC Selection").type = "SELECTED"
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Material").type = "MATERIAL"
self.layout.operator("bim.override_mesh_separate", icon="PLUGIN", text="IFC By Loose Parts").type = "LOOSE"
# TODO: remove as it's the same as BIM_MT_separate?
class BIM_MT_hotkey_separate(Menu):
bl_idname = "BIM_MT_hotkey_separate"
bl_label = "Separate"
def draw(self, context):
assert self.layout
self.layout.label(text="IFC Separate", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator_enum("bim.override_mesh_separate", "type")
self.layout.operator("bim.override_mesh_separate", text="Selection").type = "SELECTED"
self.layout.operator("bim.override_mesh_separate", text="By Material").type = "MATERIAL"
self.layout.operator("bim.override_mesh_separate", text="By Loose Parts").type = "LOOSE"
self.layout.separator()
self.layout.label(text="Blender Separate", icon="BLENDER")
self.layout.operator("mesh.separate", text="Selection").type = "SELECTED"
self.layout.operator("mesh.separate", text="By Material").type = "MATERIAL"
self.layout.operator("mesh.separate", text="By Loose Parts").type = "LOOSE"
class BIM_MT_object_set_origin(Menu):
@@ -98,9 +102,21 @@ class BIM_MT_object_set_origin(Menu):
bl_label = "IFC Set Origin"
def draw(self, context):
assert self.layout
self.layout.label(text="IFC Set Origin", icon_value=bonsai.bim.icons["IFC"].icon_id)
self.layout.operator_enum("bim.override_origin_set", property="origin_type")
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Geometry to Origin").origin_type = (
"GEOMETRY_ORIGIN"
)
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Geometry").origin_type = (
"ORIGIN_GEOMETRY"
)
self.layout.operator("bim.override_origin_set", icon="PLUGIN", text="IFC Origin to 3D Cursor").origin_type = (
"ORIGIN_CURSOR"
)
self.layout.operator(
"bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Surface)"
).origin_type = "ORIGIN_CENTER_OF_MASS"
self.layout.operator(
"bim.override_origin_set", icon="PLUGIN", text="IFC Origin to Center of Mass (Volume)"
).origin_type = "ORIGIN_CENTER_OF_VOLUME"
def outliner_menu(self, context):
@@ -209,7 +225,7 @@ class BIM_PT_representation_items(Panel):
RepresentationItemsData.load()
props = tool.Geometry.get_geometry_props()
obj = tool.Geometry.get_active_or_representation_obj()
obj = props.representation_obj or tool.Blender.get_active_object()
assert obj
props = tool.Geometry.get_object_geometry_props(obj)
@@ -38,7 +38,6 @@ classes = (
operator.RefreshGit,
operator.SwitchRevision,
operator.InstallGit,
operator.RunGitDiff,
prop.IfcGitTag,
prop.IfcGitListItem,
prop.IfcGitProperties,
@@ -409,25 +409,3 @@ class InstallGit(bpy.types.Operator):
core.install_git(tool.IfcGit, self)
refresh()
return {"FINISHED"}
class RunGitDiff(bpy.types.Operator):
"""Run `git diff` for the current version of IFC file and the last saved one."""
bl_label = "Git Diff"
bl_idname = "ifcgit.git_diff"
bl_options = set()
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("No IFC file loaded.")
return False
if not tool.Ifc.get_path():
cls.poll_message_set("Current IFC file was never saved.")
return False
return True
def execute(self, context):
core.run_git_diff(tool.IfcGit, self)
return {"FINISHED"}
@@ -283,5 +283,3 @@ class IFCGIT_PT_revision_inspector(bpy.types.Panel):
"ifcgit.object_log",
icon="TEXT",
)
row = layout.row()
row.operator("ifcgit.git_diff")
@@ -392,7 +392,6 @@ ground_glow source ground
scene.add_surface(scene_path)
scene.add_source(sky_file_path)
print("Setting up view...")
assert isinstance(camera.data, bpy.types.Camera)
if camera.data.type == "PERSP":
# Perspective camera
camera_fov = camera.data.angle
@@ -548,11 +547,9 @@ class ViewFromSun(bpy.types.Operator):
def execute(self, context):
if not (camera := bpy.data.objects.get("SunPathCamera")):
camera = bpy.data.objects.new("SunPathCamera", bpy.data.cameras.new("SunPathCamera"))
assert isinstance(camera.data, bpy.types.Camera)
assert context.scene
camera.data.type = "ORTHO"
camera.data.ortho_scale = 100 # The default of 6m is too small
context.scene.collection.objects.link(camera)
bpy.context.scene.collection.objects.link(camera)
tool.Blender.activate_camera(camera)
props = context.scene.BIMSolarProperties
props.hour = props.hour # Just to refresh camera position
@@ -40,7 +40,6 @@ from . import (
railing,
roof,
mep,
external,
)
from typing import NamedTuple
@@ -77,7 +76,6 @@ classes = (
wall.ExtendWallsToWall,
wall.FlipWall,
wall.MergeWall,
wall.OffsetWalls,
wall.RecalculateWall,
wall.SplitWall,
wall.UnjoinWalls,
@@ -140,7 +138,6 @@ classes = (
prop.BIMRoofProperties,
prop.BIMPolylineProperties,
prop.BIMProductPreviewProperties,
prop.BIMExternalParametricGeometryProperties,
ui.BIM_PT_array,
ui.BIM_PT_stair,
ui.BIM_PT_sverchok,
@@ -150,7 +147,6 @@ 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,
@@ -202,7 +198,6 @@ classes = (
mep.MEPAddObstruction,
mep.MEPAddTransition,
mep.MEPAddBend,
external.ApplyExternalParametricGeometry,
)
addon_keymaps = []
@@ -260,9 +255,6 @@ 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)
@@ -285,7 +277,6 @@ 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)
+7 -15
View File
@@ -20,7 +20,6 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.pset
import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.tool as tool
@@ -34,16 +33,8 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
assert (obj := context.active_object)
assert (element := tool.Ifc.get_entity(obj))
ifc_file = tool.Ifc.get()
if not element.is_a("IfcElement") and not element.is_a("IfcAnnotation"):
self.report(
{"ERROR"},
f"Adding array to element of type '{element.is_a()}' is not supported. Supported types: IfcElement, IfcAnnotation.",
)
return {"CANCELLED"}
obj = context.active_object
element = tool.Ifc.get_entity(obj)
array = {
"children": [],
@@ -63,13 +54,14 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator):
data.append(array)
pset = tool.Ifc.get().by_id(pset["id"])
else:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Array")
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="BBIM_Array")
data = [array]
ifcopenshell.api.pset.edit_pset(
ifc_file,
ifcopenshell.api.run(
"pset.edit_pset",
tool.Ifc.get(),
pset=pset,
properties={"Parent": element.GlobalId, "Data": ifc_file.create_entity("IfcText", json.dumps(data))},
properties={"Parent": element.GlobalId, "Data": tool.Ifc.get().createIfcText(json.dumps(data))},
)
return {"FINISHED"}
+11 -21
View File
@@ -16,7 +16,6 @@
# 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
@@ -35,7 +34,6 @@ 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):
@@ -350,28 +348,21 @@ class PolylineDecorator:
cls.is_installed = False
@classmethod
def update(
cls,
event: bpy.types.Event,
tool_state: tool.Polyline.ToolState,
input_ui: tool.Polyline.PolylineUI,
snapping_point: Vector,
) -> None:
def update(cls, event, tool_state, input_ui, snapping_point):
cls.event = event
cls.tool_state = tool_state
cls.input_ui = input_ui
# TODO: unused?
@classmethod
def set_input_ui(cls, input_ui: tool.Polyline.PolylineUI) -> None:
def set_input_ui(cls, input_ui):
cls.input_ui = input_ui
@classmethod
def set_angle_axis_line(cls, start: Vector, end: Vector) -> None:
def set_angle_axis_line(cls, start, end):
cls.axis_start = start
cls.axis_end = end
def calculate_measurement_x_y_and_z(self, context: bpy.types.Context) -> None:
def calculate_measurement_x_y_and_z(self, context):
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
polyline_points = polyline_data[0].polyline_points if polyline_data else []
@@ -394,7 +385,7 @@ class PolylineDecorator:
return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle)
@classmethod
def calculate_polygon(cls, points: list[Vector]) -> dict[str, Any]:
def calculate_polygon(self, points):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
@@ -423,7 +414,7 @@ class PolylineDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_input_ui(self, context: bpy.types.Context) -> None:
def draw_input_ui(self, context):
texts = {
"D": "Distance: ",
"A": "Angle: ",
@@ -433,7 +424,6 @@ 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)
@@ -548,7 +538,7 @@ class PolylineDecorator:
polyline_verts = [Vector((p.x, p.y, p.z)) for p in polyline_points]
# Area
if measure_type == "POLY_AREA" and polyline_data.area:
if measure_type == "AREA" and polyline_data.area:
if len(polyline_verts) < 3:
return
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
@@ -566,7 +556,7 @@ class PolylineDecorator:
blf.draw(self.font_id, text)
# Length
if measure_type in {"POLYLINE", "POLY_AREA"}:
if measure_type in {"POLYLINE", "AREA"}:
if len(polyline_verts) < 3:
return
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
@@ -736,8 +726,8 @@ class PolylineDecorator:
polyline_points = polyline_data.polyline_points
else:
polyline_points = []
polyline_verts: list[Vector] = []
polyline_edges: list[list[int]] = []
polyline_verts = []
polyline_edges = []
for point_prop in polyline_points:
point = Vector((point_prop.x, point_prop.y, point_prop.z))
polyline_verts.append(point)
@@ -775,7 +765,7 @@ class PolylineDecorator:
# Area highlight
if polyline_data:
area = polyline_data.area.split(" ")[0]
if polyline_data.measurement_type == "POLY_AREA" and area:
if area:
if float(area) > 0:
tris = self.calculate_polygon(polyline_verts)["tris"]
self.draw_batch("TRIS", polyline_verts, transparent_color(decorator_color_special), tris)
@@ -1,40 +0,0 @@
# 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"}
+9 -21
View File
@@ -18,18 +18,14 @@
import bpy
import ifcopenshell.api
import ifcopenshell.api.grid
import bonsai.tool as tool
import bonsai.core.geometry
import bonsai.core.root
from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty
from mathutils import Vector
from typing import TYPE_CHECKING
def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get()
def add_object(self, context):
obj = bpy.data.objects.new("Grid", None)
obj.name = "Grid"
@@ -37,11 +33,6 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcGrid", should_add_representation=False
)
grid = tool.Ifc.get_entity(obj)
assert grid
# Requirement in IFC4+.
if tool.Ifc.get_schema() != "IFC2X3":
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj)
for i in range(0, self.total_u):
verts = [
@@ -55,7 +46,9 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
tag = chr(ord("A") + i)
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
result = ifcopenshell.api.grid.create_grid_axis(ifc_file, axis_tag=tag, uvw_axes="UAxes", grid=grid)
result = ifcopenshell.api.run(
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="UAxes", grid=grid
)
tool.Ifc.link(result, obj)
tool.Model.create_axis_curve(obj, result)
tool.Collector.assign(obj)
@@ -72,7 +65,9 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
tag = str(i + 1).zfill(2)
obj = bpy.data.objects.new(f"IfcGridAxis/{tag}", mesh)
result = ifcopenshell.api.grid.create_grid_axis(ifc_file, axis_tag=tag, uvw_axes="VAxes", grid=grid)
result = ifcopenshell.api.run(
"grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="VAxes", grid=grid
)
tool.Ifc.link(result, obj)
tool.Model.create_axis_curve(obj, result)
tool.Collector.assign(obj)
@@ -83,20 +78,13 @@ def add_object(self: "BIM_OT_add_object", context: bpy.types.Context) -> None:
class BIM_OT_add_object(Operator, tool.Ifc.Operator):
bl_idname = "mesh.add_grid"
bl_label = "Grid"
bl_description = "Add IfcGrid."
bl_options = {"REGISTER", "UNDO"}
u_spacing: FloatProperty(name="U Spacing", default=10, subtype="DISTANCE")
u_spacing: FloatProperty(name="U Spacing", default=10)
total_u: IntProperty(name="Number of U Grids", default=3)
v_spacing: FloatProperty(name="V Spacing", default=10, subtype="DISTANCE")
v_spacing: FloatProperty(name="V Spacing", default=10)
total_v: IntProperty(name="Number of V Grids", default=3)
if TYPE_CHECKING:
u_spacing: float
total_u: int
v_spacing: float
total_v: int
@classmethod
def poll(cls, context):
return tool.Ifc.get() and context.mode == "OBJECT"
+3 -11
View File
@@ -550,8 +550,6 @@ 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"
@@ -599,7 +597,6 @@ class PolylineOperator:
"Increment Angle": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_MMB_SCROLL"]},
"Modify Snap Point": {"icons": True, "keys": ["EVENT_M"]},
"Close Polyline": {"icons": True, "keys": ["EVENT_C"]},
"Offset": {"icons": True, "keys": ["EVENT_O"]},
"Remove Point": {"icons": True, "keys": ["EVENT_BACKSPACE"]},
}
@@ -683,10 +680,7 @@ class PolylineOperator:
for action, settings in instructions.items():
if settings["icons"]:
for key in settings["keys"]:
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="", icon=key)
self.layout.label(text=action)
else:
key = settings["keys"][0]
@@ -905,8 +899,7 @@ class PolylineOperator:
if self.mousemove_count == 2:
self.objs_2d_bbox = []
for obj in self.visible_objs:
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
self.objs_2d_bbox.append(bbox_2d)
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
if self.mousemove_count > 3:
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
@@ -995,8 +988,7 @@ class PolylineOperator:
self.tool_state.mode = "Mouse"
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
self.objs_2d_bbox.append(bbox_2d)
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
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)
+41 -4
View File
@@ -577,12 +577,49 @@ class AlignProduct(bpy.types.Operator):
align_type: AlignType
def execute(self, context):
try:
core.align_objects(tool.Blender, tool.Model, self.align_type)
except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e))
selected_objs = context.selected_objects
if len(selected_objs) < 2 or not context.active_object:
self.report({"ERROR"}, "Please select atleast 2 objects.")
return {"FINISHED"}
if self.align_type == "CENTERLINE":
point = context.active_object.matrix_world @ (
Vector(context.active_object.bound_box[0]) + (context.active_object.dimensions / 2)
)
elif self.align_type == "POSITIVE":
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[6])
elif self.align_type == "NEGATIVE":
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[0])
else:
assert_never(self.align_type)
active_x_axis = context.active_object.matrix_world.to_quaternion() @ Vector((1, 0, 0))
active_y_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 1, 0))
active_z_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 0, 1))
x_distances = self.get_axis_distances(point, active_x_axis, context)
y_distances = self.get_axis_distances(point, active_y_axis, context)
if abs(sum(x_distances)) < abs(sum(y_distances)):
for i, obj in enumerate(selected_objs):
obj.matrix_world = Matrix.Translation(active_x_axis * -x_distances[i]) @ obj.matrix_world
else:
for i, obj in enumerate(selected_objs):
obj.matrix_world = Matrix.Translation(active_y_axis * -y_distances[i]) @ obj.matrix_world
return {"FINISHED"}
def get_axis_distances(self, point: Vector, axis: Vector, context: bpy.types.Context) -> list[float]:
results = []
for obj in context.selected_objects:
if self.align_type == "CENTERLINE":
obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2))
elif self.align_type == "POSITIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[6])
elif self.align_type == "NEGATIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[0])
else:
assert_never(self.align_type)
results.append(mathutils.geometry.distance_point_to_plane(obj_point, point, axis))
return results
class LoadTypeThumbnails(bpy.types.Operator):
bl_idname = "bim.load_type_thumbnails"
@@ -36,6 +36,7 @@ import bonsai.core.root
from bonsai.bim.ifc import IfcStore
from math import pi, degrees, atan2
from mathutils import Vector, Matrix
from bonsai.bim.module.model.wall import DumbWallRecalculator
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union, Any, Optional
@@ -968,7 +969,7 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
obj.matrix_world @= rotate_matrix
bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(profile_objs)
tool.Model.recalculate_walls(layer2_objs)
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
+2 -42
View File
@@ -221,13 +221,13 @@ class BIMModelProperties(PropertyGroup):
items=[("EXTERIOR", "Exterior", ""), ("CENTER", "Center", ""), ("INTERIOR", "Interior", "")],
name="Vertical Layer Offset Type",
default="EXTERIOR",
description="Offset convention to reference line",
description="It's a convention that affects the offset to reference line",
)
offset_type_horizontal: bpy.props.EnumProperty(
items=[("TOP", "Top", ""), ("CENTER", "Center", ""), ("BOTTOM", "Bottom", "")],
name="Horizontal Layer Offset Type",
default="TOP",
description="Offset convention to reference line",
description="It's a convention that affects the offset to reference line",
)
offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line")
show_wall_axis: bpy.props.BoolProperty(
@@ -1142,43 +1142,3 @@ 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 -7
View File
@@ -36,6 +36,7 @@ from math import cos, pi
from mathutils import Vector, Matrix
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from bonsai.bim.module.model.wall import DumbWallRecalculator
from typing import Optional
@@ -676,9 +677,8 @@ 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")
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
offset = Vector((0.0, 0.0, -layer_params["offset"]))
rot_offset = offset @ rot_matrix
tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix
@@ -725,9 +725,8 @@ 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")
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
offset = Vector((0.0, 0.0, -layer_params["offset"]))
rot_offset = offset @ rot_matrix
tranlation_matrix = Matrix.Translation(rot_offset)
position = position @ tranlation_matrix
@@ -1034,5 +1033,5 @@ class RecalculateSlab(bpy.types.Operator, tool.Ifc.Operator):
if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement.is_a("IfcWall"):
walls.append(tool.Ifc.get_object(rel.RelatedElement))
tool.Model.recalculate_walls(walls)
DumbWallRecalculator().recalculate(walls)
return {"FINISHED"}
-43
View File
@@ -701,49 +701,6 @@ 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")
+140 -150
View File
@@ -24,12 +24,10 @@ 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
@@ -54,10 +52,7 @@ class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
return context.selected_objects
def _execute(self, context):
core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
@@ -71,7 +66,7 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
slab = None
walls: list[bpy.types.Object] = []
walls = []
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):
@@ -117,8 +112,8 @@ class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.geometry.connect_wall(
tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True
)
tool.Model.recreate_wall(element, obj)
tool.Model.recreate_wall(target_element, target_obj)
joiner.recreate_wall(element, obj)
joiner.recreate_wall(target_element, target_obj)
else:
self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element")
@@ -129,10 +124,10 @@ class AlignWall(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = """ Align the selected walls to the active wall:
'Ext.': align to the EXTERIOR face
'C/L': align to wall CENTER
'C/L': align to wall CENTERLINE
'Int.': align to the INTERIOR face"""
AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"]
AlignType = Literal["CENTERLINE", "EXTERIOR", "INTERIOR"]
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
items=((i, i, "") for i in get_args(AlignType))
)
@@ -140,11 +135,25 @@ class AlignWall(bpy.types.Operator):
if TYPE_CHECKING:
align_type: AlignType
@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
def execute(self, context):
try:
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), self.align_type)
except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e))
selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
for obj in selected_objects:
if obj == context.active_object:
continue
aligner = DumbWallAligner(obj, context.active_object)
if self.align_type == "CENTERLINE":
aligner.align_centerline()
elif self.align_type == "EXTERIOR":
aligner.align_first_layer()
elif self.align_type == "INTERIOR":
aligner.align_last_layer()
else:
assert_never(self.align_type)
return {"FINISHED"}
@@ -156,13 +165,10 @@ class FlipWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
return context.selected_objects
def _execute(self, context):
selected_objs = tool.Model.get_selected_mesh_objects()
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
joiner = DumbWallJoiner()
for obj in selected_objs:
joiner.flip(obj)
@@ -179,13 +185,10 @@ class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
if not tool.Model.has_selected_ifc_objects():
cls.poll_message_set("No IFC objects selected.")
return False
return True
return context.selected_objects
def _execute(self, context):
selected_objs = tool.Model.get_selected_mesh_objects()
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
for obj in selected_objs:
DumbWallJoiner().split(obj, context.scene.cursor.location)
return {"FINISHED"}
@@ -199,23 +202,12 @@ class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
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
return context.selected_objects
def _execute(self, context):
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)
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)
return {"FINISHED"}
@@ -226,94 +218,82 @@ class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
return context.selected_objects
def _execute(self, context):
objects = tool.Model.get_selected_mesh_ifc_objects()
tool.Model.recalculate_walls(objects)
DumbWallRecalculator().recalculate(context.selected_objects)
return {"FINISHED"}
class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_depth"
bl_label = "Update"
bl_description = "Update height for the selected objects."
bl_description = "Update Height"
bl_options = {"REGISTER", "UNDO"}
depth: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
return context.selected_objects
def _execute(self, context):
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:
layer2_objs = []
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
assert element
if not element:
return
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
continue
return
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
continue
return
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.geometry.disconnect_element(
ifc_file,
ifcopenshell.api.run(
"geometry.disconnect_element",
tool.Ifc.get(),
relating_element=rel.RelatingElement,
related_element=element,
)
layer2_objs.append(obj)
if layer2_objs:
tool.Model.recalculate_walls(layer2_objs)
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_extrusion_x_angle"
bl_label = "Update"
bl_description = "Update angle for the selected objects."
bl_description = "Update Angle"
bl_options = {"REGISTER", "UNDO"}
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
@classmethod
def poll(cls, context):
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
return context.selected_objects
def _execute(self, context):
layer2_objs: list[bpy.types.Object] = []
layer2_objs = []
other_objs = []
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())
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs:
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
assert element
if not element:
return
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
continue
return
extrusion = tool.Model.get_extrusion(representation)
if not extrusion:
continue
return
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
@@ -393,47 +373,26 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
obj.rotation_euler.z = current_z_rot
if layer2_objs:
tool.Model.recalculate_walls(layer2_objs)
DumbWallRecalculator().recalculate(layer2_objs)
return {"FINISHED"}
class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_layer_length"
bl_label = "Update"
bl_description = "Update length for the selected objects."
bl_description = "Update Length"
bl_options = {"REGISTER", "UNDO"}
length: bpy.props.FloatProperty()
@classmethod
def poll(cls, context):
if not tool.Model.has_selected_mesh_ifc_objects():
cls.poll_message_set("No mesh IFC objects selected.")
return False
return True
return context.selected_objects
def _execute(self, context):
joiner = DumbWallJoiner()
selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs:
for obj in context.selected_objects:
joiner.set_length(obj, self.length)
class OffsetWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.offset_walls"
bl_label = "Offset Walls"
bl_description = "Offset selected objects from their reference line."
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
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):
props = tool.Model.get_model_props()
core.offset_walls(tool.Ifc, tool.Blender, tool.Model, props.offset_type_vertical)
return {"FINISHED"}
class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator):
@@ -504,8 +463,12 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
material_set_usage = model.by_id(material.id())
# if material.is_a("IfcMaterialLayerSetUsage"):
attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense}
ifcopenshell.api.run("material.edit_layer_usage", model, usage=material_set_usage, attributes=attributes)
tool.Model.recalculate_walls([wall["obj"]])
ifcopenshell.api.run(
"material.edit_layer_usage",
model,
**{"usage": material_set_usage, "attributes": attributes},
)
DumbWallRecalculator().recalculate([wall["obj"]])
if walls:
if is_polyline_closed:
@@ -606,11 +569,11 @@ class DumbWallAligner:
# An alignment shifts the origin of all walls to the closest point on the
# local X axis of the reference wall. In addition, the Z rotation is copied.
# Z translations are ignored for alignment.
def set_reference_wall(self, reference_wall: bpy.types.Object):
def __init__(self, wall: bpy.types.Object, reference_wall: bpy.types.Object):
self.wall = wall
self.reference_wall = reference_wall
def align_centerline(self, wall: bpy.types.Object) -> None:
self.wall = wall
def align_centerline(self) -> None:
self.align_rotation()
l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5)
@@ -628,8 +591,7 @@ class DumbWallAligner:
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_last_layer(self, wall: bpy.types.Object) -> None:
self.wall = wall
def align_last_layer(self) -> None:
self.align_rotation()
if self.is_rotation_flipped():
@@ -652,8 +614,7 @@ class DumbWallAligner:
new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_first_layer(self, wall: bpy.types.Object) -> None:
self.wall = wall
def align_first_layer(self) -> None:
self.align_rotation()
if self.is_rotation_flipped():
@@ -695,6 +656,30 @@ class DumbWallAligner:
return round(degrees(angle) % 360) == 180
class DumbWallRecalculator:
def recalculate(self, walls: list[bpy.types.Object]) -> None:
queue: set[tuple[ifcopenshell.entity_instance, bpy.types.Object]] = set()
for wall in walls:
element = tool.Ifc.get_entity(wall)
if tool.Ifc.is_moved(wall):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall)
queue.add((element, wall))
for rel in getattr(element, "ConnectedTo", []):
obj = tool.Ifc.get_object(rel.RelatedElement)
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
queue.add((rel.RelatedElement, obj))
for rel in getattr(element, "ConnectedFrom", []):
obj = tool.Ifc.get_object(rel.RelatingElement)
if tool.Ifc.is_moved(obj):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
queue.add((rel.RelatingElement, obj))
joiner = DumbWallJoiner()
for element, wall in queue:
if tool.Model.get_usage_type(element) == "LAYER2" and wall:
joiner.recreate_wall(element, wall)
class DumbWallGenerator:
def __init__(self, relating_type):
self.relating_type = relating_type
@@ -926,7 +911,7 @@ class DumbWallPlaner:
else:
for rel in inverse.AssociatedTo:
walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects])
tool.Model.recalculate_walls([w for w in set(walls) if w])
DumbWallRecalculator().recalculate([w for w in set(walls) if w])
def regenerate_from_type(self, usecase_path, ifc_file, settings):
relating_type = settings["relating_type"]
@@ -957,7 +942,7 @@ class DumbWallPlaner:
if layer_set_direction:
material.LayerSetDirection = layer_set_direction
if material.LayerSetDirection == "AXIS2":
tool.Model.recalculate_walls([obj])
DumbWallRecalculator().recalculate([obj])
class DumbWallJoiner:
@@ -977,7 +962,7 @@ class DumbWallJoiner:
axis1 = tool.Model.get_wall_axis(wall1)
axis = copy.deepcopy(axis1["reference"])
body = copy.deepcopy(axis1["reference"])
tool.Model.recreate_wall(element1, wall1)
self.recreate_wall(element1, wall1, axis, body)
def split(self, wall1: bpy.types.Object, target: Vector) -> None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -990,6 +975,7 @@ class DumbWallJoiner:
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
axis1 = tool.Model.get_wall_axis(wall1)
axis2 = copy.deepcopy(axis1)
intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)):
return
@@ -1050,36 +1036,20 @@ class DumbWallJoiner:
# During the duplication process, filled voids are not copied. So we
# only need to check fillings on the original element1.
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings if r.RelatedOpeningElement.HasFillings]:
rel = opening.HasFillings[0]
filling = rel.RelatedBuildingElement
filling_obj = tool.Ifc.get_object(filling)
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
filling_location = filling_obj.matrix_world.translation
_, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis1["reference"])
if filling_position > cut_percentage:
# The filling should be moved from element1 to element2.
new_opening = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=opening)
new_opening.VoidsElements[0].RelatingBuildingElement = element2
if new_opening.ObjectPlacement and new_opening.ObjectPlacement.is_a("IfcLocalPlacement"):
if element2.ObjectPlacement:
new_opening.ObjectPlacement.PlacementRelTo = element2.ObjectPlacement
# For now, we do copy opening representations
if opening.Representation:
new_opening.Representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(), opening.Representation, exclude=["IfcGeometricRepresentationContext"]
)
rel.RelatedBuildingElement = element2
# Remove the old opening
ifcopenshell.api.run("feature.remove_feature", tool.Ifc.get(), feature=opening)
FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation)
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p3 = (wall1.matrix_world.inverted() @ intersect.to_3d()).to_2d() / unit_scale
self.set_axis(element1, p1, p3)
self.set_axis(element2, p3, p2)
tool.Model.recreate_wall(element1, wall1)
tool.Model.recreate_wall(element2, wall2)
self.recreate_wall(element1, wall1)
self.recreate_wall(element2, wall2)
def flip(self, wall1: bpy.types.Object) -> None:
if tool.Ifc.is_moved(wall1):
@@ -1106,9 +1076,9 @@ class DumbWallJoiner:
ifcopenshell.api.geometry.edit_object_placement(
tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False
)
tool.Model.recreate_wall(element1, wall1)
self.recreate_wall(element1, wall1)
def merge(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
def merge(self, wall1, wall2):
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):
@@ -1116,7 +1086,6 @@ 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)
@@ -1159,7 +1128,7 @@ class DumbWallJoiner:
related_connection=rel.RelatedConnectionType,
)
tool.Model.recreate_wall(element1, wall1)
self.recreate_wall(element1, wall1)
tool.Geometry.delete_ifc_object(wall2)
@@ -1192,7 +1161,7 @@ class DumbWallJoiner:
description="TOP",
)
tool.Model.recreate_wall(element1, wall1)
self.recreate_wall(element1, wall1)
def set_axis(self, wall, p1, p2):
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
@@ -1221,23 +1190,24 @@ class DumbWallJoiner:
self.set_axis(element1, p1, intersect)
else:
self.set_axis(element1, intersect, p2)
tool.Model.recreate_wall(element1, wall1)
self.recreate_wall(element1, wall1)
def set_length(self, wall1: bpy.types.Object, si_length: float) -> None:
def set_length(self, wall1, si_length):
element1 = tool.Ifc.get_entity(wall1)
assert element1
if not element1:
return
if tool.Ifc.is_moved(wall1):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
ifcopenshell.api.geometry.disconnect_path(tool.Ifc.get(), element=element1, connection_type="ATEND")
ifcopenshell.api.run("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)
p2[0] = p1[0] + si_length / unit_scale
self.set_axis(element1, p1, p2)
tool.Model.recreate_wall(element1, wall1)
self.recreate_wall(element1, wall1)
def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
def join_T(self, wall1, wall2):
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(wall2)
axis1 = tool.Model.get_wall_axis(wall1)
@@ -1249,7 +1219,8 @@ class DumbWallJoiner:
return
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
ifcopenshell.api.geometry.connect_path(
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
related_element=element1,
relating_element=element2,
@@ -1258,9 +1229,9 @@ class DumbWallJoiner:
description="BUTT",
)
tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
def connect(self, obj1, obj2):
wall1 = tool.Ifc.get_entity(obj1)
wall2 = tool.Ifc.get_entity(obj2)
if tool.Ifc.is_moved(obj1):
@@ -1268,8 +1239,27 @@ class DumbWallJoiner:
if tool.Ifc.is_moved(obj2):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj2)
ifcopenshell.api.geometry.connect_wall(tool.Ifc.get(), wall1=wall1, wall2=wall2)
tool.Model.recreate_wall(wall1, obj1)
tool.Model.recreate_wall(wall2, obj2)
self.recreate_wall(wall1, obj1)
self.recreate_wall(wall2, obj2)
def recreate_wall(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None) -> None:
rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=rep,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
tool.Geometry.record_object_materials(obj)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
matrix[:, 3] *= unit_scale
obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix)
tool.Geometry.record_object_position(obj)
def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed()
+16 -61
View File
@@ -23,7 +23,7 @@ import bpy.utils.previews
import bonsai.bim
import bonsai.tool as tool
import bonsai.core.model as core
from bonsai.bim.module.model.wall import DumbWallJoiner, DumbWallAligner
from bonsai.bim.module.model.wall import DumbWallJoiner
from bonsai.bim.helper import prop_with_search, draw_attribute
from bpy.types import WorkSpaceTool, Menu
from bonsai.bim.module.model.data import AuthoringData, ItemData
@@ -516,7 +516,6 @@ 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)
@@ -717,14 +716,12 @@ class EditObjectUI:
AuthoringData.load(ifc_element_type)
if context.region.type == "TOOL_HEADER":
aprops = tool.Aggregate.get_aggregate_props()
if aprops.in_aggregate_mode:
if context.scene.BIMAggregateProperties.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")
nprops = tool.Nest.get_nest_props()
if nprops.in_nest_mode:
if context.scene.BIMNestProperties.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")
@@ -773,12 +770,6 @@ class EditObjectUI:
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
row.prop(
data=cls.props, property="offset_type_vertical", text="Offset" if ui_context != "TOOL_HEADER" else ""
)
row.operator("bim.offset_walls", icon="FILE_REFRESH", text="")
elif AuthoringData.data["active_material_usage"] == "LAYER3":
row.prop(data=cls.props, property="x_angle", text="Angle" if ui_context != "TOOL_HEADER" else "A")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
@@ -918,9 +909,7 @@ 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 Underside", "S_E", bpy.ops.bim.extend_to_underside.__doc__, ui_context
)
add_layout_hotkey_operator(cls.layout, "Extend To Undersideb", "S_E", "", ui_context)
if AuthoringData.data["is_flippable_element"]:
cls.draw_flip(ui_context, row)
@@ -970,13 +959,6 @@ 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"]:
@@ -999,19 +981,12 @@ 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", description, ui_context)
add_layout_hotkey_operator(row, "Exterior", "S_X", "", ui_context)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Centreline", "S_C", description, ui_context)
add_layout_hotkey_operator(row, "Centreline", "S_C", "", ui_context)
row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row
add_layout_hotkey_operator(row, "Interior", "S_V", description, ui_context)
add_layout_hotkey_operator(row, "Interior", "S_V", "", 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)
@@ -1187,15 +1162,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
try:
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "CENTER")
except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e))
if bpy.ops.bim.align_wall.poll():
bpy.ops.bim.align_wall(align_type="CENTERLINE")
else:
try:
core.align_objects(tool.Blender, tool.Model, "CENTER")
except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e))
bpy.ops.bim.align_product(align_type="CENTERLINE")
def hotkey_S_E(self):
if not bpy.context.selected_objects or not (active_object := bpy.context.active_object):
@@ -1339,29 +1309,18 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects:
return
elif self.active_material_usage == "LAYER2":
try:
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "INTERIOR")
except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e))
bpy.ops.bim.align_wall(align_type="INTERIOR")
else:
try:
core.align_objects(tool.Blender, tool.Model, "POSITIVE")
except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e))
bpy.ops.bim.align_product(align_type="POSITIVE")
def hotkey_S_X(self):
if not bpy.context.selected_objects:
return
if self.active_material_usage == "LAYER2":
try:
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "EXTERIOR")
except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e))
if bpy.ops.bim.align_wall.poll():
bpy.ops.bim.align_wall(align_type="EXTERIOR")
else:
try:
core.align_objects(tool.Blender, tool.Model, "NEGATIVE")
except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e))
bpy.ops.bim.align_product(align_type="NEGATIVE")
def hotkey_S_Y(self):
if not bpy.context.selected_objects:
@@ -1375,12 +1334,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.add_boundary()
def hotkey_S_O(self):
if len(bpy.context.selected_objects) > 1:
if len(bpy.context.selected_objects) == 2:
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",):
@@ -20,7 +20,6 @@ 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
@@ -163,9 +162,8 @@ class NestDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_nest(self, context: bpy.types.Context) -> None:
props = tool.Nest.get_nest_props()
if props.in_nest_mode:
def draw_nest(self, context):
if context.scene.BIMNestProperties.in_nest_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
@@ -264,7 +262,7 @@ class NestModeDecorator:
return
region = context.region
rv3d = region.data
props = tool.Nest.get_nest_props()
props = context.scene.BIMNestProperties
aggregate_obj = props.editing_nest
if not aggregate_obj:
@@ -293,7 +291,7 @@ class NestModeDecorator:
def draw_nest_empty(self, context):
if context.mode == "EDIT_MESH":
return
props = tool.Nest.get_nest_props()
props = context.scene.BIMNestProperties
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 = tool.Nest.get_nest_props()
props = context.scene.BIMNestProperties
objs = [o.obj for o in props.editing_objects]
if props.in_nest_mode:
if context.space_data.local_view:
+4 -20
View File
@@ -32,11 +32,10 @@ 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: "BIMObjectNestProperties", context: bpy.types.Context) -> None:
def message(self, context: bpy.types.Context) -> None:
def update_relating_object(self, context):
def message(self, context):
self.layout.label(text="Please select a valid Ifc Element")
if self.relating_object is None:
@@ -50,19 +49,15 @@ 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: "BIMNestProperties", context: bpy.types.Context) -> None:
def update_nest_decorator(self, context):
if self.nest_decorator:
NestDecorator.install(bpy.context)
else:
NestDecorator.uninstall()
def update_nest_mode_decorator(self: "BIMNestProperties", context: bpy.types.Context) -> None:
def update_nest_mode_decorator(self, context):
if self.in_nest_mode:
NestModeDecorator.install(bpy.context)
else:
@@ -73,10 +68,6 @@ 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)
@@ -88,10 +79,3 @@ 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
+1 -2
View File
@@ -48,8 +48,7 @@ class BIM_PT_nest(Panel):
if not NestData.is_loaded:
NestData.load()
assert (obj := context.active_object)
props = tool.Nest.get_object_nest_props(obj)
props = context.active_object.BIMObjectNestProperties
if props.is_editing:
row = layout.row(align=True)
+2 -7
View File
@@ -16,15 +16,10 @@
# 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):
@@ -59,8 +54,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_(attribute: Attribute, row: bpy.types.UILayout) -> None:
if props.ifc_patch_recipes == "ExtractElements" and attribute.name == "Query":
def draw_callback_(_, row: bpy.types.UILayout) -> None:
if props.ifc_patch_recipes == "ExtractElements":
row.operator("bim.patch_query_from_selected", text="", icon="EYEDROPPER")
if props.ifc_patch_args_attr:
@@ -329,7 +329,7 @@ class MeasureDecorator:
all_positions = []
for i in range(len(polyline_points)):
if i < 1 and measure_type == "POLY_AREA":
if i < 1 and measure_type == "AREA":
continue
if i == 0:
continue
@@ -374,7 +374,7 @@ class MeasureDecorator:
polyline_verts = [Vector((p.x, p.y, p.z)) for p in polyline_points]
# Area
if measure_type == "POLY_AREA" and polyline_data.area:
if measure_type == "AREA" and polyline_data.area:
if len(polyline_verts) < 3:
continue
center = sum(polyline_verts, Vector()) / len(polyline_verts) # Center between all polyline points
@@ -392,7 +392,7 @@ class MeasureDecorator:
blf.draw(self.font_id, text)
# Length
if measure_type in {"POLYLINE", "POLY_AREA"}:
if measure_type in {"POLYLINE", "AREA"}:
if len(polyline_verts) < 3:
continue
total_length_text_coords = view3d_utils.location_3d_to_region_2d(region, rv3d, polyline_verts[-1])
@@ -455,7 +455,7 @@ class MeasureDecorator:
# Area highlight
if polyline_data:
area = polyline_data.area.split(" ")[0]
if polyline_data.measurement_type == "POLY_AREA" and area:
if area:
if float(area) > 0:
tris = self.calculate_polygon(polyline_verts)["tris"]
self.draw_batch("TRIS", polyline_verts, transparent_color(decorator_color_special), tris)
@@ -1103,8 +1103,8 @@ class LoadProjectElements(bpy.types.Operator):
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
def get_decomposition_elements(self) -> set[ifcopenshell.entity_instance]:
containers: set[ifcopenshell.entity_instance] = set()
def get_decomposition_elements(self):
containers = set()
for filter_category in self.props.filter_categories:
if not filter_category.is_selected:
continue
@@ -1116,15 +1116,15 @@ class LoadProjectElements(bpy.types.Operator):
container = None
elif self.file.schema != "IFC2X3" and container.is_a("IfcContext"):
container = None
elements: set[ifcopenshell.entity_instance] = set()
elements = set()
for container in containers:
for rel in container.ContainsElements:
elements.update(rel.RelatedElements)
self.append_decomposed_elements(elements)
return elements
def append_decomposed_elements(self, elements: set[ifcopenshell.entity_instance]) -> None:
decomposed_elements: set[ifcopenshell.entity_instance] = set()
def append_decomposed_elements(self, elements):
decomposed_elements = set()
for element in elements:
if element.IsDecomposedBy:
for subelement in element.IsDecomposedBy[0].RelatedObjects:
@@ -1133,26 +1133,26 @@ class LoadProjectElements(bpy.types.Operator):
self.append_decomposed_elements(decomposed_elements)
elements.update(decomposed_elements)
def get_ifc_class_elements(self) -> set[ifcopenshell.entity_instance]:
elements: set[ifcopenshell.entity_instance] = set()
def get_ifc_class_elements(self):
elements = set()
for filter_category in self.props.filter_categories:
if not filter_category.is_selected:
continue
elements.update(self.file.by_type(filter_category.name, include_subtypes=False))
return elements
def get_ifc_type_elements(self) -> set[ifcopenshell.entity_instance]:
elements: set[ifcopenshell.entity_instance] = set()
def get_ifc_type_elements(self):
elements = set()
for filter_category in self.props.filter_categories:
if not filter_category.is_selected:
continue
elements.update(ifcopenshell.util.element.get_types(self.file.by_id(filter_category.ifc_definition_id)))
return elements
def get_whitelist_elements(self) -> set[ifcopenshell.entity_instance]:
def get_whitelist_elements(self):
return set(ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query))
def get_blacklist_elements(self) -> set[ifcopenshell.entity_instance]:
def get_blacklist_elements(self):
return set(self.file.by_type("IfcElement")) - set(
ifcopenshell.util.selector.filter_elements(self.file, self.props.filter_query)
)
@@ -1192,9 +1192,6 @@ class LinkIfc(bpy.types.Operator, ImportHelper):
filepath: str
files: list[bpy.types.OperatorFileListElement]
directory: str
filter_glob: str
use_relative_path: bool
use_cache: bool
def draw(self, context):
pprops = tool.Project.get_project_props()
@@ -1307,9 +1304,6 @@ 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)
@@ -1435,11 +1429,23 @@ class ReloadLink(bpy.types.Operator):
filepath: bpy.props.StringProperty()
def execute(self, context):
filepath = Path(self.filepath)
def get_linked_ifcs() -> set[bpy.types.Library]:
return {
c.library
for c in bpy.data.collections
if "IfcProject" in c.name and c.library and Path(c.library.filepath) == filepath
}
for library in get_linked_ifcs():
library.reload()
is_abs = os.path.isabs(Path(self.filepath))
use_relative_path = not is_abs
bpy.ops.bim.unlink_ifc(filepath=self.filepath)
filepath = tool.Ifc.resolve_uri(self.filepath)
status = bpy.ops.bim.link_ifc(filepath=filepath, use_cache=False, use_relative_path=use_relative_path)
status = bpy.ops.bim.link_ifc(filepath=self.filepath, use_cache=False, use_relative_path=use_relative_path)
return {"FINISHED"}
@@ -2340,9 +2346,7 @@ class RefreshClippingPlanes(bpy.types.Operator):
data = region.data
props = tool.Project.get_project_props()
# See 6452 and 6478.
# if not len(props.clipping_planes) and not self.camera:
if not len(props.clipping_planes):
if not len(props.clipping_planes) and not self.camera:
data.use_clip_planes = False
else:
with bpy.context.temp_override(area=area, region=region):
@@ -2371,14 +2375,13 @@ class RefreshClippingPlanes(bpy.types.Operator):
clip_planes.append(clip_plane)
bm.free()
# 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)
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)]
+3 -9
View File
@@ -35,7 +35,7 @@ from bpy.props import (
IntProperty,
StringProperty,
)
from typing import TYPE_CHECKING, Literal, Union, get_args, Generator
from typing import TYPE_CHECKING, Literal, Union, get_args
from typing_extensions import assert_never
@@ -219,7 +219,7 @@ class FilterCategory(PropertyGroup):
class Link(PropertyGroup):
name: StringProperty(
name="Name",
description="Filepath to linked .ifc file, stored in posix format (could be relative to .ifc file, not to .blend)",
description="Filepath to linked .ifc file, stored in posix format (could be relative to .blend file, not to .ifc)",
)
is_loaded: BoolProperty(name="Is Loaded", default=False)
is_selectable: BoolProperty(name="Is Selectable", default=True)
@@ -372,7 +372,7 @@ class BIMProjectProperties(PropertyGroup):
items=get_template_file,
name="Template File",
description=(
"Template to use for a new project. All types from the template will be appended to a new project."
"Template to use for a new project. All types from the template will be appended to a new project)."
),
)
@@ -500,12 +500,6 @@ class BIMProjectProperties(PropertyGroup):
return self.library_breadcrumb[-1]
return None
def get_loaded_links(self) -> Generator[Link, None, None]:
for link in self.links:
if not link.is_loaded:
continue
yield link
class MeasureToolSettings(PropertyGroup):
measurement_type_items = [
+14 -99
View File
@@ -35,7 +35,6 @@ 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):
@@ -174,35 +173,24 @@ 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 non-ifc objects."
bl_description = "Assign the IFC Class to the selected 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]]
else:
objects = list(tool.Blender.get_selected_objects())
elif objects := context.selected_objects:
pass
elif obj := context.active_object:
objects = [obj]
if not objects:
self.report({"INFO"}, "No objects selected.")
@@ -215,20 +203,8 @@ 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)
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()
active_object = context.active_object
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
@@ -242,41 +218,13 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
)
continue
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 self.should_add_representation and isinstance(obj.data, bpy.types.Mesh) and obj.data.polygons:
if obj.scale != (1, 1, 1):
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 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)
if tool.Geometry.mesh_has_loose_geometry(obj.data):
self.report(
{"WARNING"},
@@ -304,21 +252,6 @@ 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,
@@ -326,27 +259,12 @@ 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 and is_representation_supported(),
should_add_representation=self.should_add_representation,
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)
# 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)
context.view_layer.objects.active = active_object
class UnlinkObject(bpy.types.Operator, tool.Ifc.Operator):
@@ -442,7 +360,6 @@ 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")
@@ -463,8 +380,6 @@ 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,7 +190,6 @@ class BIMRootProperties(PropertyGroup):
if TYPE_CHECKING:
contexts: str
name: str
description: str
ifc_product: str
ifc_class: str
@@ -40,7 +40,6 @@ from bpy.props import (
CollectionProperty,
)
from typing import TYPE_CHECKING, Literal, get_args
from typing_extensions import assert_never
class AddFilterGroup(Operator):
@@ -145,7 +144,6 @@ 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(
@@ -161,18 +159,17 @@ class Search(Operator):
elif self.property_group == "BIMSearchProperties":
props = tool.Search.get_search_props()
else:
assert_never(self.property_group)
raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.")
results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
)
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.")
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")
return {"FINISHED"}
@@ -239,7 +236,6 @@ class LoadSearch(Operator, tool.Ifc.Operator):
class ColourByProperty(Operator):
bl_idname = "bim.colour_by_property"
bl_label = "Colour by Property"
bl_description = "Color all visible objects by the provided property."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
@@ -276,8 +272,6 @@ class ColourByProperty(Operator):
colourscheme = {cs.name: {"colour": cs.colour[0:3], "total": 0} for cs in props.colourscheme}
for obj in context.visible_objects:
if obj.type not in ("MESH", "CURVE"):
continue
element = tool.Ifc.get_entity(obj)
if not element:
continue
@@ -368,7 +362,6 @@ class ColourByProperty(Operator):
class SelectByProperty(Operator):
bl_idname = "bim.select_by_property"
bl_label = "Select by Property"
bl_description = "Select objects based on currently selected colored property value."
bl_options = {"REGISTER", "UNDO"}
@classmethod
+1 -6
View File
@@ -202,12 +202,7 @@ class BIMSearchProperties(PropertyGroup):
filter_classes_index: IntProperty(name="Filter Classes Index")
filter_container: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level")
filter_container_index: IntProperty(name="Filter Level Index")
show_flat_colours: BoolProperty(
name="Flat Colours",
description="Toggle flat shading in the active viewport.",
default=False,
update=update_show_flat_colours,
)
show_flat_colours: BoolProperty(name="Flat Colours", default=False, update=update_show_flat_colours)
if TYPE_CHECKING:
element_key: str
@@ -659,7 +659,6 @@ 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"})
@@ -687,7 +686,6 @@ 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"})
@@ -716,7 +714,6 @@ 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"})
@@ -745,7 +742,6 @@ 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"})
@@ -774,7 +770,6 @@ 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"})
@@ -803,7 +798,6 @@ 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"})
@@ -837,7 +831,6 @@ 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,13 +24,11 @@ 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,
+3 -15
View File
@@ -19,7 +19,6 @@
import bpy
import bonsai.tool as tool
import ifcopenshell.util.element
from typing import Any
def refresh():
@@ -80,20 +79,9 @@ class SpatialData:
return label
@classmethod
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
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])
@classmethod
def is_directly_contained(cls):
@@ -73,85 +73,15 @@ 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 = (
"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_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_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
@@ -173,33 +103,12 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
pass
else:
return
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:
for element_obj in tool.Blender.get_selected_objects():
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"
+17 -35
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, cast, Any
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.spatial.prop import BIMSpatialDecompositionProperties, BIMContainer, Element
@@ -56,8 +56,6 @@ 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")
@@ -88,16 +86,14 @@ class BIM_PT_spatial(Panel):
row.label(text="No Spatial Container")
row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="")
if references := cast(list[dict[str, Any]], SpatialData.data["references"]):
if references := 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["name"], icon="LINKED")
op = row.operator("bim.dereference_from_provided_structure", icon="X", text="")
op.structure = reference["id"]
row.label(text=reference, icon="LINKED")
class BIM_PT_spatial_decomposition(Panel):
@@ -173,14 +169,7 @@ class BIM_PT_spatial_decomposition(Panel):
if not self.props.active_container:
return
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:
if not self.props.total_elements:
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
@@ -188,44 +177,37 @@ 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"
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 = row.operator("bim.set_element_visibility", icon="FULLSCREEN_EXIT", text="Isolate")
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
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="")
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
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")
@@ -130,7 +130,6 @@ class LoadsDecorator:
coord = Vector(coord)
rv3d = context.region_data
assert rv3d and context.region
perspective = rv3d.view_perspective
view_matrix = rv3d.view_matrix
point_view_space = view_matrix @ coord
@@ -303,7 +303,6 @@ 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)
@@ -740,7 +739,6 @@ class EnableEditingSurfaceStyle(bpy.types.Operator):
if self.ifc_class == "IfcSurfaceStyleLighting":
def callback(attribute_name, _, data):
assert attributes
color = attributes.add()
color.name = attribute_name
color_value = data[attribute_name]
@@ -816,7 +814,10 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
"style.edit_surface_style",
tool.Ifc.get(),
style=self.surface_style,
attributes=self.get_shading_attributes(),
attributes={
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
"Transparency": self.props.transparency or None,
},
)
tool.Loader.create_surface_style_shading(material, self.surface_style)
elif self.surface_style.is_a() == "IfcSurfaceStyleRendering":
@@ -910,11 +911,10 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
)
def get_shading_attributes(self) -> dict[str, Any]:
attributes = {}
attributes["SurfaceColour"] = self.color_to_dict(self.props.surface_colour)
if tool.Ifc.get_schema() != "IFC2X3":
attributes["Transparency"] = self.props.transparency or None
return attributes
return {
"SurfaceColour": self.color_to_dict(self.props.surface_colour),
"Transparency": self.props.transparency or None,
}
def get_rendering_attributes(self) -> dict[str, Any]:
if self.props.is_diffuse_colour_null:
+2 -4
View File
@@ -55,7 +55,6 @@ class BIM_PT_styles(Panel):
row.operator("bim.load_styles", text="", icon="IMPORT").style_type = style_type
return
self.is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3"
active_style = self.props.active_style
row = self.layout.row(align=True)
row.label(text="{} {}s".format(len(self.props.styles), self.props.style_type), icon="SHADING_RENDERED")
@@ -159,9 +158,8 @@ class BIM_PT_styles(Panel):
def draw_surface_style_shading(self):
row = self.layout.row()
row.prop(self.props, "surface_colour")
if not self.is_ifc2x3:
row = self.layout.row()
row.prop(self.props, "transparency")
row = self.layout.row()
row.prop(self.props, "transparency")
def draw_surface_style_rendering(self):
row = self.layout.row()
@@ -70,7 +70,6 @@ class ExecuteIfcTester(bpy.types.Operator):
def execute_tester(self, ifc_data: ifcopenshell.file, ifc_path: str, specs_path: str) -> Union[set[str], None]:
props = bpy.context.scene.IfcTesterProperties
props.failed_entities.clear()
# No need for if-statement, just postponing lots of diffs.
if True:
+1 -5
View File
@@ -96,11 +96,7 @@ class BIM_PT_tester(Panel):
op2.spec_index = props.active_specification_index
op2.req_index = i
if (
props.old_index == props.active_specification_index
and props.n_entities > 0
and len(props.failed_entities) > 0
):
if props.old_index == props.active_specification_index and props.n_entities > 0:
row = self.layout.row()
row.label(text=f"Failed entities [{props.n_entities}]:")
self.layout.template_list(
+1 -2
View File
@@ -67,8 +67,7 @@ class TypeData:
if not relating_type_classes:
return []
results = []
assert (obj := bpy.context.active_object)
relating_type_class = tool.Type.get_object_type_props(obj).relating_type_class
relating_type_class = bpy.context.active_object.BIMTypeProperties.relating_type_class
if not relating_type_class and relating_type_classes:
relating_type_class = relating_type_classes[0][0]
elements = tool.Ifc.get().by_type(relating_type_class)
+7 -15
View File
@@ -46,13 +46,9 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator):
related_object: str
def _execute(self, context):
if self.relating_type:
relating_type = self.relating_type
else:
assert (obj := context.active_object)
props = tool.Type.get_object_type_props(obj)
relating_type = int(props.relating_type)
relating_type = tool.Ifc.get().by_id(relating_type)
relating_type = tool.Ifc.get().by_id(
self.relating_type or int(context.active_object.BIMTypeProperties.relating_type)
)
if self.related_object:
related_objects = [bpy.data.objects[self.related_object]]
else:
@@ -134,10 +130,8 @@ class EnableEditingType(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
assert (obj := context.active_object)
props = tool.Type.get_object_type_props(obj)
props.is_editing_type = True
props.relating_type_object = None
context.active_object.BIMTypeProperties.is_editing_type = True
context.active_object.BIMTypeProperties.relating_type_object = None
return {"FINISHED"}
@@ -148,10 +142,8 @@ class DisableEditingType(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects[self.obj] if self.obj else context.active_object
assert obj
props = tool.Type.get_object_type_props(obj)
props.is_editing_type = False
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj.BIMTypeProperties.is_editing_type = False
return {"FINISHED"}
+6 -13
View File
@@ -21,7 +21,6 @@ import ifcopenshell.util.element
import ifcopenshell.util.type
from bonsai.bim.module.type.data import TypeData
import bonsai.tool as tool
from typing import TYPE_CHECKING, Union
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -35,23 +34,23 @@ from bpy.props import (
)
def get_relating_type_class(self: "BIMTypeProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
def get_relating_type_class(self, context):
if not TypeData.is_loaded:
TypeData.load()
return TypeData.data["relating_type_classes"]
def get_relating_type(self: "BIMTypeProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
def get_relating_type(self, context):
if not TypeData.is_loaded:
TypeData.load()
return TypeData.data["relating_types"]
def update_relating_type_class(self: "BIMTypeProperties", context: bpy.types.Context) -> None:
def update_relating_type_class(self, context):
TypeData.is_loaded = False
def update_relating_type_from_object(self: "BIMTypeProperties", context: bpy.types.Context) -> None:
def update_relating_type_from_object(self, context):
if self.relating_type_object is None:
return
element = tool.Ifc.get_entity(self.relating_type_object)
@@ -64,12 +63,12 @@ def update_relating_type_from_object(self: "BIMTypeProperties", context: bpy.typ
bpy.ops.bim.assign_type()
def is_object_class_applicable(self: "BIMTypeProperties", obj: bpy.types.Object) -> bool:
def is_object_class_applicable(self, obj):
if not TypeData.is_loaded:
TypeData.load()
element = tool.Ifc.get_entity(obj)
if not element:
return False
return
element_type = ifcopenshell.util.element.get_type(element)
if element_type is None:
return False
@@ -90,9 +89,3 @@ class BIMTypeProperties(PropertyGroup):
update=update_relating_type_from_object,
poll=is_object_class_applicable,
)
if TYPE_CHECKING:
is_editing_type: bool
relating_type_class: str
relating_type: str
relating_type_object: Union[bpy.types.Object, None]
+4 -7
View File
@@ -16,7 +16,6 @@
# 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 bonsai.tool as tool
import bonsai.bim.module.type.prop as type_prop
from bpy.types import Panel
@@ -56,9 +55,8 @@ class BIM_PT_type(Panel):
else:
self.draw_type_ui(context)
def draw_type_ui(self, context: bpy.types.Context) -> None:
assert (obj := context.active_object)
oprops = tool.Blender.get_object_bim_props(obj)
def draw_type_ui(self, context):
oprops = tool.Blender.get_object_bim_props(context.active_object)
row = self.layout.row(align=True)
row.label(text=f"{TypeData.data['total_instances']} Typed Objects")
select_type_objects_row = row.row(align=True)
@@ -68,10 +66,9 @@ class BIM_PT_type(Panel):
op.element = oprops.ifc_definition_id
row.operator("bim.auto_rename_occurrences", icon="ITALIC", text="")
def draw_product_ui(self, context: bpy.types.Context) -> None:
def draw_product_ui(self, context):
layout = self.layout
assert (obj := context.active_object)
props = tool.Type.get_object_type_props(obj)
props = context.active_object.BIMTypeProperties
if props.is_editing_type:
row = layout.row(align=True)
+6 -7
View File
@@ -118,24 +118,23 @@ 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) -> list[ifcopenshell.entity_instance]:
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
def booleans(cls):
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or bpy.context.active_object
if not (representation := tool.Geometry.get_active_representation(obj)):
return []
return tool.Model.get_booleans(representation=representation)
@classmethod
def manual_booleans(cls) -> list[ifcopenshell.entity_instance]:
obj = tool.Geometry.get_active_or_representation_obj()
assert obj
def manual_booleans(cls):
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or bpy.context.active_object
if not (representation := tool.Geometry.get_active_representation(obj)):
return []
return tool.Model.get_manual_booleans(tool.Ifc.get_entity(obj), representation=representation)
@@ -32,16 +32,14 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Apply Opening"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"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."
"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"
)
@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):
+3 -2
View File
@@ -52,8 +52,9 @@ class BIM_PT_voids(Panel):
if not VoidsData.is_loaded:
VoidsData.load()
row = self.layout.row(align=True)
op = row.operator("bim.add_opening", icon="ADD", text="Add Opening")
if len(context.selected_objects) >= 2:
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()

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