Merge branch 'v0.8.0' into ifcmax/initial-refresh

This commit is contained in:
Josef Wienerroither
2025-12-21 16:31:19 +01:00
810 changed files with 4995 additions and 2270 deletions
@@ -0,0 +1,41 @@
from __future__ import annotations
import json
import os
import sys
from typing import TypedDict
import github_action_utils as gha_utils
class Entry(TypedDict):
location: Location
class Location(TypedDict):
path: str
lines: Lines
class Lines(TypedDict):
begin: int
end: int
json_data: list[Entry] = json.load(sys.stdin)
if os.getenv("RUNNER_DEBUG"):
print("Debug: Black formatting JSON data:")
print(json.dumps(json_data, indent=2))
for change in json_data:
location = change["location"]
path = location["path"]
lines = location["lines"]
gha_utils.error(
f"Black formatting issue in {path}",
title="Black Format Issue",
file=path,
line=lines["begin"],
end_line=lines["end"],
)
+3 -3
View File
@@ -21,12 +21,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -81,7 +81,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: build-logs-osx-${{ matrix.arch }}
path: |
+3 -3
View File
@@ -9,13 +9,13 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
path: IfcOpenShell
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: IfcOpenShell/build-outputs
path: ifcopenshell_build
@@ -42,7 +42,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: build-logs-pyodide
path: |
+3 -3
View File
@@ -29,12 +29,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -61,7 +61,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: build-logs-rocky
path: |
+3 -3
View File
@@ -29,12 +29,12 @@ jobs:
aws --version
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: IfcOpenShell/build-outputs
path: ./build
@@ -61,7 +61,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: build-logs-rocky-arm64
path: |
+2 -2
View File
@@ -12,12 +12,12 @@ jobs:
arch: ['x64']
steps:
- name: Checkout Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
- name: Checkout Build Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: IfcOpenShell/build-outputs
path: _deps-vs2022-x64-installed
+2 -2
View File
@@ -1,9 +1,9 @@
import os
import pathlib
import shutil
import zipfile
import requests
import zipfile
import os
# To test this locally, set these environment variables
REPO_OWNER = os.environ.get("REPO_OWNER", "IfcOpenShell/IfcOpenShell")
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5 # https://github.com/actions/checkout
- uses: actions/checkout@v6 # https://github.com/actions/checkout
- uses: actions/setup-python@v6 # 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
+43 -8
View File
@@ -9,12 +9,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Action - checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Action - install python
uses: actions/setup-python@v6
with:
python-version: "3.9"
python-version: "3.10"
- name: Action - install python
uses: actions/setup-python@v6
@@ -33,23 +33,58 @@ jobs:
id: syntax-errors
run: |
ERROR=0
python3.9 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3.10 -W error -m compileall -q src/ifcopenshell-python || ERROR=1
python3.11 -W error -m compileall -q src/bonsai || ERROR=1
exit $ERROR
continue-on-error: true
- name: Black formatter
id: black
uses: psf/black@stable
continue-on-error: true
# Same check as above, but just for creating github annotations.
- name: Black formatter annotations
id: black-annotations
run: |
black --diff --check .
uv tool install black-codeclimate
pip install github_action_utils
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
continue-on-error: true
- name: Ruff check
id: ruff
run: |
# Ensure execution continues, since we need to cache the output.
set +e
ERROR=0
poe ruff-main || ERROR=1
poe ruff-old || ERROR=1
# Keep colored output inside action logs, strip it from color codes for summary.
uv tool install ansi2txt
# `ruff` disables color output in CI by default.
export FORCE_COLOR="1"
run_check() {
local out
out="$("$@" 2>&1)"
local exit_code=$?
if [ "$exit_code" -ne 0 ]; then
ERROR=1
fi
# Rerun just for GitHub annotations.
"$@" --output-format=github || true
echo "$out"
echo "\`\`\`python" >> $GITHUB_STEP_SUMMARY
echo "$out" | ansi2txt >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
}
run_check poe ruff-main
run_check poe ruff-old
exit $ERROR
continue-on-error: true
@@ -60,9 +95,9 @@ jobs:
echo "::error::Syntax errors check failed, see 'syntax-errors' step for the details." && ERROR=1
fi
if [ "${{ steps.black.outcome }}" != "success" ]; then
echo "::error::Black formatting check failed, see 'black' step for the details." && ERROR=1
echo "::error::Black formatting check failed, see Summary or 'black' step for the details." && ERROR=1
fi
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
echo "::error::Ruff check failed, see 'ruff' step for the details." && ERROR=1
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
fi
exit $ERROR
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-tags: true
fetch-depth: 0
+2 -2
View File
@@ -54,7 +54,7 @@ jobs:
short_name: macosm1,
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
@@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout bonsai_unstable_repo repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: IfcOpenShell/bonsai_unstable_repo
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
short_name: macosm1,
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
@@ -21,7 +21,7 @@ jobs:
date: ${{ steps.date.outputs.date }}
verdate: ${{ steps.verdate.outputs.verdate }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set env
run: echo ok go
@@ -54,8 +54,7 @@ jobs:
platform: [
{ name: win, distver: windows-latest, pkg_dir: 'win-64' },
{ name: linux, distver: ubuntu-latest, pkg_dir: 'linux-64' },
{ name: macOS-arm, distver: macos-latest, pkg_dir: 'osx-arm64' },
{ name: macOS-x86, distver: macos-13, pkg_dir: 'osx-64' }
{ name: macOS-arm, distver: macos-latest, pkg_dir: 'osx-arm64' }
]
steps:
- name: Set Swap Space
@@ -76,7 +75,7 @@ jobs:
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
fi
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
+4 -4
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-22.04
needs: activate
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
@@ -73,7 +73,7 @@ jobs:
make package
working-directory: build
- name: Upload
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
# Artifact name
name: ifcos-artifacts
@@ -86,12 +86,12 @@ jobs:
name: Docker Build, Tag, Push
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
lfs: true
- name: Download
uses: actions/download-artifact@v6.0.0
uses: actions/download-artifact@v7.0.0
with:
# Artifact name
name: ifcos-artifacts
@@ -47,7 +47,7 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
short_name: macosm164
}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
with:
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
publish_website:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Checkout ifctester_org_static_html
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: IfcOpenShell/ifctester_org_static_html
token: ${{ secrets.IFCOPENBOT_TOKEN }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # 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
+5 -1
View File
@@ -34,8 +34,12 @@ jobs:
compile-and-test:
runs-on: ubuntu-22.04
needs: activate
env:
# Colored output for cmake.
CLICOLOR_FORCE: "1"
CMAKE_COLOR_DIAGNOSTICS: "ON"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install C++ dependencies
+1 -1
View File
@@ -1,6 +1,6 @@
import boto3
import ifcopenshell
import ifcopenshell.util.element
import boto3
s3 = boto3.client('s3')
+3 -2
View File
@@ -13,9 +13,10 @@ import hashlib
import os
import pathlib
import re
from urllib import request
from github import Github
from typing import NoReturn
from urllib import request
from github import Github
def get_repo_tag_names() -> list[str]:
@@ -1,6 +1,5 @@
import bpy
bpy.ops.preferences.addon_disable(module='blenderbim')
bpy.ops.wm.save_userpref()
@@ -1,6 +1,5 @@
import bpy
bpy.ops.preferences.addon_enable(module='blenderbim')
bpy.ops.wm.save_userpref()
+2 -1
View File
@@ -104,6 +104,7 @@ option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only ins
option(USE_DEBUG_PYTHON "Use debug binaries when building Debug IfcPython on Windows." OFF)
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
option(VERSION_OVERRIDE "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" OFF)
option(USE_CCACHE "Enable use of ccache if it's available from PATH." ON)
set(
PYTHON_MODULE_INSTALL_DIR
@@ -132,7 +133,7 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
endif()
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
if(USE_CCACHE AND CCACHE_FOUND)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
+35
View File
@@ -4,7 +4,42 @@
set(IFCOPENSHELL_SCHEMA_VERSIONS @SCHEMA_VERSIONS@)
set(IFCOPENSHELL_WITH_OPENCASCADE @WITH_OPENCASCADE@)
set(IFCOPENSHELL_WITH_CGAL @WITH_CGAL@)
set(IFCOPENSHELL_IFCXML @IFCXML_SUPPORT@)
set(IFCOPENSHELL_WITH_ROCKSDB @WITH_ROCKSDB@)
include(CMakeFindDependencyMacro)
set(Boost_USE_STATIC_LIBS ON)
set(Boost_USE_STATIC_RUNTIME OFF)
set(Boost_USE_MULTITHREADED ON)
set(Boost_COMPONENTS system program_options regex thread date_time iostreams)
find_dependency(Boost CONFIG COMPONENTS ${Boost_COMPONENTS})
find_dependency(Eigen3 CONFIG)
if(IFCOPENSHELL_WITH_ROCKSDB)
find_dependency(zstd CONFIG)
find_dependency(RocksDB CONFIG)
endif()
if(IFCOPENSHELL_IFCXML)
find_dependency(LibXml2 CONFIG)
endif()
if(IFCOPENSHELL_WITH_CGAL)
find_dependency(CGAL CONFIG)
endif()
if(IFCOPENSHELL_WITH_OPENCASCADE)
find_dependency(OpenCASCADE CONFIG)
if(OpenCASCADE_VERSION VERSION_LESS "7.7.0")
# cmake configs < 7.7.0 were not adding include directories to targets automatically.
set_target_properties(TKernel PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${OpenCASCADE_INCLUDE_DIR}"
)
endif()
endif()
include("${CMAKE_CURRENT_LIST_DIR}/@CONFIG_TARGETS_FILENAME@")
+2 -1
View File
@@ -1,7 +1,8 @@
import re
import argparse
import re
from pathlib import Path
def update_version(file_path: str, version: str) -> None:
"""Update the version string in the given __init__.py file."""
file_path = Path(file_path)
+1
View File
@@ -1,4 +1,5 @@
import textwrap
# The `extensions` list should already be in here from `sphinx-quickstart`
extensions = [
# there may be others here already, e.g. 'sphinx.ext.mathjax'
+2 -2
View File
@@ -1,10 +1,10 @@
# This program requires doxygen, sphinx, breathe and exhale.
import multiprocessing
import os
import sys
import shutil
import subprocess
import multiprocessing
import sys
# some extra check to see if we can find sphinx in pypy bin dir
sphinx_build = os.path.join(os.path.dirname(sys.executable), 'sphinx-build')
+14 -13
View File
@@ -99,35 +99,36 @@ Used environment variables:
"""
import logging
import os
import re
import sys
import glob
import subprocess as sp
import shutil
import tarfile
import logging
import multiprocessing
import os
import platform
import threading
import sysconfig
from datetime import datetime
import re
import shutil
# @todo temporary for expired mpfr.org certificate on 2023-04-08
import ssl
import subprocess as sp
import sys
import sysconfig
import tarfile
import threading
from datetime import datetime
ssl._create_default_https_context = ssl._create_unverified_context
import time
from urllib.request import urlretrieve
from collections.abc import Generator, Sequence
from pathlib import Path
from urllib.request import urlretrieve
try:
from typing import Union, Literal
from typing import Literal, Union
except:
# python 3.6 compatibility for rocky 8
from typing import Union
from typing_extensions import Literal
logger = logging.getLogger(__name__)
@@ -139,7 +140,7 @@ PROJECT_NAME = "IfcOpenShell"
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
PYTHON_VERSIONS = ["3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
JSON_VERSION = "3.11.3"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.8.1"
+1 -2
View File
@@ -8,12 +8,11 @@ or unpacks existing archives.
Usage: python cache_dependencies.py [pack|unpack]
"""
import tarfile
import sys
import tarfile
from pathlib import Path
from typing import Literal
CACHE_PREFIX = "cache-"
-1
View File
@@ -1,6 +1,5 @@
from pathlib import Path
WHEEL_FILENAME = next(
p.name for p in (Path.cwd() / "pyodide").iterdir() if p.name.startswith("ifcopenshell-") and p.suffix == ".whl"
)
+1
View File
@@ -47,6 +47,7 @@ select = [
"UP", # pyupgrade
"RUF015", # next() > list_comprehension[0]
"RUF022", # sort __all__
"I", # import sorting
]
ignore = [
"FA100", # Conflicts with Blender using annotations for props definitions.
+3 -2
View File
@@ -1,7 +1,8 @@
from dataclasses import fields
from typing import NamedTuple, Union
import bcf.v2.model.extensions
import bcf.v3.model.extensions
from typing import NamedTuple, Union
from dataclasses import fields
class AttributeData(NamedTuple):
+2 -1
View File
@@ -1,6 +1,7 @@
from typing import Union
import bcf.v2.model
import bcf.v3.model
from typing import Union
BimSnippet = Union[bcf.v2.model.BimSnippet, bcf.v3.model.BimSnippet]
BitMap = Union[bcf.v2.model.VisualizationInfoBitmap, bcf.v3.model.Bitmap]
+4 -3
View File
@@ -1,13 +1,14 @@
import tempfile
from pathlib import Path
from typing import Optional, Union
import bcf.agnostic.model as mdl
import bcf.v2.bcfxml
import bcf.v2.model
import bcf.v2.topic
import bcf.v3.bcfxml
import bcf.v3.model
import bcf.v3.topic
import bcf.agnostic.model as mdl
from pathlib import Path
from typing import Union, Optional
from typing_extensions import assert_never
TopicHandler = Union[bcf.v2.topic.TopicHandler, bcf.v3.topic.TopicHandler]
+3 -2
View File
@@ -1,5 +1,6 @@
import bcf.v2.visinfo
import bcf.v3.visinfo
from typing import Union
import bcf.v2.visinfo
import bcf.v3.visinfo
VisualizationInfoHandler = Union[bcf.v2.visinfo.VisualizationInfoHandler, bcf.v3.visinfo.VisualizationInfoHandler]
-1
View File
@@ -29,7 +29,6 @@ from bcf.v3.bcfxml import BcfXml as BcfXml3
from bcf.v3.model import Version as Version3
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
BcfXml = Union[BcfXml2, BcfXml3]
+1
View File
@@ -7,6 +7,7 @@ original idea from https://stackoverflow.com/a/19722365/1307905
"""
from __future__ import annotations
import zipfile
from io import BytesIO
from os import PathLike
+1
View File
@@ -1,6 +1,7 @@
"""BCF XML V2 handler."""
from __future__ import annotations
import uuid
import warnings
import zipfile
+8 -12
View File
@@ -14,15 +14,11 @@
# Currently extensions support for v2 is only read-only.
import sys
from dataclasses import dataclass, field, fields
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsPriorities:
class Meta:
global_type = False
@@ -39,7 +35,7 @@ class ExtensionsPriorities:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsSnippetTypes:
class Meta:
global_type = False
@@ -56,7 +52,7 @@ class ExtensionsSnippetTypes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsStages:
class Meta:
global_type = False
@@ -73,7 +69,7 @@ class ExtensionsStages:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicLabels:
class Meta:
global_type = False
@@ -90,7 +86,7 @@ class ExtensionsTopicLabels:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicStatuses:
class Meta:
global_type = False
@@ -107,7 +103,7 @@ class ExtensionsTopicStatuses:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicTypes:
class Meta:
global_type = False
@@ -124,7 +120,7 @@ class ExtensionsTopicTypes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsUsers:
class Meta:
global_type = False
@@ -141,7 +137,7 @@ class ExtensionsUsers:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Extensions:
topic_types: Optional[ExtensionsTopicTypes] = field(
default=None,
+10 -13
View File
@@ -1,13 +1,10 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
from xsdata.models.datatype import XmlDateTime
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class BimSnippet:
reference: str = field(
metadata={
@@ -41,7 +38,7 @@ class BimSnippet:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class CommentViewpoint:
class Meta:
global_type = False
@@ -56,7 +53,7 @@ class CommentViewpoint:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class HeaderFile:
class Meta:
global_type = False
@@ -112,7 +109,7 @@ class HeaderFile:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicDocumentReference:
class Meta:
global_type = False
@@ -150,7 +147,7 @@ class TopicDocumentReference:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopic:
class Meta:
global_type = False
@@ -165,7 +162,7 @@ class TopicRelatedTopic:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ViewPoint:
viewpoint: Optional[str] = field(
default=None,
@@ -201,7 +198,7 @@ class ViewPoint:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Comment:
date: XmlDateTime = field(
metadata={
@@ -261,7 +258,7 @@ class Comment:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Header:
file: list[HeaderFile] = field(
default_factory=list,
@@ -274,7 +271,7 @@ class Header:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Topic:
reference_link: list[str] = field(
default_factory=list,
@@ -428,7 +425,7 @@ class Topic:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Markup:
header: Optional[Header] = field(
default=None,
+2 -5
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Project:
name: Optional[str] = field(
default=None,
@@ -24,7 +21,7 @@ class Project:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ProjectExtension:
project: Optional[Project] = field(
default=None,
+1 -4
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Version:
detailed_version: Optional[str] = field(
default=None,
+18 -21
View File
@@ -1,17 +1,14 @@
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
class BitmapFormat(Enum):
PNG = "PNG"
JPG = "JPG"
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Component:
originating_system: Optional[str] = field(
default=None,
@@ -38,7 +35,7 @@ class Component:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Direction:
x: float = field(
metadata={
@@ -63,7 +60,7 @@ class Direction:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Point:
x: float = field(
metadata={
@@ -88,7 +85,7 @@ class Point:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ViewSetupHints:
spaces_visible: Optional[bool] = field(
default=None,
@@ -113,7 +110,7 @@ class ViewSetupHints:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ClippingPlane:
location: Point = field(
metadata={
@@ -131,7 +128,7 @@ class ClippingPlane:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColor:
class Meta:
global_type = False
@@ -154,7 +151,7 @@ class ComponentColoringColor:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentSelection:
component: list[Component] = field(
default_factory=list,
@@ -166,7 +163,7 @@ class ComponentSelection:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentVisibilityExceptions:
class Meta:
global_type = False
@@ -181,7 +178,7 @@ class ComponentVisibilityExceptions:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Line:
start_point: Point = field(
metadata={
@@ -199,7 +196,7 @@ class Line:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class OrthogonalCamera:
"""
Attributes
@@ -239,7 +236,7 @@ class OrthogonalCamera:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class PerspectiveCamera:
"""
Attributes
@@ -284,7 +281,7 @@ class PerspectiveCamera:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoBitmap:
class Meta:
global_type = False
@@ -333,7 +330,7 @@ class VisualizationInfoBitmap:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoring:
color: list[ComponentColoringColor] = field(
default_factory=list,
@@ -345,7 +342,7 @@ class ComponentColoring:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentVisibility:
exceptions: Optional[ComponentVisibilityExceptions] = field(
default=None,
@@ -363,7 +360,7 @@ class ComponentVisibility:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoClippingPlanes:
class Meta:
global_type = False
@@ -377,7 +374,7 @@ class VisualizationInfoClippingPlanes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoLines:
class Meta:
global_type = False
@@ -392,7 +389,7 @@ class VisualizationInfoLines:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Components:
view_setup_hints: Optional[ViewSetupHints] = field(
default=None,
@@ -424,7 +421,7 @@ class Components:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfo:
"""
VisualizationInfo documentation.
+1
View File
@@ -1,6 +1,7 @@
"""BCF XML V2 Topic handler."""
from __future__ import annotations
import datetime
import tempfile
import uuid
+3 -3
View File
@@ -1,12 +1,12 @@
import uuid
import zipfile
from typing import Any, Optional, Literal, Union
from collections.abc import Iterable
from typing import Any, Literal, Optional, Union
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import numpy as np
from ifcopenshell import entity_instance
import ifcopenshell.util.unit
import ifcopenshell.util.placement
from numpy.typing import NDArray
import bcf.v2.model as mdl
+1
View File
@@ -1,6 +1,7 @@
"""BCF XML V3 handlers."""
from __future__ import annotations
import uuid
import warnings
import zipfile
+3 -6
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Document:
filename: str = field(
metadata={
@@ -37,7 +34,7 @@ class Document:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class DocumentInfoDocuments:
class Meta:
global_type = False
@@ -52,7 +49,7 @@ class DocumentInfoDocuments:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class DocumentInfo:
documents: Optional[DocumentInfoDocuments] = field(
default=None,
+8 -11
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsPriorities:
class Meta:
global_type = False
@@ -22,7 +19,7 @@ class ExtensionsPriorities:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsSnippetTypes:
class Meta:
global_type = False
@@ -39,7 +36,7 @@ class ExtensionsSnippetTypes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsStages:
class Meta:
global_type = False
@@ -56,7 +53,7 @@ class ExtensionsStages:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicLabels:
class Meta:
global_type = False
@@ -73,7 +70,7 @@ class ExtensionsTopicLabels:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicStatuses:
class Meta:
global_type = False
@@ -90,7 +87,7 @@ class ExtensionsTopicStatuses:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicTypes:
class Meta:
global_type = False
@@ -107,7 +104,7 @@ class ExtensionsTopicTypes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ExtensionsUsers:
class Meta:
global_type = False
@@ -124,7 +121,7 @@ class ExtensionsUsers:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Extensions:
topic_types: Optional[ExtensionsTopicTypes] = field(
default=None,
+17 -20
View File
@@ -1,13 +1,10 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
from xsdata.models.datatype import XmlDateTime
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class BimSnippet:
reference: str = field(
metadata={
@@ -47,7 +44,7 @@ class BimSnippet:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class CommentViewpoint:
class Meta:
global_type = False
@@ -62,7 +59,7 @@ class CommentViewpoint:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class DocumentReference:
document_guid: Optional[str] = field(
default=None,
@@ -103,7 +100,7 @@ class DocumentReference:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class File:
filename: Optional[str] = field(
default=None,
@@ -160,7 +157,7 @@ class File:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicLabels:
class Meta:
global_type = False
@@ -177,7 +174,7 @@ class TopicLabels:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicReferenceLinks:
class Meta:
global_type = False
@@ -194,7 +191,7 @@ class TopicReferenceLinks:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopicsRelatedTopic:
class Meta:
global_type = False
@@ -209,7 +206,7 @@ class TopicRelatedTopicsRelatedTopic:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ViewPoint:
viewpoint: Optional[str] = field(
default=None,
@@ -249,7 +246,7 @@ class ViewPoint:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Comment:
date: XmlDateTime = field(
metadata={
@@ -315,7 +312,7 @@ class Comment:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class HeaderFiles:
class Meta:
global_type = False
@@ -330,7 +327,7 @@ class HeaderFiles:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicDocumentReferences:
class Meta:
global_type = False
@@ -345,7 +342,7 @@ class TopicDocumentReferences:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicRelatedTopics:
class Meta:
global_type = False
@@ -360,7 +357,7 @@ class TopicRelatedTopics:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicViewpoints:
class Meta:
global_type = False
@@ -375,7 +372,7 @@ class TopicViewpoints:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Header:
files: Optional[HeaderFiles] = field(
default=None,
@@ -387,7 +384,7 @@ class Header:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class TopicComments:
class Meta:
global_type = False
@@ -402,7 +399,7 @@ class TopicComments:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Topic:
reference_links: Optional[TopicReferenceLinks] = field(
default=None,
@@ -599,7 +596,7 @@ class Topic:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Markup:
header: Optional[Header] = field(
default=None,
+2 -5
View File
@@ -1,11 +1,8 @@
import sys
from dataclasses import dataclass, field
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Project:
name: Optional[str] = field(
default=None,
@@ -28,7 +25,7 @@ class Project:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ProjectInfo:
project: Project = field(
metadata={
+1 -4
View File
@@ -1,10 +1,7 @@
import sys
from dataclasses import dataclass, field
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Version:
version_id: str = field(
metadata={
+20 -24
View File
@@ -1,18 +1,14 @@
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
DATACLASS_KWARGS = {} if sys.version_info < (3, 10) else {"slots": True, "kw_only": True}
class BitmapFormat(Enum):
PNG = "png"
JPG = "jpg"
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Component:
originating_system: Optional[str] = field(
default=None,
@@ -43,7 +39,7 @@ class Component:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Direction:
x: float = field(
metadata={
@@ -68,7 +64,7 @@ class Direction:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Point:
x: float = field(
metadata={
@@ -93,7 +89,7 @@ class Point:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ViewSetupHints:
spaces_visible: bool = field(
default=False,
@@ -118,7 +114,7 @@ class ViewSetupHints:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Bitmap:
format: BitmapFormat = field(
metadata={
@@ -166,7 +162,7 @@ class Bitmap:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ClippingPlane:
location: Point = field(
metadata={
@@ -184,7 +180,7 @@ class ClippingPlane:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColorComponents:
class Meta:
global_type = False
@@ -199,7 +195,7 @@ class ComponentColoringColorComponents:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentSelection:
component: list[Component] = field(
default_factory=list,
@@ -210,7 +206,7 @@ class ComponentSelection:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentVisibilityExceptions:
class Meta:
global_type = False
@@ -224,7 +220,7 @@ class ComponentVisibilityExceptions:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Line:
start_point: Point = field(
metadata={
@@ -242,7 +238,7 @@ class Line:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class OrthogonalCamera:
"""
Attributes
@@ -292,7 +288,7 @@ class OrthogonalCamera:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class PerspectiveCamera:
"""
Attributes
@@ -348,7 +344,7 @@ class PerspectiveCamera:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoringColor:
class Meta:
global_type = False
@@ -370,7 +366,7 @@ class ComponentColoringColor:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentVisibility:
view_setup_hints: Optional[ViewSetupHints] = field(
default=None,
@@ -395,7 +391,7 @@ class ComponentVisibility:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoBitmaps:
class Meta:
global_type = False
@@ -409,7 +405,7 @@ class VisualizationInfoBitmaps:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoClippingPlanes:
class Meta:
global_type = False
@@ -423,7 +419,7 @@ class VisualizationInfoClippingPlanes:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfoLines:
class Meta:
global_type = False
@@ -437,7 +433,7 @@ class VisualizationInfoLines:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class ComponentColoring:
color: list[ComponentColoringColor] = field(
default_factory=list,
@@ -448,7 +444,7 @@ class ComponentColoring:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class Components:
selection: Optional[ComponentSelection] = field(
default=None,
@@ -473,7 +469,7 @@ class Components:
)
@dataclass(**DATACLASS_KWARGS)
@dataclass(slots=True, kw_only=True)
class VisualizationInfo:
"""
VisualizationInfo documentation.
+1
View File
@@ -1,6 +1,7 @@
"""BCF XML V3 Topic handler."""
from __future__ import annotations
import datetime
import uuid
import zipfile
+3 -3
View File
@@ -1,12 +1,12 @@
import uuid
import zipfile
from typing import Any, Optional, Literal, Union
from collections.abc import Iterable
from typing import Any, Literal, Optional, Union
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import numpy as np
from ifcopenshell import entity_instance
import ifcopenshell.util.unit
import ifcopenshell.util.placement
from numpy.typing import NDArray
import bcf.v3.model as mdl
-1
View File
@@ -1,5 +1,4 @@
import pytest
from bcf.xml_parser import XmlParserSerializer
+1 -2
View File
@@ -4,9 +4,8 @@ import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
import bcf.v2.model as mdl
import pytest
from bcf.v2.bcfxml import BcfXml
from bcf.v2.topic import TopicHandler
from bcf.v2.visinfo import (
+1 -2
View File
@@ -3,11 +3,10 @@ import os
import zipfile
from pathlib import Path
from xsdata.models.datatype import XmlDateTime
import bcf.v2.model as mdl
from bcf.v2.bcfxml import BcfXml
from bcf.v2.topic import TopicHandler
from xsdata.models.datatype import XmlDateTime
def test_maximum_information() -> None:
-1
View File
@@ -1,6 +1,5 @@
import numpy as np
import pytest
from bcf.v2.bcfxml import BcfXml
+1 -2
View File
@@ -4,9 +4,8 @@ import uuid
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
import bcf.v3.model as mdl
import pytest
from bcf.v3.bcfxml import BcfXml
from bcf.v3.topic import TopicHandler
from bcf.v3.visinfo import (
+1 -2
View File
@@ -3,11 +3,10 @@ import os
import zipfile
from pathlib import Path
from xsdata.models.datatype import XmlDateTime
import bcf.v3.model as mdl
from bcf.v3.bcfxml import BcfXml
from bcf.v3.topic import TopicHandler
from xsdata.models.datatype import XmlDateTime
def test_doc_ref_internal() -> None:
-1
View File
@@ -1,6 +1,5 @@
import numpy as np
import pytest
from bcf.v3.bcfxml import BcfXml
+20
View File
@@ -115,11 +115,13 @@ classes = [
operator.EditBlenderCollection,
operator.FileAssociate,
operator.FileUnassociate,
operator.LoadBlendMetadataAndIFC,
operator.OpenPath,
operator.OpenUpstream,
operator.OpenUri,
operator.ReloadIfcFile,
operator.RevertClippingPlaneCut,
operator.SaveBlendMetadataFile,
operator.SelectDir,
operator.SelectIfcFile,
operator.SelectURIAttribute,
@@ -129,12 +131,21 @@ classes = [
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
operator.BIM_OT_attribute_search_values,
operator.BIM_UL_tab_panels,
operator.BIM_OT_toggle_panel_visibility,
operator.BIM_OT_bookmark_panel,
operator.BIM_OT_manage_tab_panels,
operator.BIM_OT_manage_tab_visibility,
operator.BIM_OT_toggle_tab_visibility,
operator.BIM_OT_reset_ui_layout,
prop.ObjProperty,
prop.MultipleFileSelect,
prop.Attribute,
prop.ISODuration,
prop.BIMAreaProperties,
prop.BIMTabProperties,
prop.BIMTabVisibility, # Must be registered before BIMProperties
prop.BIMPanelProperties, # Must be registered before BIMProperties
prop.BIMProperties,
prop.IfcParameter,
prop.PsetQto,
@@ -272,6 +283,7 @@ def register():
bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
if hasattr(bpy.types, "UI_MT_button_context_menu"):
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
bpy.types.STATUSBAR_HT_header.append(ui.draw_statusbar)
@@ -307,6 +319,10 @@ def register():
# RestrictedContext doesn't allow accessing scene attribute, postpone it for a bit.
bpy.app.timers.register(tool.Blender.setup_user_data_dir, first_interval=0.1)
bpy.types.Scene.active_tab_name = bpy.props.StringProperty()
bpy.types.Scene.tab_panels = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)
bpy.types.Scene.active_tab_panel_index = bpy.props.IntProperty()
def unregister():
global icons
@@ -348,3 +364,7 @@ def unregister():
tool.Blender.remove_scene_panel_override(panel)
bpy.app.translations.unregister("bonsai")
del bpy.types.Scene.active_tab_name
del bpy.types.Scene.tab_panels
del bpy.types.Scene.active_tab_panel_index
+367 -2
View File
@@ -498,6 +498,8 @@ def draw_filter(
if data.data["saved_searches"]:
row.operator("bim.load_search", text="", icon="IMPORT").module = module
row.operator("bim.save_search", text="", icon="EXPORT").module = module
if data.data["saved_searches"]:
row.operator("bim.remove_search", text="", icon="REMOVE").module = module
if module != "search":
if module == "drawing_include":
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "INCLUDE"
@@ -505,8 +507,17 @@ def draw_filter(
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE"
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
row = layout.row(align=True)
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
else:
if not filter_groups or not any(fg.filters for fg in filter_groups):
op = row.operator("bim.add_filter", text="Add Filter", icon="ADD")
op.type = "entity"
op.index = 0
op.module = module
op = row.operator("bim.edit_filter_query", text="", icon="FILTER")
if "module" in op.bl_rna.properties:
op.module = module
for i, filter_group in enumerate(filter_groups):
box = layout.box()
@@ -524,43 +535,251 @@ def draw_filter(
for j, ifc_filter in enumerate(filter_group.filters):
if ifc_filter.type == "entity":
row = box.row(align=True)
preferences = tool.Blender.get_addon_preferences()
if preferences.chain_filter_with_set_operations:
show_mode_toggle = j > 0
else:
show_mode_toggle = (
preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0
) # PR 7315 mode
if show_mode_toggle:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="FILE_3D")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "attribute":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "name", text="", icon="COPY_ID")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "attribute_name"
row.prop(ifc_filter, "value", text="")
if ifc_filter.name:
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "attribute_value"
elif ifc_filter.type == "type":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="FILE_VOLUME")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "material":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="MATERIAL")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "property":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "pset", text="", icon="PROPERTIES")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "pset"
row.prop(ifc_filter, "name", text="")
if ifc_filter.pset:
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "property_name"
row.prop(ifc_filter, "comparison", text="")
row.prop(ifc_filter, "value", text="")
if ifc_filter.pset and ifc_filter.name:
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "property_value"
elif ifc_filter.type == "classification":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="OUTLINER")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "location":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="PACKAGE")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "group":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="OUTLINER_COLLECTION")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "parent":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="FILE_PARENT")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "query":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA")
row.prop(ifc_filter, "comparison", text="")
row.prop(ifc_filter, "value", text="")
elif ifc_filter.type == "instance":
row = box.row(align=True)
preferences = tool.Blender.get_addon_preferences()
if preferences.chain_filter_with_set_operations:
show_mode_toggle = j > 0
else:
show_mode_toggle = (
preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0
) # PR 7315 mode
if show_mode_toggle:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="GRIP")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op = row.operator("bim.select_filter_elements", text="", icon="EYEDROPPER")
op.group_index = i
op.index = j
@@ -569,3 +788,149 @@ def draw_filter(
op.group_index = i
op.index = j
op.module = module
# ============================================================================
# UI Panel Visibility Helpers
# ============================================================================
def get_tab_names():
from bonsai.bim.prop import get_tab
enum_items = get_tab(None, None)
# Exclude None separators and the BLENDER tab (not part of BIM tab system)
return [item[0] for item in enum_items if item is not None and item[0] != "BLENDER"]
def get_panel_tab_name(panel_class):
if hasattr(panel_class, "bim_tab_name"):
return panel_class.bim_tab_name
return "PROJECT" # Default fallback
def should_show_panel(panel_id, panel_tab_name, context):
if tool.Blender.is_tab(context, "BOOKMARK"):
return is_panel_bookmarked(panel_id) and get_panel_visibility(panel_id, "BOOKMARK")
if tool.Blender.is_tab(context, panel_tab_name):
return get_tab_visibility(panel_tab_name) and get_panel_visibility(panel_id, panel_tab_name)
return False
def get_tab_visibility(tab_name):
bim_props = tool.Blender.get_bim_props()
tab_vis = bim_props.tab_visibilities.get(tab_name)
return tab_vis.is_visible if tab_vis else True
def set_tab_visibility(tab_name, visible):
bim_props = tool.Blender.get_bim_props()
tab_vis = bim_props.tab_visibilities.get(tab_name)
if tab_vis:
tab_vis.is_visible = visible
else:
new_tab = bim_props.tab_visibilities.add()
new_tab.name = tab_name
new_tab.is_visible = visible
def get_panel_visibility(panel_id, current_tab=None):
panel_config = get_panel_config(panel_id)
if panel_config:
if current_tab == "BOOKMARK":
return panel_config.is_visible_in_bookmarks
else:
return panel_config.is_visible_in_tab
return True
def is_panel_bookmarked(panel_id):
panel_config = get_panel_config(panel_id)
if panel_config:
return panel_config.is_bookmarked
return False
def get_panel_config(panel_id, create_if_missing=False):
try:
bim_props = tool.Blender.get_bim_props()
except (AttributeError, AssertionError):
return None
for prop in bim_props.panel_properties:
if prop.name == panel_id:
return prop
if create_if_missing:
try:
prop = bim_props.panel_properties.add()
prop.name = panel_id
prop.is_visible_in_tab = True
prop.is_visible_in_bookmarks = True
prop.is_bookmarked = False
return prop
except AttributeError:
pass
return None
def get_all_tab_panels(force_refresh=False):
panels = {tab_name: [] for tab_name in get_tab_names() if tab_name != "BOOKMARK"}
panels["BOOKMARK"] = []
bim_props = tool.Blender.get_bim_props()
for prop in bim_props.panel_properties:
panel_class = getattr(bpy.types, prop.name, None)
if panel_class:
tab_name = get_panel_tab_name(panel_class)
if tab_name and tab_name != "BOOKMARK":
bl_label = getattr(panel_class, "bl_label", prop.name)
panels[tab_name].append({"bl_idname": prop.name, "bl_label": bl_label})
if prop.is_bookmarked:
panel_class = getattr(bpy.types, prop.name, None)
if panel_class:
bl_label = getattr(panel_class, "bl_label", prop.name)
panels["BOOKMARK"].append({"bl_idname": prop.name, "bl_label": bl_label})
if not panels["BOOKMARK"]:
panels["BOOKMARK"] = [{}]
return panels
def initialize_tab_visibilities():
bim_props = tool.Blender.get_bim_props()
if len(bim_props.tab_visibilities) > 0:
return
for tab_name in get_tab_names():
tab_vis = bim_props.tab_visibilities.add()
tab_vis.name = tab_name
tab_vis.is_visible = True
def initialize_panel_properties():
bim_props = tool.Blender.get_bim_props()
if len(bim_props.panel_properties) > 0:
return
for attr_name in dir(bpy.types):
if attr_name.startswith("BIM_PT_tab_"):
panel_class = getattr(bpy.types, attr_name)
if not hasattr(panel_class, "bl_idname"):
continue
panel_id = panel_class.bl_idname
prop = bim_props.panel_properties.add()
prop.name = panel_id
prop.is_visible_in_tab = True
prop.is_visible_in_bookmarks = True
prop.is_bookmarked = False
@@ -143,6 +143,7 @@ def menu_func(self, context):
if element and element.is_a("IfcAnnotation") and element.ObjectType in ["SECTION", "ELEVATION"]:
self.layout.operator("bim.activate_drawing_by_annotation", text="Go to Drawing")
def register():
if not bpy.app.background:
bpy.utils.register_tool(workspace.AnnotationTool, after={"bim.bim_tool"}, separator=True, group=False)
@@ -155,7 +156,7 @@ def register():
bpy.app.handlers.load_post.append(handler.load_post)
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
def unregister():
@@ -1655,7 +1655,7 @@ class CutDecorator:
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
in_local_view = True
break
# If just entering local view (transition from False to True)
if in_local_view and not self.__class__.was_in_local_view:
self.__class__.local_view_has_annotation = False
@@ -1664,10 +1664,10 @@ class CutDecorator:
if element and element.is_a("IfcAnnotation"):
self.__class__.local_view_has_annotation = True
break
# Update the state for next time
self.__class__.was_in_local_view = in_local_view
# Skip decorations if in local view and no IfcAnnotation was selected when entering
if in_local_view and not self.__class__.local_view_has_annotation:
return
@@ -2066,7 +2066,7 @@ class DecorationsHandler:
if isinstance(space, bpy.types.SpaceView3D) and space.local_view:
in_local_view = True
break
# If just entering local view (transition from False to True)
if in_local_view and not self.__class__.was_in_local_view:
self.__class__.local_view_has_annotation = False
@@ -2075,10 +2075,10 @@ class DecorationsHandler:
if element and element.is_a("IfcAnnotation"):
self.__class__.local_view_has_annotation = True
break
# Update the state for next time
self.__class__.was_in_local_view = in_local_view
# Skip decorations if in local view and no IfcAnnotation was selected when entering
if in_local_view and not self.__class__.local_view_has_annotation:
return
@@ -2093,5 +2093,6 @@ class DecorationsHandler:
if not DecoratorData.is_loaded:
DecoratorData.load(self)
for obj, decorator in DecoratorData.data["object_decorators"]:
object_decorators = DecoratorData.data.get("object_decorators", [])
for obj, decorator in object_decorators:
decorator.decorate(context, obj)
@@ -86,7 +86,7 @@ overlapping with geometry. The get_local_view_direction() helper determines if t
camera is viewing from the positive or negative side of each axis.
"""
__all__ = [
__all__ = [ # noqa: RUF022 (unsorted `__all__`)
"GizmoColor",
"GizmoAxis",
"TextAlignment",
@@ -296,6 +296,15 @@ class CreateDrawing(bpy.types.Operator):
self.drawing_index = drawing_i
if self.print_all:
bpy.ops.bim.activate_drawing(drawing=drawing_id, should_view_from_camera=False)
original_cache_setting = self.props.should_use_underlay_cache
self.props.should_use_underlay_cache = False
# Force Blender to process all pending operations
for area in context.screen.areas:
area.tag_redraw()
# Process events to let Blender finish internal cleanup
bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
self.camera = context.scene.camera
assert (camera_element := tool.Ifc.get_entity(self.camera))
@@ -339,6 +348,23 @@ class CreateDrawing(bpy.types.Operator):
f"Failed to create drawing '{self.drawing.Name}' - drawing has underlay but there's no active drawing underlay style.",
)
return {"FINISHED"}
# Clear any local camera setup and force viewport to use scene camera
for area in context.screen.areas:
if area.type == "VIEW_3D":
for space in area.spaces:
if space.type == "VIEW_3D":
# Clear local camera to ensure we use scene.camera
space.use_local_camera = False
space.camera = context.scene.camera
space.region_3d.view_perspective = "CAMERA"
print(f"Set viewport camera to: {context.scene.camera.name}")
break
# Force complete scene update
context.view_layer.update()
context.evaluated_depsgraph_get()
underlay_svg = self.generate_underlay(context)
with profile("Generate linework"):
@@ -3050,9 +3076,9 @@ class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
filename_ext = ".svg"
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)
directory: bpy.props.StringProperty(subtype='DIR_PATH')
directory: bpy.props.StringProperty(subtype="DIR_PATH")
def _execute(self, context):
# Handle both single and multiple file selection
@@ -3661,8 +3687,12 @@ class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator):
if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
try:
tool.Search.import_filter_query(query, filter_groups)
except:
data = json.loads(query)
if isinstance(data, dict) and "filter_structure" in data:
tool.Search.import_filter_structure(data["filter_structure"], filter_groups)
else:
tool.Search.import_filter_query(query, filter_groups)
except Exception:
pass
@@ -3682,12 +3712,41 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
assert element
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
assert pset
if self.filter_mode == "INCLUDE":
query = tool.Search.export_filter_query(props.include_filter_groups) or None
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": query})
filter_groups = props.include_filter_groups
elif self.filter_mode == "EXCLUDE":
query = tool.Search.export_filter_query(props.exclude_filter_groups) or None
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": query})
filter_groups = props.exclude_filter_groups
else:
return
query = tool.Search.export_filter_query(filter_groups) or None
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and query:
filter_structure = []
for filter_group in filter_groups:
group_data = []
for ifc_filter in filter_group.filters:
filter_data = {
"type": ifc_filter.type,
"name": ifc_filter.name,
"value": ifc_filter.value,
"pset": ifc_filter.pset,
"comparison": ifc_filter.comparison,
"filter_mode": ifc_filter.filter_mode,
}
group_data.append(filter_data)
filter_structure.append(group_data)
value = json.dumps({"type": "BBIM_Search", "query": query, "filter_structure": filter_structure})
else:
value = query
if self.filter_mode == "INCLUDE":
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": value})
elif self.filter_mode == "EXCLUDE":
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": value})
props.filter_mode = "NONE"
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
@@ -4015,66 +4074,66 @@ class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator):
tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
class ActivateDrawingByAnnotation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.activate_drawing_by_annotation"
bl_label = "Activate Drawing"
bl_description = "Activate the drawing corresponding to the selected annotation"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
# Check if an annotation object is selected
if not context.selected_objects:
cls.poll_message_set("No object selected")
return False
active_obj = context.active_object
if not active_obj:
cls.poll_message_set("No active object")
return False
element = tool.Ifc.get_entity(active_obj)
if not element:
cls.poll_message_set("Selected object is not an IFC element")
return False
# Check if it's an IfcAnnotation with ObjectType = "SECTION" or "ELEVATION"
if not element.is_a("IfcAnnotation") or element.ObjectType not in ["SECTION", "ELEVATION"]:
cls.poll_message_set("Selected object is not a drawing annotation")
return False
return True
def _execute(self, context):
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in ["SECTION", "ELEVATION"]:
self.report({"ERROR"}, "Selected object is not a drawing annotation")
return {"CANCELLED"}
# Find the drawing/camera element that this annotation references
drawing_element = self.find_drawing_from_annotation(element)
if not drawing_element:
self.report({"ERROR"}, "Could not find drawing element for this annotation")
return {"CANCELLED"}
# Use the existing ActivateDrawing operator with the drawing element's ID
bpy.ops.bim.activate_drawing(drawing=drawing_element.id())
return {"FINISHED"}
def find_drawing_from_annotation(self, annotation_element):
"""Find the drawing/camera element that this annotation references."""
ifc = tool.Ifc.get()
# Check IfcRelAssignsToProduct relationships
for rel in ifc.get_inverse(annotation_element):
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct:
if rel.RelatingProduct.is_a("IfcAnnotation"):
# Found the drawing element!
return rel.RelatingProduct
return None
return None
+4 -2
View File
@@ -479,10 +479,12 @@ class BIM_PT_sheets(Panel):
drawingnamesvg = active_sheet.name
drawingname = drawingnamesvg.split(".svg")[0]
ifc_file = tool.Ifc.get()
ifc_annotations = ifc_file.by_type("IfcAnnotation")
drawingid = None
for annotation in ifc_annotations:
for annotation in ifc_file.by_type("IfcAnnotation"):
if annotation.ObjectType != "DRAWING":
continue
Annotation_Name = annotation.Name.replace(",", "") # Remove commas
if Annotation_Name == drawingname:
drawingid = annotation.id()
+43 -43
View File
@@ -151,23 +151,23 @@ class FilledOpeningGenerator:
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
assert representation
# Check if mapped representation - preserve it
if (representation.RepresentationType == 'MappedRepresentation' and
len(representation.Items) == 1 and
representation.Items[0].is_a("IfcMappedItem")):
if (
representation.RepresentationType == "MappedRepresentation"
and len(representation.Items) == 1
and representation.Items[0].is_a("IfcMappedItem")
):
source_rep = representation.Items[0].MappingSource.MappedRepresentation
representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
source_rep,
exclude=["IfcGeometricRepresentationContext"]
tool.Ifc.get(), source_rep, exclude=["IfcGeometricRepresentationContext"]
)
else:
representation = ifcopenshell.util.representation.resolve_representation(representation)
else:
# Check for library template before generating from filling
template_rep = self.get_opening_template_from_type(filling)
if template_rep:
representation = template_rep
else:
@@ -222,68 +222,68 @@ class FilledOpeningGenerator:
voided_element = opening.VoidsElements[0].RelatingBuildingElement
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
# ALWAYS preserve the existing opening representation (Tessellation, SweptSolid, etc.)
preserved_representation = None
if opening_rep:
if (opening_rep.RepresentationType == 'MappedRepresentation' and
len(opening_rep.Items) == 1 and
opening_rep.Items[0].is_a("IfcMappedItem")):
if (
opening_rep.RepresentationType == "MappedRepresentation"
and len(opening_rep.Items) == 1
and opening_rep.Items[0].is_a("IfcMappedItem")
):
# For mapped representations, copy the underlying representation
preserved_representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
opening_rep.Items[0].MappingSource.MappedRepresentation,
exclude=["IfcGeometricRepresentationContext"]
exclude=["IfcGeometricRepresentationContext"],
)
else:
# For direct representations (non-mapped), copy them too
preserved_representation = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
opening_rep,
exclude=["IfcGeometricRepresentationContext"]
tool.Ifc.get(), opening_rep, exclude=["IfcGeometricRepresentationContext"]
)
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
existing_opening_occurrence = self.get_existing_opening_occurrence_if_any(filling)
# Priority order for choosing representation:
# 1. Existing occurrence with Tessellation (best quality)
# 2. Library template with Tessellation
# 3. Preserved representation from old opening (maintain user's work)
# 4. Generate from filling (last resort)
representation_to_use = None
if existing_opening_occurrence:
representation = ifcopenshell.util.representation.get_representation(
existing_opening_occurrence, "Model", "Body", "MODEL_VIEW"
)
if (representation and
representation.RepresentationType == 'MappedRepresentation' and
len(representation.Items) == 1 and
representation.Items[0].is_a("IfcMappedItem")):
if (
representation
and representation.RepresentationType == "MappedRepresentation"
and len(representation.Items) == 1
and representation.Items[0].is_a("IfcMappedItem")
):
source_rep = representation.Items[0].MappingSource.MappedRepresentation
# Prefer Tessellation from existing occurrence over preserved representation
if source_rep.RepresentationType == 'Tessellation':
if source_rep.RepresentationType == "Tessellation":
representation_to_use = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
source_rep,
exclude=["IfcGeometricRepresentationContext"]
tool.Ifc.get(), source_rep, exclude=["IfcGeometricRepresentationContext"]
)
else:
representation_to_use = ifcopenshell.util.representation.resolve_representation(representation)
if not representation_to_use:
template_rep = self.get_opening_template_from_type(filling)
if template_rep and template_rep.RepresentationType == 'Tessellation':
if template_rep and template_rep.RepresentationType == "Tessellation":
representation_to_use = template_rep
if not representation_to_use and preserved_representation:
representation_to_use = preserved_representation
if not representation_to_use:
opening_obj = tool.Ifc.get_object(opening)
if opening_obj:
@@ -299,7 +299,7 @@ class FilledOpeningGenerator:
ifcopenshell.api.geometry.assign_representation(
tool.Ifc.get(), product=opening, representation=mapped_representation
)
# update voided object representation...
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
for voided_element in voided_elements:
@@ -314,31 +314,31 @@ class FilledOpeningGenerator:
representation=representation,
)
def get_opening_template_from_type(self, filling: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
def get_opening_template_from_type(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""
Check if the filling's type has a stored opening template from library import.
"""
element_type = ifcopenshell.util.element.get_type(filling)
if not element_type:
return None
desc = element_type.Description
if not desc or "||BonsaiOpeningTemplate:" not in desc:
return None
# Extract template ID
marker = desc.split("||BonsaiOpeningTemplate:")[-1]
template_id = int(marker.split("||")[0])
try:
template_rep = tool.Ifc.get().by_id(template_id)
# Make a copy so we don't reuse the same representation instance
copied = ifcopenshell.util.element.copy_deep(
tool.Ifc.get(),
template_rep,
exclude=["IfcGeometricRepresentationContext"]
tool.Ifc.get(), template_rep, exclude=["IfcGeometricRepresentationContext"]
)
return copied
except:
@@ -715,11 +715,8 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
ifc_importer.create_style(element)
def store_opening_template_from_library(
self,
element: ifcopenshell.entity_instance,
library_file: ifcopenshell.file
self, element: ifcopenshell.entity_instance, library_file: ifcopenshell.file
) -> None:
"""
Find an opening representation in the library and copy it to the current file
@@ -729,36 +726,36 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
library_element = library_file.by_guid(element.GlobalId)
except:
return
# Find occurrences with openings in the library
library_occurrences = ifcopenshell.util.element.get_types(library_element)
for occurrence in library_occurrences:
if not getattr(occurrence, "FillsVoids", None):
continue
library_opening = occurrence.FillsVoids[0].RelatingOpeningElement
library_opening_rep = ifcopenshell.util.representation.get_representation(
library_opening, "Model", "Body", "MODEL_VIEW"
)
if not library_opening_rep:
continue
# Check if mapped representation
if (library_opening_rep.RepresentationType == 'MappedRepresentation' and
len(library_opening_rep.Items) == 1 and
library_opening_rep.Items[0].is_a("IfcMappedItem")):
if (
library_opening_rep.RepresentationType == "MappedRepresentation"
and len(library_opening_rep.Items) == 1
and library_opening_rep.Items[0].is_a("IfcMappedItem")
):
mapped_rep = library_opening_rep.Items[0].MappingSource.MappedRepresentation
# Store ALL representation types (Tessellation, SweptSolid, etc.)
template_rep = ifcopenshell.util.element.copy_deep(
self.file,
mapped_rep,
exclude=["IfcGeometricRepresentationContext"]
self.file, mapped_rep, exclude=["IfcGeometricRepresentationContext"]
)
# Store reference in type's Description
current_desc = element.Description or ""
element.Description = f"{current_desc}||BonsaiOpeningTemplate:{template_rep.id()}"
@@ -1059,6 +1056,20 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
return tooltip
def execute(self, context):
if (
tool.Blender.get_addon_preferences().save_metadata_blend_file
and self.should_start_fresh_session
and not self.is_advanced
):
filepath = self.get_filepath()
metadata_path = Path(str(filepath) + ".metadata.blend")
if metadata_path.exists() and metadata_path.is_file():
try:
bpy.ops.bim.load_blend_metadata_and_ifc(filepath=filepath)
return {"FINISHED"}
except Exception as e:
self.report({"WARNING"}, f"Failed to load metadata file, using regular load: {e}")
@persistent
def load_handler(*args):
bpy.app.handlers.load_post.remove(load_handler)
@@ -1747,15 +1758,28 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
bim_props = tool.Blender.get_bim_props()
if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"):
tool.Ifc.set_path(output_file)
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
if save_blend_file:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
bim_props.is_dirty = False
if tool.Blender.get_addon_preferences().save_metadata_blend_file:
try:
bpy.ops.bim.save_blend_metadata_file()
blendmetadata_path = output_file + ".metadata.blend"
self.report(
{"INFO"},
f'IFC Project "{os.path.basename(output_file)}" And Metadata File Saved to: {os.path.basename(blendmetadata_path)}',
)
except Exception as e:
self.report({"ERROR"}, f"Failed to save blend metadata file: {e}")
else:
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
if save_blend_file:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
self.report(
{"INFO"},
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
)
bonsai.bim.handler.refresh_ui_data()
self.report(
{"INFO"},
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
)
@classmethod
def description(cls, context, properties):
@@ -331,6 +331,13 @@ class BIM_PT_project(Panel):
col.prop(props, "ifc_file", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
if tool.Blender.get_addon_preferences().save_metadata_blend_file:
row = self.layout.row(align=True)
col = row.column()
col.enabled = False
metadata_filename = os.path.basename(props.ifc_file) + ".metadata.blend"
col.label(text=f"Saving session data to: {metadata_filename}")
class BIM_PT_new_project_wizard(Panel):
bl_label = "New Project Wizard"
@@ -24,12 +24,15 @@ classes = (
operator.ActivateIfcClassFilter,
operator.AddFilter,
operator.AddFilterGroup,
operator.ApplyFilterFromText,
operator.ColourByProperty,
operator.EditFilterQuery,
operator.FilterValueSuggestions,
operator.LoadColourscheme,
operator.LoadSearch,
operator.RemoveFilter,
operator.RemoveFilterGroup,
operator.RemoveSearch,
operator.ResetObjectColours,
operator.SaveColourscheme,
operator.SaveSearch,
@@ -40,6 +43,7 @@ classes = (
operator.SelectIfcClass,
operator.SelectSimilar,
operator.ShowAllElements,
operator.ToggleFilterInclusion,
operator.ToggleFilterSelection,
prop.BIMColour,
prop.BIMFilterItem,
@@ -55,7 +59,9 @@ classes = (
def register():
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
bpy.types.TEXT_HT_header.append(operator.draw_text_editor_header)
def unregister():
del bpy.types.Scene.BIMSearchProperties
bpy.types.TEXT_HT_header.remove(operator.draw_text_editor_header)
+680 -20
View File
@@ -40,9 +40,476 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from bonsai.bim.prop import StrProperty
from typing import TYPE_CHECKING, Literal, get_args, assert_never
def draw_text_editor_header(self, context):
if context.space_data.text and context.space_data.text.name.startswith("FilterQuery_"):
layout = self.layout
layout.separator()
op = layout.operator("bim.apply_filter_from_text", text="Apply Filter Configuration", icon="CHECKMARK")
def update_filter_search_value(self: "FilterValueSuggestions", context: bpy.types.Context) -> None:
filter_groups = tool.Search.get_filter_groups(self.module)
ifc_filter = filter_groups[self.group_index].filters[self.filter_index]
value = self.search_value
if " < " in value:
value = value.split(" < ")[-1]
if ifc_filter.type == "entity":
if " (superclass)" in value:
value = value.replace(" (superclass)", "")
if " > " in value:
value = value.split(" > ")[-1]
elif ifc_filter.type == "instance":
if ": " in value:
value = value.split(": ")[-1]
elif " (" in value:
hierarchy_class = value.split(" (")
element_class = hierarchy_class[-1].rstrip(")")
element_name = hierarchy_class[0] if len(hierarchy_class) > 1 else None
ifc_file = tool.Ifc.get()
if ifc_file:
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
if element.is_a() == element_class:
if element_name is None or (hasattr(element, "Name") and element.Name == element_name):
value = element.GlobalId
break
except:
continue
elif ifc_filter.type == "parent":
if " (" in value:
value = value.split(" (")[0]
if ifc_filter.type == "property":
if self.suggestion_type == "pset":
ifc_filter.pset = value
elif self.suggestion_type == "property_name":
ifc_filter.name = value
else:
ifc_filter.value = value
elif ifc_filter.type == "attribute":
if self.suggestion_type == "attribute_name":
ifc_filter.name = value
else:
ifc_filter.value = value
else:
ifc_filter.value = value
if self.first_launch:
self.first_launch = False
else:
context.window.screen = context.window.screen
class FilterValueSuggestions(Operator):
bl_idname = "bim.filter_value_suggestions"
bl_label = "Filter Value Suggestions"
bl_description = "Get suggestions for filter values from the current IFC file"
bl_options = {"REGISTER", "UNDO"}
group_index: IntProperty()
filter_index: IntProperty()
module: StringProperty(default="search")
filter_type: StringProperty()
suggestion_type: StringProperty(default="value")
first_launch: BoolProperty(default=True, options={"SKIP_SAVE"})
search_value: StringProperty(
name="Search",
description="Search for filter values",
update=update_filter_search_value,
default="",
options={"SKIP_SAVE"},
)
collection_values: CollectionProperty(type=StrProperty, options={"SKIP_SAVE"})
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"WARNING"}, "No IFC file loaded")
return {"CANCELLED"}
filter_groups = tool.Search.get_filter_groups(self.module)
ifc_filter = filter_groups[self.group_index].filters[self.filter_index]
string_suggestions = self.get_suggestions(ifc_file, ifc_filter)
if not string_suggestions:
self.report({"INFO"}, f"No suggestions available")
return {"CANCELLED"}
self.collection_values.clear()
for suggestion in natsorted(string_suggestions):
self.collection_values.add().name = suggestion
return context.window_manager.invoke_props_dialog(self, width=800)
def draw(self, context):
layout = self.layout
filter_groups = tool.Search.get_filter_groups(self.module)
ifc_filter = filter_groups[self.group_index].filters[self.filter_index]
label_map = {
"entity": "Select Class",
"type": "Select Type",
"material": "Select Material",
"location": "Select Location",
"group": "Select Group",
"classification": "Select Classification",
"parent": "Select Parent",
"instance": "Select GlobalId",
}
if ifc_filter.type == "attribute":
if self.suggestion_type == "attribute_value":
label = f"Select Value for {ifc_filter.name}"
else:
label = "Select Attribute Name"
elif ifc_filter.type == "property":
if self.suggestion_type == "property_value":
label = f"Select Value for {ifc_filter.pset}.{ifc_filter.name}"
elif self.suggestion_type == "property_name":
label = f"Select Property from {ifc_filter.pset}"
else:
label = "Select Property Set"
else:
label = label_map.get(ifc_filter.type, "Select Value")
row = layout.row()
row.label(text=label)
row = layout.row()
row.prop_search(
self,
"search_value",
self,
"collection_values",
text="",
results_are_suggestions=True,
)
def get_suggestions(self, ifc_file, ifc_filter):
suggestions = set()
if ifc_filter.type == "entity":
suggestions = self.get_entity_suggestions(ifc_file)
elif ifc_filter.type == "type":
suggestions = self.get_type_suggestions(ifc_file)
elif ifc_filter.type == "material":
suggestions = self.get_material_suggestions(ifc_file)
elif ifc_filter.type == "location":
suggestions = self.get_location_suggestions(ifc_file)
elif ifc_filter.type == "group":
suggestions = self.get_group_suggestions(ifc_file)
elif ifc_filter.type == "classification":
suggestions = self.get_classification_suggestions(ifc_file)
elif ifc_filter.type == "parent":
suggestions = self.get_parent_suggestions(ifc_file)
elif ifc_filter.type == "instance":
suggestions = self.get_instance_suggestions(ifc_file)
elif ifc_filter.type == "attribute":
if self.suggestion_type == "attribute_name":
suggestions = self.get_attribute_names(ifc_file)
else:
suggestions = self.get_attribute_values(ifc_file, ifc_filter.name)
elif ifc_filter.type == "property":
if self.suggestion_type == "pset":
suggestions = self.get_property_sets(ifc_file)
elif self.suggestion_type == "property_name":
suggestions = self.get_property_names(ifc_file, ifc_filter.pset)
else:
suggestions = self.get_property_values(ifc_file, ifc_filter.pset, ifc_filter.name)
return suggestions
def build_hierarchy_path(self, element):
path = []
current = element
while current:
if hasattr(current, "Name") and current.Name:
path.insert(0, current.Name)
parent = None
if hasattr(current, "Decomposes") and current.Decomposes:
for rel in current.Decomposes:
if hasattr(rel, "RelatingObject"):
parent = rel.RelatingObject
break
if not parent and hasattr(current, "ContainedInStructure") and current.ContainedInStructure:
for rel in current.ContainedInStructure:
if hasattr(rel, "RelatingStructure"):
parent = rel.RelatingStructure
break
current = parent
return path
def get_entity_suggestions(self, ifc_file):
all_classes = set()
schema = tool.Ifc.schema()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
class_name = element.is_a()
try:
entity = schema.declaration_by_name(class_name).as_entity()
current = entity
chain_names = [class_name]
while current.supertype():
supertype = current.supertype()
chain_names.insert(0, supertype.name())
current = supertype
if len(chain_names) > 1:
all_classes.add(" > ".join(chain_names))
for i in range(len(chain_names) - 1):
superclass_chain = " > ".join(chain_names[: i + 1])
all_classes.add(f"{superclass_chain} (superclass)")
else:
all_classes.add(class_name)
except:
all_classes.add(class_name)
except:
continue
return all_classes
def get_type_suggestions(self, ifc_file):
suggestions = set()
for element_type in ifc_file.by_type("IfcTypeObject"):
if element_type.Name:
hierarchy_path = self.build_hierarchy_path(element_type)
if len(hierarchy_path) > 1:
suggestions.add(" < ".join(hierarchy_path))
else:
suggestions.add(element_type.Name)
return suggestions
def get_material_suggestions(self, ifc_file):
suggestions = set()
for material in ifc_file.by_type("IfcMaterial"):
if material.Name:
suggestions.add(material.Name)
return suggestions
def get_location_suggestions(self, ifc_file):
suggestions = set()
for spatial in ifc_file.by_type("IfcSpatialStructureElement"):
if spatial.Name:
hierarchy_path = self.build_hierarchy_path(spatial)
if len(hierarchy_path) > 1:
suggestions.add(" < ".join(hierarchy_path))
else:
suggestions.add(spatial.Name)
return suggestions
def get_group_suggestions(self, ifc_file):
suggestions = set()
for group in ifc_file.by_type("IfcGroup"):
if group.Name:
hierarchy_path = self.build_hierarchy_path(group)
if len(hierarchy_path) > 1:
suggestions.add(" < ".join(hierarchy_path))
else:
suggestions.add(group.Name)
return suggestions
def get_classification_suggestions(self, ifc_file):
suggestions = set()
for ref in ifc_file.by_type("IfcClassificationReference"):
if ref.Identification:
suggestions.add(ref.Identification)
return suggestions
def get_parent_suggestions(self, ifc_file):
suggestions = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
has_children = False
if hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy:
has_children = True
elif hasattr(element, "ContainsElements") and element.ContainsElements:
has_children = True
elif hasattr(element, "HasOpenings") and element.HasOpenings:
has_children = True
if has_children:
hierarchy_path = self.build_hierarchy_path(element)
element_class = element.is_a()
if len(hierarchy_path) > 0:
hierarchy_str = " < ".join(hierarchy_path)
suggestions.add(f"{hierarchy_str} ({element_class})")
else:
element_name = element.Name if hasattr(element, "Name") and element.Name else element_class
suggestions.add(f"{element_name} ({element_class})")
except:
continue
return suggestions
def get_instance_suggestions(self, ifc_file):
suggestions = set()
element_data = []
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
if hasattr(element, "GlobalId") and element.GlobalId:
hierarchy_path = self.build_hierarchy_path(element)
element_class = element.is_a()
if len(hierarchy_path) > 0:
hierarchy_str = " < ".join(hierarchy_path)
display_str = f"{hierarchy_str} ({element_class})"
else:
display_str = f"({element_class})"
element_data.append((display_str, element.GlobalId))
except:
continue
display_counts = {}
for display_str, global_id in element_data:
display_counts[display_str] = display_counts.get(display_str, 0) + 1
for display_str, global_id in element_data:
if display_counts[display_str] > 1:
suggestions.add(f"{display_str}: {global_id}")
else:
suggestions.add(display_str)
return suggestions
def get_property_sets(self, ifc_file):
psets = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
for definition in getattr(element, "IsDefinedBy", []):
if definition.is_a("IfcRelDefinesByProperties"):
pset = definition.RelatingPropertyDefinition
if pset.is_a("IfcPropertySet") and pset.Name:
psets.add(pset.Name)
except:
continue
return psets
def get_property_names(self, ifc_file, pset_name):
property_names = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
for definition in getattr(element, "IsDefinedBy", []):
if definition.is_a("IfcRelDefinesByProperties"):
pset = definition.RelatingPropertyDefinition
if pset.is_a("IfcPropertySet") and pset.Name == pset_name:
if pset.HasProperties:
for prop in pset.HasProperties:
if hasattr(prop, "Name") and prop.Name:
property_names.add(prop.Name)
except:
continue
return property_names
def get_property_values(self, ifc_file, pset_name, property_name):
property_values = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
for definition in getattr(element, "IsDefinedBy", []):
if definition.is_a("IfcRelDefinesByProperties"):
pset = definition.RelatingPropertyDefinition
if pset.is_a("IfcPropertySet") and pset.Name == pset_name:
if pset.HasProperties:
for prop in pset.HasProperties:
if hasattr(prop, "Name") and prop.Name == property_name:
if hasattr(prop, "NominalValue") and prop.NominalValue:
try:
value = prop.NominalValue.wrappedValue
if value is not None and value != "":
if not hasattr(value, "is_a") and not isinstance(
value, (tuple, list)
):
str_value = str(value)
if not str_value.startswith("#") and not str_value.startswith(
"("
):
property_values.add(str_value)
except:
continue
except:
continue
return property_values
def get_attribute_names(self, ifc_file):
attribute_names = set()
schema = tool.Ifc.schema()
ifc_classes = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
ifc_classes.add(element.is_a())
except:
continue
for ifc_class in ifc_classes:
try:
entity = schema.declaration_by_name(ifc_class).as_entity()
attributes = entity.all_attributes()
for attr in attributes:
attribute_names.add(attr.name())
except:
continue
return attribute_names
def get_attribute_values(self, ifc_file, attribute_name):
attribute_values = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
if element.is_a("IfcRelationship") or element.is_a("IfcTypeObject"):
continue
if hasattr(element, attribute_name):
value = getattr(element, attribute_name, None)
if value is not None and value != "":
if not hasattr(value, "is_a") and not isinstance(value, (tuple, list)):
str_value = str(value)
if not str_value.startswith("#") and not str_value.startswith("("):
attribute_values.add(str_value)
except:
continue
return attribute_values
class AddFilterGroup(Operator):
bl_idname = "bim.add_filter_group"
bl_label = "Add Filter Group"
@@ -96,11 +563,38 @@ class AddFilter(Operator):
def execute(self, context):
filter_groups = tool.Search.get_filter_groups(self.module)
if self.index >= len(filter_groups):
filter_groups.add()
new = filter_groups[self.index].filters.add()
new.type = self.type
return {"FINISHED"}
class ToggleFilterInclusion(Operator):
bl_idname = "bim.toggle_filter_inclusion"
bl_label = "Toggle Filter Mode"
bl_description = "Cycle between Add (+), Subtract (-), and Filter modes for this filter"
bl_options = {"REGISTER", "UNDO"}
group_index: IntProperty()
filter_index: IntProperty()
module: StringProperty()
def execute(self, context):
filter_groups = tool.Search.get_filter_groups(self.module)
filter_group = filter_groups[self.group_index]
ifc_filter = filter_group.filters[self.filter_index]
if ifc_filter.filter_mode == "ADD":
ifc_filter.filter_mode = "SUBTRACT"
elif ifc_filter.filter_mode == "SUBTRACT":
ifc_filter.filter_mode = "FILTER"
else:
ifc_filter.filter_mode = "ADD"
return {"FINISHED"}
class SelectFilterElements(bpy.types.Operator):
bl_idname = "bim.select_filter_elements"
bl_label = "Select Filter Elements"
@@ -120,6 +614,47 @@ class SelectFilterElements(bpy.types.Operator):
return {"FINISHED"}
class ApplyFilterFromText(Operator, tool.Ifc.Operator):
bl_idname = "bim.apply_filter_from_text"
bl_label = "Apply Filter Configuration"
bl_description = "Apply the JSON filter configuration from the current text block"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if context.area and context.area.type == "TEXT_EDITOR":
space = context.space_data
if space.text and space.text.name.startswith("FilterQuery_"):
return True
return False
def execute(self, context):
space = context.space_data
text = space.text
if not text or not text.name.startswith("FilterQuery_"):
self.report({"ERROR"}, "No valid filter configuration text block")
return {"CANCELLED"}
module = text.name.replace("FilterQuery_", "")
try:
json_data = json.loads(text.as_string())
filter_structure = json_data.get("filter_structure", [])
filter_groups = tool.Search.get_filter_groups(module)
tool.Search.import_filter_structure(filter_structure, filter_groups)
self.report({"INFO"}, "Filter configuration applied successfully")
if len(context.window_manager.windows) > 1:
bpy.ops.wm.window_close()
except Exception as e:
self.report({"ERROR"}, f"Invalid JSON: {str(e)}")
return {"CANCELLED"}
return {"FINISHED"}
class EditFilterQuery(Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_filter_query"
bl_label = "Edit Filter Query"
@@ -127,29 +662,82 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
query: StringProperty(name="Query")
old_query: StringProperty(name="Old Query")
module: StringProperty()
module: StringProperty(default="search")
def _execute(self, context):
if self.query == self.old_query:
return
module = getattr(self, "module", "search")
filter_groups = tool.Search.get_filter_groups(self.module)
try:
tool.Search.import_filter_query(self.query, filter_groups)
except:
return
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
if self.query == self.old_query:
return
filter_groups = tool.Search.get_filter_groups(module)
try:
tool.Search.import_filter_query(self.query, filter_groups)
except:
return
def draw(self, context):
row = self.layout.row()
row.prop(self, "query", text="")
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
row = self.layout.row()
row.prop(self, "query", text="")
def invoke(self, context, event):
filter_groups = tool.Search.get_filter_groups(self.module)
module = getattr(self, "module", "search")
filter_groups = tool.Search.get_filter_groups(module)
self.query = tool.Search.export_filter_query(filter_groups)
self.old_query = self.query
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
filter_structure = []
for filter_group in filter_groups:
group_data = []
for ifc_filter in filter_group.filters:
filter_data = {
"type": ifc_filter.type,
"name": ifc_filter.name,
"value": ifc_filter.value,
"pset": ifc_filter.pset,
"comparison": ifc_filter.comparison,
"filter_mode": ifc_filter.filter_mode,
}
group_data.append(filter_data)
filter_structure.append(group_data)
return context.window_manager.invoke_props_dialog(self)
query = tool.Search.export_filter_query(filter_groups)
json_data = {"type": "BBIM_Search", "query": query, "filter_structure": filter_structure}
text_block_name = f"FilterQuery_{module}"
text = bpy.data.texts.get(text_block_name)
if not text:
text = bpy.data.texts.new(text_block_name)
text.clear()
text.write(json.dumps(json_data, indent=2))
bpy.ops.wm.window_new()
new_window = context.window_manager.windows[-1]
new_area = new_window.screen.areas[0]
new_area.type = "TEXT_EDITOR"
text_space = None
for space in new_area.spaces:
if space.type == "TEXT_EDITOR":
text_space = space
break
if text_space:
text_space.text = text
self.report(
{"INFO"}, "Compact text editor opened. Edit JSON and click 'Apply Filter Configuration' in header"
)
return {"FINISHED"}
else:
self.query = tool.Search.export_filter_query(filter_groups)
self.old_query = self.query
return context.window_manager.invoke_props_dialog(self, width=400)
class Search(Operator):
@@ -174,9 +762,26 @@ class Search(Operator):
else:
assert_never(self.property_group)
results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
)
preferences = tool.Blender.get_addon_preferences()
# Migrate old ! prefix filters to new filter_mode system when preferences are enabled
if (
preferences.chain_filter_with_set_operations
or preferences.default_filter_with_set_operations_for_globalid_and_class
):
for filter_group in props.filter_groups:
for ifc_filter in filter_group.filters:
if ifc_filter.type not in ["entity", "instance"]:
continue
if ifc_filter.value.startswith("!"):
ifc_filter.value = ifc_filter.value[1:]
ifc_filter.filter_mode = "SUBTRACT"
results = tool.Search.execute_filter_groups(props.filter_groups)
else:
results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
)
objs = [obj for e in results if isinstance(obj := tool.Ifc.get_object(e), bpy.types.Object)]
for obj in objs:
@@ -242,13 +847,28 @@ class SaveSearch(Operator, tool.Ifc.Operator):
try:
query = tool.Search.export_filter_query(filter_groups)
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
results = tool.Search.execute_filter_groups(filter_groups)
filter_structure = []
for filter_group in filter_groups:
group_data = []
for ifc_filter in filter_group.filters:
filter_data = {
"type": ifc_filter.type,
"name": ifc_filter.name,
"value": ifc_filter.value,
"pset": ifc_filter.pset,
"comparison": ifc_filter.comparison,
"filter_mode": ifc_filter.filter_mode,
}
group_data.append(filter_data)
filter_structure.append(group_data)
except:
print(traceback.format_exc())
self.report({"ERROR"}, "Error occurred trying save search.")
return
description = json.dumps({"type": "BBIM_Search", "query": query})
description = json.dumps({"type": "BBIM_Search", "query": query, "filter_structure": filter_structure})
ifc_file = tool.Ifc.get()
group = next(
(
@@ -289,7 +909,12 @@ class LoadSearch(Operator, tool.Ifc.Operator):
filter_groups = tool.Search.get_filter_groups(self.module)
props = tool.Search.get_search_props()
group = tool.Ifc.get().by_id(int(props.saved_searches))
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
group_data = tool.Search.get_group_data(group)
if group_data and "filter_structure" in group_data:
tool.Search.import_filter_structure(group_data["filter_structure"], filter_groups)
else:
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
def draw(self, context):
assert self.layout
@@ -302,6 +927,41 @@ class LoadSearch(Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
class RemoveSearch(Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_search"
bl_label = "Remove Search"
bl_description = "Remove a saved search filter"
bl_options = {"REGISTER", "UNDO"}
module: StringProperty()
def _execute(self, context):
props = tool.Search.get_search_props()
group_id = props.saved_searches
if not group_id:
self.report({"ERROR"}, "No search selected for removal")
return
group = tool.Ifc.get().by_id(int(group_id))
group_name = group.Name or "Unnamed"
ifcopenshell.api.group.remove_group(tool.Ifc.get(), group=group)
tool.Search.patch_search_ifcgroups()
self.report({"INFO"}, f"Removed saved search: {group_name}")
def draw(self, context):
self.layout.label(text="Select search to remove:", icon="ERROR")
row = self.layout.row()
props = tool.Search.get_search_props()
row.prop(props, "saved_searches", text="")
def invoke(self, context, event):
tool.Search.patch_search_ifcgroups()
from bonsai.bim.module.search.data import SearchData
if not SearchData.is_loaded:
SearchData.load()
return context.window_manager.invoke_props_dialog(self)
class ColourByProperty(Operator):
bl_idname = "bim.colour_by_property"
bl_label = "Colour by Property"
+1 -1
View File
@@ -101,7 +101,7 @@ class TypeData:
element_type = ifcopenshell.util.element.get_type(element)
if not element_type:
return results
data = element_type.get_info()
if "GlobalId" in data:
excluded_keys = ["id", "type"]
+12 -12
View File
@@ -429,18 +429,18 @@ class EnableEditingTypeAttributes(bpy.types.Operator):
obj = context.active_object
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
return {"CANCELLED"}
element_type = ifcopenshell.util.element.get_type(element)
if not element_type:
return {"CANCELLED"}
props = tool.Type.get_object_type_props(obj)
props.type_attributes.clear()
bonsai.bim.helper.import_attributes(element_type, props.type_attributes)
props.is_editing_type_attributes = True
return {"FINISHED"}
@@ -456,7 +456,7 @@ class DisableEditingTypeAttributes(bpy.types.Operator):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Type.get_object_type_props(obj)
props.type_attributes.clear()
props.property_unset("is_editing_type_attributes")
@@ -473,24 +473,24 @@ class EditTypeAttributes(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
if not obj:
return {"CANCELLED"}
element = tool.Ifc.get_entity(obj)
if not element:
return {"CANCELLED"}
element_type = ifcopenshell.util.element.get_type(element)
if not element_type:
return {"CANCELLED"}
props = tool.Type.get_object_type_props(obj)
attributes = bonsai.bim.helper.export_attributes(props.type_attributes)
ifcopenshell.api.attribute.edit_attributes(tool.Ifc.get(), product=element_type, attributes=attributes)
type_obj = tool.Ifc.get_object(element_type)
if type_obj:
tool.Root.set_object_name(type_obj, element_type)
bpy.ops.bim.disable_editing_type_attributes()
return {"FINISHED"}
+7 -6
View File
@@ -124,27 +124,28 @@ class BIM_PT_type_attributes(Panel):
def draw(self, context):
if not TypeData.is_loaded:
TypeData.load()
assert (layout := self.layout)
assert (obj := context.active_object)
if not TypeData.data.get("relating_type"):
layout.label(text="No Relating Type", icon="INFO")
return
props = tool.Type.get_object_type_props(obj)
if props.is_editing_type_attributes:
row = layout.row(align=True)
row.operator("bim.edit_type_attributes", icon="CHECKMARK", text="Save Attributes")
row.operator("bim.disable_editing_type_attributes", icon="CANCEL", text="")
import bonsai.bim.helper
bonsai.bim.helper.draw_attributes(props.type_attributes, layout)
else:
row = layout.row()
row.operator("bim.enable_editing_type_attributes", icon="GREASEPENCIL", text="Edit")
for attribute in TypeData.data["relating_type_attributes"]:
row = layout.row(align=True)
row.label(text=attribute["name"])
+391
View File
@@ -33,6 +33,15 @@ import bonsai.bim
import bonsai.tool as tool
import bonsai.bim.handler
from enum import Enum
from bonsai.bim.helper import (
get_all_tab_panels,
get_tab_visibility,
set_tab_visibility,
get_tab_names,
get_panel_config,
initialize_panel_properties,
initialize_tab_visibilities,
)
from bpy_extras.io_utils import ImportHelper
from bonsai.bim import import_ifc
from bonsai.bim.prop import StrProperty
@@ -236,6 +245,147 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
return ImportHelper.invoke(self, context, event)
class SaveBlendMetadataFile(bpy.types.Operator):
bl_idname = "bim.save_blend_metadata_file"
bl_label = "Save Blend Metadata File"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
"""
Save the current blend file as a metadata-only file (no geometry), preserving settings, window arrangement, geometry nodes, etc.
"""
props = tool.Blender.get_bim_props()
ifc_file = getattr(props, "ifc_file", None)
if not ifc_file:
self.report({"WARNING"}, "No IFC file path set.")
return {"CANCELLED"}
blendmetadata_path = ifc_file + ".metadata.blend"
# Save a temporary copy of the current blend file
temp_path = bpy.path.abspath("//__temp_blendmetadata.blend")
bpy.ops.wm.save_as_mainfile(filepath=temp_path, copy=True)
cleanup_script = f"""
import bpy
# Ensure all styles are loaded before attempting to remove them
try:
bpy.ops.bim.load_styles()
except Exception:
pass
# 1. Collect all IfcStyle material names
ifcstyle_material_names = []
try:
styles_props = getattr(bpy.context.scene, "BIMStylesProperties", None)
if styles_props is None and bpy.data.scenes:
styles_props = getattr(bpy.data.scenes[0], "BIMStylesProperties", None)
if styles_props:
for style in list(styles_props.styles):
material = getattr(style, "blender_material", None)
if material and material.name:
ifcstyle_material_names.append(material.name)
except Exception:
pass
# 2. Purge IfcStore
try:
from bonsai.bim.ifc import IfcStore
except ImportError:
IfcStore = None
if IfcStore:
try:
IfcStore.purge()
except Exception:
pass
# 3. Remove all collections named IfcProject*
for collection in list(bpy.data.collections):
if collection.name.startswith('IfcProject'):
try:
bpy.data.collections.remove(collection, do_unlink=True)
except Exception:
pass
# 4. Purge orphaned data blocks after removing IfcProject collections
try:
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
except Exception:
pass
# 5. Remove all materials corresponding to the IfcStyles we collected
materials_removed = 0
try:
for mat_name in ifcstyle_material_names:
if mat_name in bpy.data.materials:
try:
bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True)
materials_removed += 1
except Exception:
pass
except Exception:
pass
bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}')
"""
import tempfile
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as script_file:
script_file.write(cleanup_script)
script_path = script_file.name
blender_exe = bpy.app.binary_path
import subprocess
result = subprocess.run(
[blender_exe, temp_path, "--background", "--python", script_path], capture_output=True, text=True
)
# Print the output from the background process (includes debug info)
if result.stdout:
print("\n=== Background Blender Output ===")
print(result.stdout)
if result.stderr:
print("\n=== Background Blender Errors ===")
print(result.stderr)
try:
os.remove(temp_path)
os.remove(script_path)
except Exception:
pass
return {"FINISHED"}
class LoadBlendMetadataAndIFC(bpy.types.Operator):
bl_idname = "bim.load_blend_metadata_and_ifc"
bl_label = "Load Blend Metadata and IFC"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(name="IFC File Path", default="")
def execute(self, context):
ifc_file = self.filepath
if not ifc_file:
props = tool.Blender.get_bim_props()
ifc_file = getattr(props, "ifc_file", None)
if not ifc_file:
self.report({"WARNING"}, "No IFC file path set.")
return {"CANCELLED"}
metadata_path = ifc_file + ".metadata.blend"
# Open the metadata blend file
bpy.ops.wm.open_mainfile(filepath=metadata_path)
# After loading metadata, clear blend warning (no geometry loaded yet)
props = tool.Blender.get_bim_props()
props.has_blend_warning = False
# Load the IFC file into the current session (preserve layout)
bpy.ops.bim.load_project(filepath=ifc_file, should_start_fresh_session=False)
self.report({"INFO"}, f"Loaded metadata and IFC: {metadata_path}, {ifc_file}")
return {"FINISHED"}
# TODO: Unused operator.
# Is there a need for this or 'DIR_PATH' propety subtype does almost the same,
# but also has alt+click?
@@ -1605,3 +1755,244 @@ class BIM_OT_attribute_remove_subitem(bpy.types.Operator):
attr.is_null = True
return {"FINISHED"}
class BIM_UL_tab_panels(bpy.types.UIList):
"""UIList for Tab Panels"""
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
row = layout.row(align=True)
row.label(text=item["bl_label"])
row.operator(
"bim.toggle_panel_visibility",
text="",
icon="HIDE_OFF" if item.get("visible", True) else "HIDE_ON",
).action = f"TOGGLE_VISIBILITY_{item.name}"
row.operator(
"bim.bookmark_panel",
text="",
icon="SOLO_ON" if item.get("bookmarked", False) else "SOLO_OFF",
).action = f"BOOKMARK_{item.name}"
class BIM_OT_toggle_panel_visibility(bpy.types.Operator):
"""Toggle Panel Visibility"""
bl_idname = "bim.toggle_panel_visibility"
bl_label = "Toggle Panel Visibility"
bl_options = {"REGISTER", "UNDO"}
action: bpy.props.StringProperty()
def execute(self, context):
panel_name = self.action.replace("TOGGLE_VISIBILITY_", "")
active_tab = getattr(context.scene, "active_tab_name", None) or getattr(
tool.Blender.get_bim_props(), "tab", None
)
is_bookmark_tab = active_tab == "BOOKMARK"
panel_config = get_panel_config(panel_name, create_if_missing=True)
if panel_config:
if is_bookmark_tab:
panel_config.is_visible_in_bookmarks = not panel_config.is_visible_in_bookmarks
new_value = panel_config.is_visible_in_bookmarks
else:
panel_config.is_visible_in_tab = not panel_config.is_visible_in_tab
new_value = panel_config.is_visible_in_tab
for item in context.scene.tab_panels:
if item.name == panel_name:
item["visible"] = new_value
break
for area in bpy.context.window.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
tab_context = "Bookmarks" if is_bookmark_tab else "Tab"
self.report({"INFO"}, f"Toggled visibility for {panel_name} in {tab_context}.")
return {"FINISHED"}
class BIM_OT_bookmark_panel(bpy.types.Operator):
"""Bookmark Panel"""
bl_idname = "bim.bookmark_panel"
bl_label = "Bookmark Panel"
bl_options = {"REGISTER", "UNDO"}
action: bpy.props.StringProperty()
def execute(self, context):
panel_name = self.action.replace("BOOKMARK_", "")
panel_config = get_panel_config(panel_name, create_if_missing=True)
if panel_config:
panel_config.is_bookmarked = not panel_config.is_bookmarked
for item in context.scene.tab_panels:
if item.name == panel_name:
item["bookmarked"] = panel_config.is_bookmarked
break
for area in bpy.context.window.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
self.report({"INFO"}, f"Toggled bookmark for {panel_name}.")
return {"FINISHED"}
class BIM_OT_manage_tab_panels(bpy.types.Operator):
"""Manage Tab Panels"""
bl_idname = "bim.manage_tab_panels"
bl_label = "Manage Tab Panels"
bl_options = {"REGISTER", "UNDO"}
tab_name: bpy.props.StringProperty()
def invoke(self, context, event):
context.scene.active_tab_name = self.tab_name
context.scene.tab_panels.clear()
initialize_tab_visibilities()
initialize_panel_properties()
all_panels = get_all_tab_panels(force_refresh=True)
for panel_data in all_panels.get(self.tab_name, []):
panel_name = panel_data.get("bl_idname", "")
panel_label = panel_data.get("bl_label", "")
if not panel_name or not panel_label:
continue
item = context.scene.tab_panels.add()
item.name = panel_name
item["bl_label"] = panel_label
panel_config = get_panel_config(panel_name, create_if_missing=True)
if panel_config:
if self.tab_name == "BOOKMARK":
item["visible"] = panel_config.is_visible_in_bookmarks
else:
item["visible"] = panel_config.is_visible_in_tab
item["bookmarked"] = panel_config.is_bookmarked
else:
item["visible"] = True
item["bookmarked"] = False
return context.window_manager.invoke_popup(self)
def draw(self, context):
layout = self.layout
layout.label(text=f"Manage Panels for {self.tab_name} Tab")
row = layout.row()
row.template_list("BIM_UL_tab_panels", "", context.scene, "tab_panels", context.scene, "active_tab_panel_index")
def execute(self, context):
for item in context.scene.tab_panels:
panel_config = get_panel_config(item.name, create_if_missing=True)
if panel_config:
if self.tab_name == "BOOKMARK":
panel_config.is_visible_in_bookmarks = item["visible"]
else:
panel_config.is_visible_in_tab = item["visible"]
panel_config.is_bookmarked = item["bookmarked"]
self.report({"INFO"}, f"Panels for {self.tab_name} managed successfully.")
return {"FINISHED"}
class BIM_OT_manage_tab_visibility(bpy.types.Operator):
"""Manage Tab Visibility"""
bl_idname = "bim.manage_tab_visibility"
bl_label = "Manage Tab Visibility"
bl_options = {"REGISTER", "UNDO"}
def draw(self, context):
layout = self.layout
row = layout.row()
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.reset_ui_layout", icon="FILE_REFRESH", text="")
row = layout.row()
row = self.layout.row(align=True)
row.alignment = "CENTER"
for tab_name in get_tab_names():
row = layout.row()
row.label(text=tab_name)
is_visible = get_tab_visibility(tab_name)
icon = "HIDE_OFF" if is_visible else "HIDE_ON"
op = row.operator("bim.toggle_tab_visibility", text="", icon=icon)
op.tab_name = tab_name
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self)
class BIM_OT_toggle_tab_visibility(bpy.types.Operator):
"""Toggle Tab Visibility"""
bl_idname = "bim.toggle_tab_visibility"
bl_label = "Toggle Tab Visibility"
bl_options = {"REGISTER", "UNDO"}
tab_name: bpy.props.StringProperty()
def execute(self, context):
if self.tab_name in get_tab_names():
current_visibility = get_tab_visibility(self.tab_name)
set_tab_visibility(self.tab_name, not current_visibility)
for area in bpy.context.window.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
self.report({"INFO"}, f"Toggled visibility for {self.tab_name}.")
return {"FINISHED"}
class BIM_OT_reset_ui_layout(bpy.types.Operator):
"""Reset UI Layout to Default"""
bl_idname = "bim.reset_ui_layout"
bl_label = "Reset UI Layout"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for tab_name in get_tab_names():
set_tab_visibility(tab_name, True)
get_all_tab_panels()["BOOKMARK"] = [{}]
for tab_name, panels in get_all_tab_panels().items():
for panel in panels:
panel_name = panel.get("bl_idname", "")
if not panel_name:
continue
show_prop_name = f"show_{panel_name.lower()}"
if hasattr(context.scene, show_prop_name):
setattr(context.scene, show_prop_name, True)
bookmark_prop_name = f"bookmark_{panel_name.lower()}"
if hasattr(context.scene, bookmark_prop_name):
setattr(context.scene, bookmark_prop_name, False)
for area in bpy.context.window.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
self.report({"INFO"}, "UI layout reset to default.")
return {"FINISHED"}
+37 -1
View File
@@ -493,8 +493,9 @@ def get_tab(
("SCHEDULING", "Costing and Scheduling", "", "NLA", 6),
("FM", "Facility Management", "", "PACKAGE", 7),
("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8),
("BOOKMARK", "Bookmark", "", "SOLO_ON", 9),
None,
("BLENDER", "Blender Properties", "", "BLENDER", 9),
("BLENDER", "Blender Properties", "", "BLENDER", 10),
]
return get_tab.enum_items
@@ -529,6 +530,26 @@ class BIMTabProperties(PropertyGroup):
inactive_tab: bool
class BIMTabVisibility(PropertyGroup):
name: StringProperty(name="Tab Name")
is_visible: BoolProperty(name="Is Visible", default=True)
if TYPE_CHECKING:
name: str
is_visible: bool
class BIMPanelProperties(PropertyGroup):
is_visible_in_tab: BoolProperty(name="Is Visible in Tab", default=True)
is_visible_in_bookmarks: BoolProperty(name="Is Visible in Bookmarks", default=True)
is_bookmarked: BoolProperty(name="Is Bookmarked", default=False)
if TYPE_CHECKING:
is_visible_in_tab: bool
is_visible_in_bookmarks: bool
is_bookmarked: bool
class BIMProperties(PropertyGroup):
is_dirty: BoolProperty(name="Is Dirty", default=False)
schema_dir: StringProperty(
@@ -613,6 +634,9 @@ class BIMProperties(PropertyGroup):
name="Time Unit",
default="HOUR",
)
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties")
if TYPE_CHECKING:
is_dirty: bool
schema_dir: str
@@ -627,6 +651,8 @@ class BIMProperties(PropertyGroup):
volume_unit: str
mass_unit: str
time_unit: str
tab_visibilities: bpy.types.bpy_prop_collection[BIMTabVisibility]
panel_properties: bpy.types.bpy_prop_collection[BIMPanelProperties]
class IfcParameter(PropertyGroup):
@@ -748,6 +774,15 @@ class BIMFacet(PropertyGroup):
pset: StringProperty(name="Pset")
value: StringProperty(name="Value")
type: StringProperty(name="Type")
filter_mode: EnumProperty(
name="Filter Mode",
items=[
("ADD", "Add", "Add elements to the result set (query entire IFC file)", "ADD", 0),
("SUBTRACT", "Subtract", "Subtract matching elements from previous results", "REMOVE", 1),
("FILTER", "Filter", "Filter down previous results to matching elements", "FILTER", 2),
],
default="ADD",
)
comparison: EnumProperty(
items=[
("=", "equal to", ""),
@@ -765,6 +800,7 @@ class BIMFacet(PropertyGroup):
pset: str
value: str
type: str
filter_mode: Literal["ADD", "SUBTRACT", "FILTER"]
comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
File diff suppressed because it is too large Load Diff
+17
View File
@@ -487,12 +487,29 @@ def sync_references(
potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing)
for element in potential_reference_elements:
# Skip spatial elements - their IFC placement is the source of truth
if element.is_a("IfcSpatialElement"):
continue
# Skip grids - their IFC placement is the source of truth
if element.is_a("IfcGrid") or element.is_a("IfcGridAxis"):
continue
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
drawing_tool.sync_object_placement(obj)
for element in drawing_tool.get_group_elements(group):
if not drawing_tool.is_auto_annotation(element):
continue
# Skip spatial elements - should never sync their placement
if element.is_a("IfcSpatialElement"):
continue
# Skip grids
if element.is_a("IfcGrid") or element.is_a("IfcGridAxis"):
continue
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
drawing_tool.sync_object_placement(obj)
if not (reference_element := drawing_tool.get_assigned_product(element)):
+6 -2
View File
@@ -28,9 +28,13 @@ if TYPE_CHECKING:
def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None:
if unit.is_scene_unit_metric():
lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix=unit.get_scene_unit_si_prefix("LENGTHUNIT"))
lengthunit = ifc.run(
"unit.add_si_unit", unit_type="LENGTHUNIT", prefix=unit.get_scene_unit_si_prefix("LENGTHUNIT")
)
areaunit = ifc.run("unit.add_si_unit", unit_type="AREAUNIT", prefix=unit.get_scene_unit_si_prefix("AREAUNIT"))
volumeunit = ifc.run("unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=unit.get_scene_unit_si_prefix("VOLUMEUNIT"))
volumeunit = ifc.run(
"unit.add_si_unit", unit_type="VOLUMEUNIT", prefix=unit.get_scene_unit_si_prefix("VOLUMEUNIT")
)
planeangleunit = ifc.run("unit.add_conversion_based_unit", name="degree")
units = [lengthunit, areaunit, volumeunit, planeangleunit]
+44 -5
View File
@@ -2123,7 +2123,15 @@ class Drawing(bonsai.core.tool.Drawing):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
value = '"' + str(value).replace('"', '\\"') + '"'
command = command.replace(variable, value)
text = text.replace(original_command, ifcopenshell.util.selector.format(command[2:-2]))
# Defensive: skip if command[2:-2] is None or 'None'
command_content = command[2:-2]
if command_content is None or str(command_content).strip().lower() == "none":
text = text.replace(original_command, "")
else:
try:
text = text.replace(original_command, ifcopenshell.util.selector.format(command_content))
except Exception:
text = text.replace(original_command, "")
for variable in re.findall("{{.*?}}", text):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
if isinstance(value, (list, tuple)):
@@ -2277,7 +2285,16 @@ class Drawing(bonsai.core.tool.Drawing):
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
include = pset.get("Include", None)
if include:
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
try:
data = json.loads(include)
if isinstance(data, dict) and "filter_structure" in data:
elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
elif isinstance(data, dict) and "query" in data:
elements = ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
else:
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
except (json.JSONDecodeError, ValueError):
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
else:
if ifc_file.schema == "IFC2X3":
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
@@ -2291,7 +2308,7 @@ class Drawing(bonsai.core.tool.Drawing):
if not i.is_a("IfcAnnotation"):
updated_set.add(i)
# add aggregate too, if element is host by one
if decomposes := i.Decomposes:
if hasattr(i, "Decomposes") and (decomposes := i.Decomposes):
aggregate = decomposes[0].RelatingObject
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
if aggregate.is_a("IfcProduct"):
@@ -2304,7 +2321,18 @@ class Drawing(bonsai.core.tool.Drawing):
exclude = pset.get("Exclude", None)
if exclude:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
try:
data = json.loads(exclude)
if isinstance(data, dict) and "filter_structure" in data:
exclude_elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
elements -= exclude_elements
elif isinstance(data, dict) and "query" in data:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
else:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
except (json.JSONDecodeError, ValueError):
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
elements -= set(ifc_file.by_type("IfcOpeningElement"))
return elements
@@ -2318,7 +2346,18 @@ class Drawing(bonsai.core.tool.Drawing):
# NOTE: EPset_Drawing.Include is not used to avoid adding other elements besides spaces
exclude = pset.get("Exclude", None)
if exclude:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
try:
data = json.loads(exclude)
if isinstance(data, dict) and "filter_structure" in data:
exclude_elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
elements -= exclude_elements
elif isinstance(data, dict) and "query" in data:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
else:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
except (json.JSONDecodeError, ValueError):
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
return elements
@classmethod
+9 -8
View File
@@ -91,20 +91,21 @@ class Root(bonsai.core.tool.Root):
elif dest.is_a("IfcTypeProduct"):
if not source.RepresentationMaps:
return copied_entities
# Copy representation maps while preserving mapped representation structures
new_maps = []
for i, rep_map in enumerate(source.RepresentationMaps):
source_rep = rep_map.MappedRepresentation
# Copy the map itself
new_map = ifcopenshell.util.element.copy(tool.Ifc.get(), rep_map)
# Handle the mapped representation - preserve mapping structure if present
if (source_rep.RepresentationType == 'MappedRepresentation' and
len(source_rep.Items) == 1 and
source_rep.Items[0].is_a("IfcMappedItem")):
if (
source_rep.RepresentationType == "MappedRepresentation"
and len(source_rep.Items) == 1
and source_rep.Items[0].is_a("IfcMappedItem")
):
# This is a mapped representation - preserve the structure
new_rep = ifcopenshell.util.element.copy(tool.Ifc.get(), source_rep)
new_rep.Items = [ifcopenshell.util.element.copy(tool.Ifc.get(), item) for item in source_rep.Items]
@@ -118,9 +119,9 @@ class Root(bonsai.core.tool.Root):
exclude_callback=exclude_callback,
copied_entities=copied_entities,
)
new_maps.append(new_map)
dest.RepresentationMaps = new_maps
return copied_entities

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