mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-08 08:51:35 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05a3004ef0 | |||
| fe5afaf172 |
@@ -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.
|
||||
-->
|
||||
@@ -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
|
||||
@@ -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?
|
||||
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9].[0-9].[0-9]*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -35,4 +35,3 @@ extend-exclude = '''
|
||||
[tool.pyright]
|
||||
reportInvalidTypeForm = false
|
||||
disableBytesTypePromotions = true
|
||||
reportUnnecessaryTypeIgnoreComment = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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"}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user