Compare commits

..

4 Commits

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

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