mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-24 05:48:35 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef055669b4 |
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
|
||||
@@ -9,13 +9,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
path: IfcOpenShell
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ifcopenshell_build
|
||||
|
||||
@@ -35,12 +35,12 @@ jobs:
|
||||
aws --version
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
|
||||
@@ -35,12 +35,12 @@ jobs:
|
||||
aws --version
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ${{ matrix.deps_dir }}
|
||||
|
||||
@@ -19,8 +19,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7 # https://github.com/actions/checkout
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -65,8 +65,8 @@ jobs:
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
python-version: '3.11'
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout bonsai_unstable_repo repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/bonsai_unstable_repo
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -37,8 +37,8 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Compile
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
date: ${{ steps.date.outputs.date }}
|
||||
verdate: ${{ steps.verdate.outputs.verdate }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
name: Docker Build, Tag, Push
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Compile
|
||||
|
||||
@@ -25,8 +25,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -19,8 +19,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -10,9 +10,9 @@ jobs:
|
||||
publish_website:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
- name: Checkout ifctester_org_static_html
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/ifctester_org_static_html
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
- 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
|
||||
- name: Compile
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
cmake --build build-ifcopenshell --target install -j "$(nproc)"
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v7
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
PY
|
||||
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v7
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
|
||||
@@ -12,15 +12,15 @@ jobs:
|
||||
MIN_BLENDER_PY_VERSION: "3.11"
|
||||
steps:
|
||||
- name: Action - checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v7
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.MIN_IOS_PY_VERSION }}
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v7
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.MIN_BLENDER_PY_VERSION }}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout IfcOpenShell
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
echo "name=$(basename $WHEEL)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout wasm-wheels
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/wasm-wheels
|
||||
path: wasm-wheels
|
||||
|
||||
@@ -43,12 +43,12 @@ jobs:
|
||||
CLICOLOR_FORCE: "1"
|
||||
CMAKE_COLOR_DIAGNOSTICS: "ON"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
@@ -257,15 +257,13 @@ jobs:
|
||||
cd ../ifcdiff && make test || ERROR=1
|
||||
cd ../ifcpatch && make test || ERROR=1
|
||||
pip install -e ../ifc5d --no-deps
|
||||
pip install odfpy openpyxl
|
||||
pip install odfpy xlsxwriter
|
||||
cd ../ifc5d && make test || ERROR=1
|
||||
pip install -e ../ifcquery --no-deps
|
||||
cd ../ifcquery && make test || ERROR=1
|
||||
pip install -e ../ifcedit --no-deps
|
||||
cd ../ifcedit && make test || ERROR=1
|
||||
# Pinned <2: mcp 2.0.0 renamed mcp.server.fastmcp.FastMCP to
|
||||
# mcp.server.mcpserver.MCPServer, which ifcmcp doesn't support yet.
|
||||
pip install "mcp>=1.0,<2"
|
||||
pip install mcp
|
||||
pip install -e ../ifcmcp --no-deps
|
||||
cd ../ifcmcp && make test || ERROR=1
|
||||
pip install -e ../ifctester --no-deps
|
||||
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (recursive)
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Checkout intermediate Pages repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/aichat_ifcopenshell_org_static_html
|
||||
ref: gh-pages
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: |
|
||||
rsync -av --delete --exclude='.git/' src/ifcchat/ output/
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v7
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Download wheels
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (recursive)
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Checkout intermediate Pages repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: IfcOpenShell/wasm_ifcopenshell_org_static_html
|
||||
ref: gh-pages
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install C++ dependencies
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>blenderbim-nightly</id>
|
||||
<version>blenderbim_build_version</version>
|
||||
<version>blenderbim_build_version-alpha</version>
|
||||
<packageSourceUrl>https://github.com/IfcOpenShell/IfcOpenShell</packageSourceUrl>
|
||||
<owners>fbpyr</owners>
|
||||
<!-- == SOFTWARE SPECIFIC SECTION == -->
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
apt update && apt install git wget curl ptpython mono-devel micro
|
||||
mkdir -p /home/runner/work/IfcOpenShell && cd /home/runner/work/IfcOpenShell
|
||||
git clone https://github.com/IfcOpenShell/IfcOpenShell
|
||||
cd /home/runner/work/IfcOpenShell/IfcOpenShell/choco/bonsai/
|
||||
cd /home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/
|
||||
micro choco_release.py # paste this script, comment out push command
|
||||
export CHOCO_TOKEN="secret_choco_release_token"
|
||||
python3 choco_release.py
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
@@ -29,7 +28,7 @@ def get_repo_tag_names() -> list[str]:
|
||||
|
||||
|
||||
def request_repo_info(url: str):
|
||||
req = request.Request(url)
|
||||
req = request.Request(url)
|
||||
resp = request.urlopen(req)
|
||||
if not resp.status == 200:
|
||||
print(f"[ERROR] could not contact server: {url}")
|
||||
@@ -86,15 +85,13 @@ def run(command: str) -> None:
|
||||
|
||||
start = datetime.datetime.now()
|
||||
|
||||
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
|
||||
URL_BLENDER_CMAKE = (
|
||||
"https://raw.githubusercontent.com/blender/blender/{}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
|
||||
)
|
||||
RE_BLENDER_VERSION_MIN_MAJ = r"Latest Version.+<span>Blender (\d+\.\d+)\..+</span>"
|
||||
RE_BLENDER_VERSION_MIN_MAJ_PAT = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
|
||||
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
|
||||
URL_BLENDER_CMAKE = "https://raw.githubusercontent.com/blender/blender/{}/build_files/cmake/Modules/FindPythonLibsUnix.cmake"
|
||||
RE_BLENDER_VERSION_MIN_MAJ = r"Latest Version.+<span>Blender (\d+\.\d+)\..+</span>"
|
||||
RE_BLENDER_VERSION_MIN_MAJ_PAT = r"Latest Version.+<span>Blender (\d+\.\d+\.\d+)</span>"
|
||||
RE_BLENDER_PYTHON_VERSION_MAJ_MIN = r"\(_PYTHON_VERSION_SUPPORTED (\d+\.\d+)\)"
|
||||
|
||||
BLENDERBIM_DIR = pathlib.Path("/home/runner/work/IfcOpenShell/IfcOpenShell/choco/bonsai/")
|
||||
BLENDERBIM_DIR = pathlib.Path("/home/runner/work/IfcOpenShell/IfcOpenShell/choco/blenderbim/")
|
||||
|
||||
print("_____ check choco release needed?")
|
||||
|
||||
@@ -151,7 +148,7 @@ print(f"{blender_python_version_maj_min=}")
|
||||
python_version = f"py{found[0].replace('.', '')}"
|
||||
print(f"{python_version=}")
|
||||
|
||||
blenderbim_build_version = target_release_tag.replace("bonsai-", "")
|
||||
blenderbim_build_version = target_release_tag.replace("blenderbim-", "")
|
||||
|
||||
# url_blenderbim_py3x_win_zip
|
||||
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
|
||||
@@ -169,15 +166,15 @@ topics = {
|
||||
"path": HERE_DIR / "blenderbim.nuspec",
|
||||
"key_values": {
|
||||
"latest_blender_version_maj_min_pat": latest_blender_release_maj_min_pat,
|
||||
"blenderbim_build_version": blenderbim_build_version,
|
||||
"blenderbim_build_version" : blenderbim_build_version,
|
||||
},
|
||||
},
|
||||
"install": {
|
||||
"path": HERE_DIR / "tools" / "chocolateyinstall.ps1",
|
||||
"key_values": {
|
||||
"url_blenderbim_py3x_win_zip": url_blenderbim_py3x_win_zip,
|
||||
"url_blenderbim_py3x_win_zip" : url_blenderbim_py3x_win_zip,
|
||||
"sha256sum_blenderbim_py3x_win_zip": sha256sum_blenderbim_py3x_win_zip,
|
||||
"latest_blender_version_maj_min": blender_version_min_maj,
|
||||
"latest_blender_version_maj_min" : blender_version_min_maj,
|
||||
},
|
||||
},
|
||||
"uninstall": {
|
||||
|
||||
+2
-5
@@ -121,7 +121,7 @@ import tarfile
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
ssl._create_default_https_context = ssl._create_unverified_context # ty:ignore[invalid-assignment]
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
import time
|
||||
from collections.abc import Generator, Sequence
|
||||
@@ -697,10 +697,7 @@ def build_dependency(
|
||||
compr = "xz"
|
||||
else:
|
||||
raise RuntimeError("fix source for new download type")
|
||||
# ty: false positive bug upstream.
|
||||
download_tarfile = tarfile.open(
|
||||
name=download_tarfile_path, mode=f"r:{compr}"
|
||||
) # ty:ignore[no-matching-overload]
|
||||
download_tarfile = tarfile.open(name=download_tarfile_path, mode=f"r:{compr}")
|
||||
# tarfile seriously doesn't have a function to retrieve the root directory more easily
|
||||
extract_dir_name = os.path.commonprefix([x for x in download_tarfile.getnames() if x != "."])
|
||||
# run([tar, "--exclude=\"*/*\"", "-tf", download_name], cwd=build_dir).strip() no longer works
|
||||
|
||||
+1
-2
@@ -156,8 +156,7 @@ exclude = [
|
||||
[tool.poe.tasks]
|
||||
|
||||
dev-setup.sequence = [
|
||||
# 3.13 is chosen because it's the version used in the latest Bonsai.
|
||||
{cmd = "uv sync --python 3.13"},
|
||||
{cmd = "uv sync"},
|
||||
{cmd = "uv pip install -e ./src/bsdd/"},
|
||||
{cmd = "uv pip install -e ./src/ifcopenshell-python/[advanced,dev]"},
|
||||
{cmd = "uv pip install -e ./src/ifcedit/"},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
black==26.3.1
|
||||
ruff==0.16.0
|
||||
ruff==0.15.12
|
||||
poethepoet
|
||||
ty==0.0.63
|
||||
gersemi==0.28.0
|
||||
ty==0.0.61
|
||||
gersemi==0.26.1
|
||||
|
||||
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_D
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2,#31,#32,#33,#34,#35,#36,#37));
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2,#31,#32,#33,#34,#35,#36));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -41,6 +41,5 @@ DATA;
|
||||
#34=IFCSIMPLEPROPERTYTEMPLATE('2epSGfC4bFM9gb1X7zBIp4',$,'RenderSharp','Whether to render ''sharp'' (convex) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#35=IFCSIMPLEPROPERTYTEMPLATE('3TZwsEjkr5WRDKcgrYzSIA',$,'RidgeAngleMinDegrees','Minimum convex dihedral deviation from flat, in degrees, for a projection edge to be classified as ''sharp'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#36=IFCSIMPLEPROPERTYTEMPLATE('2Jua$lO754vgZOkBoHM2gA',$,'RenderFlush','Whether to render ''flush'' edges (dihedral deviation below both ridge/valley thresholds). Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#37=IFCSIMPLEPROPERTYTEMPLATE('1zM9sia2L8RQDnWZxgUwlZ',$,'JoinClasses','Comma separated list of IFC classes whose cut linework will be joined together when they meet (e.g. mitred at a corner).\X2\000A\X0\Defaults to ''IfcWall,IfcSlab'' if not set. Override to also join other classes, such as ''IfcWall,IfcSlab,IfcCovering''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -10,7 +10,6 @@ if bonsai_lib_path:
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pystache
|
||||
@@ -19,53 +18,8 @@ from aiohttp import web
|
||||
|
||||
sio_port = 8080 # default port
|
||||
|
||||
|
||||
def get_asset_version() -> str:
|
||||
"""A cache-busting token appended to locally served static asset URLs.
|
||||
|
||||
Browsers otherwise keep serving a stale cached copy of static/js and
|
||||
static/css after Bonsai ships a code change, until the user does a hard
|
||||
refresh. Using the Bonsai version (which includes the build's commit
|
||||
hash) means the token changes on every shipped update.
|
||||
"""
|
||||
if bonsai_version:
|
||||
return urllib.parse.quote(bonsai_version, safe="")
|
||||
# Fallback for standalone runs without BONSAI_VERSION set (e.g. running
|
||||
# sioserver.py directly outside of Blender): derive a token from the
|
||||
# newest mtime among the static assets, so it still changes whenever the
|
||||
# shipped files change.
|
||||
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
||||
latest_mtime = 0
|
||||
for root, _dirs, files in os.walk(static_dir):
|
||||
for name in files:
|
||||
latest_mtime = max(latest_mtime, int(os.path.getmtime(os.path.join(root, name))))
|
||||
return f"dev-{latest_mtime}"
|
||||
|
||||
|
||||
asset_version = get_asset_version()
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def no_cache_static_middleware(request: web.Request, handler):
|
||||
"""Force revalidation of locally served static assets.
|
||||
|
||||
Query-string version stamping (see `asset_version`) busts the cache for
|
||||
the HTML-referenced entry points, but JS files that statically import
|
||||
other local modules (e.g. cost.js/gantt.js importing utilities/costui.js)
|
||||
reference those modules by an un-stamped relative path. Marking all
|
||||
/static/ and /jsgantt/ responses as no-cache makes browsers always
|
||||
revalidate with the server (a cheap conditional GET / 304 when nothing
|
||||
changed), so nested imports also pick up shipped changes without
|
||||
requiring a hard refresh.
|
||||
"""
|
||||
response = await handler(request)
|
||||
if request.path.startswith("/static/") or request.path.startswith("/jsgantt/"):
|
||||
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||
return response
|
||||
|
||||
|
||||
sio = socketio.AsyncServer(cors_allowed_origins="*", async_mode="aiohttp", max_http_buffer_size=10000000)
|
||||
app = web.Application(middlewares=[no_cache_static_middleware])
|
||||
app = web.Application()
|
||||
sio.attach(app)
|
||||
|
||||
|
||||
@@ -245,28 +199,28 @@ class BlenderNamespace(socketio.AsyncNamespace):
|
||||
async def schedules(request):
|
||||
with open("templates/index.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
async def costing(request):
|
||||
with open("templates/costing.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
async def sequencing(request):
|
||||
with open("templates/gantt.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
async def documentation(request):
|
||||
with open("templates/drawings.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
@@ -275,7 +229,7 @@ async def documentation(request):
|
||||
async def demo(request):
|
||||
with open("templates/demo.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ function addGanttElement(blenderId, tasks, workSched, filename) {
|
||||
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ["Hour", "Day", "Week", "Month", "Quarter"], // vUseSingleCell keeps Hour usable on large charts.
|
||||
vFormatArr: ["Day", "Week", "Month", "Quarter"], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vShowRes: true, // Disable the resource column.
|
||||
vShowComp: false, // Disable the completion column.
|
||||
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
|
||||
|
||||
@@ -250,14 +250,6 @@ export class CostUI {
|
||||
},
|
||||
});
|
||||
|
||||
CostUI.addRibbonButton({
|
||||
text: "Download CSV",
|
||||
icon: "fa-solid fa-file-csv",
|
||||
callback: () => {
|
||||
CostUI.downloadCsv();
|
||||
},
|
||||
});
|
||||
|
||||
CostUI.addRibbonButton({
|
||||
text: "Hide Schedules",
|
||||
icon: "fa-regular fa-eye-slash",
|
||||
@@ -531,81 +523,6 @@ export class CostUI {
|
||||
}
|
||||
}
|
||||
|
||||
static downloadCsv() {
|
||||
const tables = document.querySelectorAll("table[id^='cost-items-']");
|
||||
if (tables.length === 0) {
|
||||
alert("No cost schedule loaded to export!");
|
||||
return;
|
||||
}
|
||||
tables.forEach((table) => {
|
||||
const scheduleId = table.id.split("-").pop();
|
||||
const csv = CostUI.tableToCsv(table);
|
||||
if (csv === null) {
|
||||
return;
|
||||
}
|
||||
const nameEl = document.querySelector(
|
||||
"#cost-schedule-container-" + scheduleId + " .form-header span"
|
||||
);
|
||||
const scheduleName = nameEl
|
||||
? nameEl.textContent
|
||||
: "cost_schedule_" + scheduleId;
|
||||
CostUI.triggerCsvDownload(csv, scheduleName + ".csv");
|
||||
});
|
||||
}
|
||||
|
||||
static tableToCsv(table) {
|
||||
const escapeCsvCell = (value) => {
|
||||
const text = (value === null || value === undefined ? "" : value)
|
||||
.toString()
|
||||
.trim();
|
||||
if (/[",\n]/.test(text)) {
|
||||
return '"' + text.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const cellText = (cell) => {
|
||||
const input = cell.querySelector("input");
|
||||
return input ? input.value : cell.innerText;
|
||||
};
|
||||
|
||||
// The Actions column only holds buttons (edit/delete/etc), not data.
|
||||
const isDataColumn = (column) => column && column !== "Actions";
|
||||
|
||||
const headerCells = Array.from(table.querySelectorAll("thead th")).filter(
|
||||
(th) => isDataColumn(th.getAttribute("data-column"))
|
||||
);
|
||||
if (headerCells.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rows = [headerCells.map((th) => escapeCsvCell(th.textContent)).join(",")];
|
||||
|
||||
table.querySelectorAll("tbody tr").forEach((row) => {
|
||||
const cells = Array.from(row.children).filter((cell) =>
|
||||
isDataColumn(cell.getAttribute("data-column"))
|
||||
);
|
||||
if (cells.length === 0) {
|
||||
return; // e.g. the "No cost items found" placeholder row.
|
||||
}
|
||||
rows.push(cells.map((cell) => escapeCsvCell(cellText(cell))).join(","));
|
||||
});
|
||||
|
||||
return rows.join("\n");
|
||||
}
|
||||
|
||||
static triggerCsvDownload(csvContent, filename) {
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
static createCostTable({ costSchedule, currency, callbacks }) {
|
||||
const preferences = CostUI.getColumnPreferences();
|
||||
const isScheduleOfRates = costSchedule.PredefinedType === "SCHEDULEOFRATES";
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/static/css/cost.css?v={{v}}"
|
||||
href="/static/css/cost.css"
|
||||
id="index-stylesheet"
|
||||
/>
|
||||
<link
|
||||
@@ -21,7 +21,7 @@
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
src="/static/js/jquery.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
@@ -34,7 +34,7 @@
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script type="module" defer src="./static/js/cost.js?v={{v}}"></script>
|
||||
<script type="module" defer src="./static/js/cost.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
/>
|
||||
<!-- here we request the CSS file from the server, -->
|
||||
<!-- using registered static path in the server -->
|
||||
<link rel="stylesheet" href="/static/css/demo.css?v={{v}}" id="demo-stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
src="/static/js/jquery.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
@@ -33,7 +33,7 @@
|
||||
</script>
|
||||
<!-- here we request the JS file from the server-->
|
||||
<!-- using registered static path in the server -->
|
||||
<script defer src="./static/js/demo.js?v={{v}}"></script>
|
||||
<script defer src="./static/js/demo.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- the navigation bar at the top of the page. -->
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/css/drawings.css?v={{v}}"
|
||||
href="/static/css/drawings.css"
|
||||
id="drawings-stylesheet"
|
||||
/>
|
||||
<link
|
||||
@@ -24,7 +24,7 @@
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
src="/static/js/jquery.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
@@ -45,7 +45,7 @@
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script defer src="./static/js/drawings.js?v={{v}}"></script>
|
||||
<script defer src="./static/js/drawings.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
|
||||
@@ -9,25 +9,25 @@
|
||||
type="image/x-icon"
|
||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||
/>
|
||||
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css?v={{v}}" />
|
||||
<link rel="stylesheet" href="/static/css/gantt.css?v={{v}}" id="gantt-stylesheet" />
|
||||
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
|
||||
<link rel="stylesheet" href="/static/css/gantt.css" id="gantt-stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
src="/static/js/jquery.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
|
||||
></script>
|
||||
<script type="text/javascript" src="./jsgantt/jsgantt.js?v={{v}}"></script>
|
||||
<script type="text/javascript" src="./jsgantt/jsgantt.js"></script>
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script type="module" defer src="./static/js/gantt.js?v={{v}}"></script>
|
||||
<script type="module" defer src="./static/js/gantt.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="no-print">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
type="image/x-icon"
|
||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||
/>
|
||||
<link rel="stylesheet" href="/static/css/index.css?v={{v}}" id="index-stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/index.css" id="index-stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
id="tabulator-stylesheet"
|
||||
@@ -21,7 +21,7 @@
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
src="/static/js/jquery.min.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
@@ -34,7 +34,7 @@
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script defer src="./static/js/index.js?v={{v}}"></script>
|
||||
<script defer src="./static/js/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
|
||||
@@ -418,10 +418,9 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
|
||||
|
||||
|
||||
def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
|
||||
shading_type = area.spaces.active.shading.type
|
||||
tool.Style.restore_material_style_types(shading_type)
|
||||
if shading_type == "SOLID":
|
||||
area.spaces.active.shading.color_type = "MATERIAL"
|
||||
shading = area.spaces.active.shading.type
|
||||
if shading == "RENDERED":
|
||||
tool.Style.get_style_props().active_style_type = "External"
|
||||
|
||||
|
||||
def subscribe_to_viewport_shading_changes():
|
||||
|
||||
@@ -236,8 +236,11 @@ def import_attribute(
|
||||
elif data_type == "integer":
|
||||
new.int_value = 0 if new.is_null else int(data[attribute.name()])
|
||||
elif data_type == "float":
|
||||
measure_class = attribute.type_of_attribute().declared_type().name()
|
||||
new.special_type = tool.Pset.get_special_type_for_measure_class(measure_class)
|
||||
attribute_type = attribute.type_of_attribute()
|
||||
if attribute_type._is("IfcLengthMeasure"):
|
||||
new.special_type = "LENGTH"
|
||||
elif attribute_type._is("IfcForceMeasure"):
|
||||
new.special_type = "FORCE"
|
||||
new.float_value = 0.0 if new.is_null else float(data[attribute.name()])
|
||||
elif data_type == "enum":
|
||||
attribute_type = attribute.type_of_attribute()
|
||||
|
||||
@@ -133,6 +133,10 @@ class MaterialCreator:
|
||||
if shape_has_openings and coords.is_a("IfcIndexedTextureMap"):
|
||||
continue
|
||||
tool.Loader.load_indexed_map(coords, self.mesh)
|
||||
elif tool.Style.get_texture_style(material):
|
||||
# No explicit coordinate mapping (e.g. IFC2X3 has no IsMappedBy,
|
||||
# and IFC4 COORD uses generated UVs). Bake XY→UV as fallback.
|
||||
tool.Loader.load_generated_uv_map(self.mesh)
|
||||
|
||||
def assign_material_slots_to_faces(self) -> None:
|
||||
if not self.mesh["ios_materials"]:
|
||||
|
||||
@@ -47,7 +47,6 @@ classes = (
|
||||
operator.CleanWireframes,
|
||||
operator.ContractSheet,
|
||||
operator.ConvertSVGToDXF,
|
||||
operator.CopyAnnotationToDrawing,
|
||||
operator.CopyTextToSelection,
|
||||
operator.CreateDrawing,
|
||||
operator.CreateSheets,
|
||||
@@ -108,7 +107,6 @@ classes = (
|
||||
operator.SelectAssignedProduct,
|
||||
operator.SelectSimilarTextLiteralValue,
|
||||
operator.ToggleTargetView,
|
||||
operator.ToggleDrawingCategorySelection,
|
||||
operator.OpenDocumentationWebUi,
|
||||
operator.FilterSelectedObjectsIfIntersectedByCamera,
|
||||
prop.Variable,
|
||||
|
||||
@@ -228,90 +228,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
|
||||
|
||||
def get_copy_annotation_target_drawings(self, context):
|
||||
global COPY_ANNOTATION_TARGET_DRAWINGS_ENUM
|
||||
drawings = [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]
|
||||
drawings.sort(key=lambda d: d.Name or "")
|
||||
COPY_ANNOTATION_TARGET_DRAWINGS_ENUM = [(str(d.id()), d.Name or "Unnamed", "") for d in drawings]
|
||||
return COPY_ANNOTATION_TARGET_DRAWINGS_ENUM
|
||||
|
||||
|
||||
COPY_ANNOTATION_TARGET_DRAWINGS_ENUM = []
|
||||
|
||||
|
||||
class CopyAnnotationToDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.copy_annotation_to_drawing"
|
||||
bl_label = "Copy Annotation To Drawing"
|
||||
bl_description = (
|
||||
"Copy the selected annotations to another drawing.\n\n"
|
||||
"The copies become independent annotations assigned to the chosen drawing, "
|
||||
"placed in its view plane. The originals stay in their current drawing"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
target_drawing: bpy.props.EnumProperty(name="Target Drawing", items=get_copy_annotation_target_drawings)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
target_drawing: str
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not tool.Ifc.get():
|
||||
cls.poll_message_set("No IFC project loaded.")
|
||||
return False
|
||||
if not cls.get_selected_annotations(context):
|
||||
cls.poll_message_set("No annotation selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_selected_annotations(cls, context) -> list[ifcopenshell.entity_instance]:
|
||||
return [
|
||||
element
|
||||
for obj in context.selected_objects
|
||||
if (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcAnnotation")
|
||||
and element.ObjectType != "DRAWING"
|
||||
]
|
||||
|
||||
def invoke(self, context, event):
|
||||
assert context.window_manager
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
row = self.layout.row()
|
||||
row.prop(self, "target_drawing")
|
||||
|
||||
def _execute(self, context):
|
||||
if not self.target_drawing:
|
||||
self.report({"ERROR"}, "No target drawing selected.")
|
||||
return {"CANCELLED"}
|
||||
target_drawing = tool.Ifc.get().by_id(int(self.target_drawing))
|
||||
annotations = self.get_selected_annotations(context)
|
||||
previous_selection = [obj for a in annotations if (obj := tool.Ifc.get_object(a))]
|
||||
previous_active = context.view_layer.objects.active
|
||||
copied = core.copy_annotations_to_drawing(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Drawing,
|
||||
tool.Geometry,
|
||||
annotations=annotations,
|
||||
target_drawing=target_drawing,
|
||||
)
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
for obj in previous_selection:
|
||||
if obj.name in context.view_layer.objects:
|
||||
obj.select_set(True)
|
||||
if previous_active and previous_active.name in context.view_layer.objects:
|
||||
context.view_layer.objects.active = previous_active
|
||||
skipped = len(annotations) - len(copied)
|
||||
message = f"Copied {len(copied)} annotations to {target_drawing.Name or 'Unnamed'}."
|
||||
if skipped:
|
||||
message += f" Skipped {skipped} already in that drawing."
|
||||
self.report({"INFO"}, message)
|
||||
|
||||
|
||||
class CreateDrawing(bpy.types.Operator):
|
||||
"""Creates/refreshes a .svg drawing
|
||||
|
||||
@@ -1555,15 +1471,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
"Material.Name",
|
||||
]
|
||||
|
||||
join_classes = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinClasses")
|
||||
if join_classes:
|
||||
join_classes = tuple(c.strip() for c in join_classes.split(",") if c.strip())
|
||||
else:
|
||||
# Architectural convention only merges these objects by default. E.g. pipe
|
||||
# segments and fittings shouldn't merge. Users may override this per-drawing
|
||||
# via the EPset_Drawing.JoinClasses property (e.g. to also join IfcCovering).
|
||||
join_classes = ("IfcWall", "IfcSlab")
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
joined_paths = {}
|
||||
self.is_manifold_cache = {}
|
||||
@@ -1665,7 +1572,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
)
|
||||
path.attrib["d"] = d
|
||||
|
||||
if not any(element.is_a(c) for c in join_classes):
|
||||
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
|
||||
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
|
||||
continue
|
||||
|
||||
keys = []
|
||||
@@ -2310,11 +2218,7 @@ class SelectAllDrawings(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
# When filtering to sheeted drawings only, act on the visible drawings only.
|
||||
sheeted_ids = tool.Drawing.get_sheeted_drawing_ids() if props.show_drawings_on_sheets_only else None
|
||||
for drawing in props.drawings:
|
||||
if sheeted_ids is not None and drawing.is_drawing and drawing.ifc_definition_id not in sheeted_ids:
|
||||
continue
|
||||
if drawing.is_selected != self.select_all:
|
||||
drawing.is_selected = self.select_all
|
||||
return {"FINISHED"}
|
||||
@@ -3874,26 +3778,6 @@ class ToggleTargetView(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleDrawingCategorySelection(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_drawing_category_selection"
|
||||
bl_label = "Toggle Category Selection"
|
||||
bl_description = "Select or deselect all drawings in this view category"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
target_view: bpy.props.StringProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
target_view: str
|
||||
|
||||
def execute(self, context):
|
||||
drawings = tool.Drawing.get_visible_drawings_in_category(self.target_view)
|
||||
# If everything visible in the category is already selected, deselect all; otherwise select all.
|
||||
new_state = not all(d.is_selected for d in drawings)
|
||||
for drawing in drawings:
|
||||
drawing.is_selected = new_state
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExpandSheet(bpy.types.Operator):
|
||||
bl_idname = "bim.expand_sheet"
|
||||
bl_label = "Expand Sheet"
|
||||
|
||||
@@ -409,12 +409,6 @@ class DocProperties(PropertyGroup):
|
||||
options=set(),
|
||||
)
|
||||
is_editing_drawings: BoolProperty(name="Is Editing Drawings", default=False)
|
||||
show_drawings_on_sheets_only: BoolProperty(
|
||||
name="Show Only Drawings on Sheets",
|
||||
description="Only show drawings that are placed on a sheet",
|
||||
default=False,
|
||||
options=set(),
|
||||
)
|
||||
is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
|
||||
is_editing_references: BoolProperty(name="Is Editing References", default=False)
|
||||
target_view: EnumProperty(
|
||||
@@ -445,7 +439,6 @@ class DocProperties(PropertyGroup):
|
||||
should_use_annotation_cache: bool
|
||||
should_draw_linked_projects: bool
|
||||
is_editing_drawings: bool
|
||||
show_drawings_on_sheets_only: bool
|
||||
is_editing_schedules: bool
|
||||
is_editing_references: bool
|
||||
target_view: Literal["PLAN_VIEW", "ELEVATION_VIEW", "SECTION_VIEW", "REFLECTED_PLAN_VIEW", "MODEL_VIEW"]
|
||||
|
||||
@@ -332,8 +332,6 @@ class BIM_PT_drawings(Panel):
|
||||
|
||||
row3.separator(factor=0.5, type="SPACE")
|
||||
|
||||
row3.operator("bim.copy_annotation_to_drawing", icon="PASTEDOWN", text="")
|
||||
|
||||
row3.operator("bim.select_all_drawings", icon="CHECKBOX_HLT", text="")
|
||||
row3.operator("bim.create_drawing", text="", icon="OUTPUT")
|
||||
row3.operator("bim.convert_svg_to_dxf", text="", icon="SEQ_PREVIEW").view = active_drawing.name
|
||||
@@ -341,7 +339,6 @@ class BIM_PT_drawings(Panel):
|
||||
self.layout.template_list(
|
||||
"BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index"
|
||||
)
|
||||
self.layout.prop(self.props, "show_drawings_on_sheets_only")
|
||||
|
||||
|
||||
class BIM_PT_schedules(Panel):
|
||||
@@ -874,8 +871,8 @@ class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
layout.label(text="", translate=False)
|
||||
return
|
||||
|
||||
row = layout.row(align=True)
|
||||
if item.is_drawing:
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="BLANK1")
|
||||
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
|
||||
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
|
||||
@@ -896,9 +893,6 @@ class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
item.ifc_definition_id
|
||||
)
|
||||
else:
|
||||
# Give category headers a distinct inset background so they stand out from drawing rows.
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
if item.target_view == "PLAN_VIEW":
|
||||
icon = "UV_FACESEL"
|
||||
elif item.target_view == "ELEVATION_VIEW":
|
||||
@@ -919,55 +913,7 @@ class BIM_UL_drawinglist(bpy.types.UIList):
|
||||
op = row.operator("bim.toggle_target_view", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT")
|
||||
op.target_view = item.target_view
|
||||
op.option = "EXPAND"
|
||||
group = tool.Drawing.get_visible_drawings_in_category(item.target_view)
|
||||
all_selected = bool(group) and all(d.is_selected for d in group)
|
||||
row.operator(
|
||||
"bim.toggle_drawing_category_selection",
|
||||
text="",
|
||||
icon="CHECKBOX_HLT" if all_selected else "CHECKBOX_DEHLT",
|
||||
emboss=False,
|
||||
).target_view = item.target_view
|
||||
row.separator(factor=0.5, type="SPACE")
|
||||
# Clicking the header name toggles expand/contract, same as the disclosure triangle.
|
||||
op = row.operator("bim.toggle_target_view", text=item.name, icon=icon, emboss=False)
|
||||
op.target_view = item.target_view
|
||||
op.option = "CONTRACT" if item.is_expanded else "EXPAND"
|
||||
|
||||
def filter_items(self, context, data: DocProperties, propname: str):
|
||||
drawings = getattr(data, propname)
|
||||
helper_funcs = bpy.types.UI_UL_list
|
||||
|
||||
flt_flags = []
|
||||
flt_neworder = []
|
||||
|
||||
if self.filter_name:
|
||||
flt_flags = helper_funcs.filter_items_by_name(
|
||||
self.filter_name,
|
||||
self.bitflag_filter_item,
|
||||
drawings,
|
||||
"name",
|
||||
reverse=self.use_filter_sort_reverse,
|
||||
)
|
||||
if not flt_flags:
|
||||
flt_flags = [self.bitflag_filter_item] * len(drawings)
|
||||
|
||||
props = tool.Drawing.get_document_props()
|
||||
if props.show_drawings_on_sheets_only:
|
||||
ifc_file = tool.Ifc.get()
|
||||
sheeted_ids = tool.Drawing.get_sheeted_drawing_ids()
|
||||
# Target view headers are only shown if they contain a sheeted drawing.
|
||||
sheeted_target_views = {
|
||||
tool.Drawing.get_drawing_target_view(ifc_file.by_id(drawing_id)) for drawing_id in sheeted_ids
|
||||
}
|
||||
for i, item in enumerate(drawings):
|
||||
if item.is_drawing:
|
||||
is_visible = item.ifc_definition_id in sheeted_ids
|
||||
else:
|
||||
is_visible = item.target_view in sheeted_target_views
|
||||
if not is_visible:
|
||||
flt_flags[i] &= ~self.bitflag_filter_item
|
||||
|
||||
return flt_flags, flt_neworder
|
||||
row.prop(item, "name", text="", icon=icon, emboss=False)
|
||||
|
||||
|
||||
class BIM_UL_sheets(bpy.types.UIList):
|
||||
|
||||
@@ -225,9 +225,6 @@ class AnnotationToolUI:
|
||||
def draw_edit_object_interface(cls, context):
|
||||
if DecoratorData.get_text_data(bpy.context.active_object):
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
||||
if bpy.ops.bim.copy_annotation_to_drawing.poll():
|
||||
row = cls.layout.row(align=True)
|
||||
row.operator("bim.copy_annotation_to_drawing", icon="PASTEDOWN", text="Copy To Drawing")
|
||||
|
||||
@classmethod
|
||||
def draw_type_selection_interface(cls):
|
||||
|
||||
@@ -163,52 +163,14 @@ class AssignGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def _execute(self, context):
|
||||
if not self.is_assigning:
|
||||
return bpy.ops.bim.unassign_group(group=self.group)
|
||||
ifc_file = tool.Ifc.get()
|
||||
group = ifc_file.by_id(self.group)
|
||||
products = [
|
||||
element
|
||||
for o in tool.Blender.get_selected_objects(include_active=False)
|
||||
if (element := tool.Ifc.get_entity(o))
|
||||
]
|
||||
relocated_annotations = self.unassign_from_previous_drawing(ifc_file, group, products)
|
||||
ifcopenshell.api.group.assign_group(ifc_file, products=products, group=group)
|
||||
self.relocate_annotations_to_drawing(relocated_annotations, group)
|
||||
ifcopenshell.api.group.assign_group(tool.Ifc.get(), products=products, group=tool.Ifc.get().by_id(self.group))
|
||||
self.report({"INFO"}, f"Assigned {len(products)} objects to group.")
|
||||
|
||||
def unassign_from_previous_drawing(self, ifc_file, group, products) -> list[ifcopenshell.entity_instance]:
|
||||
"""Assigning an annotation to a group that represents a drawing means the
|
||||
annotation should belong to that drawing only, so it needs to leave
|
||||
whichever drawing it was previously part of, instead of ending up
|
||||
visible in both at once.
|
||||
"""
|
||||
new_drawing = tool.Drawing.get_group_drawing(group)
|
||||
if not new_drawing:
|
||||
return []
|
||||
relocated = []
|
||||
for product in products:
|
||||
if not product.is_a("IfcAnnotation") or product.ObjectType == "DRAWING":
|
||||
continue
|
||||
old_drawing = tool.Drawing.get_annotation_drawing(product)
|
||||
if not old_drawing or old_drawing.id() == new_drawing.id():
|
||||
continue
|
||||
if old_group := tool.Drawing.get_drawing_group(old_drawing):
|
||||
ifcopenshell.api.group.unassign_group(ifc_file, products=[product], group=old_group)
|
||||
relocated.append(product)
|
||||
return relocated
|
||||
|
||||
def relocate_annotations_to_drawing(self, products, group) -> None:
|
||||
"""Move the relocated annotations into the new drawing's collection and
|
||||
depth, now that they have actually been assigned to its group.
|
||||
"""
|
||||
if not products:
|
||||
return
|
||||
new_drawing = tool.Drawing.get_group_drawing(group)
|
||||
new_camera = tool.Ifc.get_object(new_drawing) or tool.Drawing.import_drawing(new_drawing)
|
||||
for product in products:
|
||||
if obj := tool.Ifc.get_object(product):
|
||||
tool.Drawing.ensure_annotation_in_drawing_plane(obj, camera=new_camera)
|
||||
tool.Collector.assign(obj)
|
||||
|
||||
|
||||
class UnassignGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.unassign_group"
|
||||
|
||||
@@ -56,7 +56,6 @@ classes = (
|
||||
operator.RemoveMaterial,
|
||||
operator.RemoveMaterialSet,
|
||||
operator.RemoveProfile,
|
||||
operator.RenameMaterial,
|
||||
operator.ReorderMaterialSetItem,
|
||||
operator.SelectByMaterial,
|
||||
operator.SelectMaterialInMaterialsUI,
|
||||
|
||||
@@ -102,26 +102,6 @@ class EditMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.edit_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material))
|
||||
|
||||
|
||||
class RenameMaterial(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.rename_material"
|
||||
bl_label = "Rename Material"
|
||||
bl_description = "Rename an IfcMaterial"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material: bpy.props.IntProperty()
|
||||
name: bpy.props.StringProperty(name="Name")
|
||||
|
||||
def invoke(self, context, event):
|
||||
material = tool.Ifc.get().by_id(self.material)
|
||||
self.name = material.Name or ""
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.prop(self, "name")
|
||||
|
||||
def _execute(self, context):
|
||||
core.rename_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material), name=self.name)
|
||||
|
||||
|
||||
class DisableEditingMaterial(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_material"
|
||||
bl_label = "Disable Editing Material"
|
||||
|
||||
@@ -93,30 +93,6 @@ def _stroke_lines_alpha(
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
def _connected_components(
|
||||
vertex_groups: dict[int, list[bmesh.types.BMVert]],
|
||||
) -> list[list[bmesh.types.BMVert]]:
|
||||
"""Split each vertex group's members into their connected components,
|
||||
since a duplicated arc/circle loop shares its source loop's group index."""
|
||||
components = []
|
||||
for verts in vertex_groups.values():
|
||||
remaining = set(verts)
|
||||
while remaining:
|
||||
seed = remaining.pop()
|
||||
stack = [seed]
|
||||
component = [seed]
|
||||
while stack:
|
||||
v = stack.pop()
|
||||
for edge in v.link_edges:
|
||||
other = edge.other_vert(v)
|
||||
if other in remaining:
|
||||
remaining.discard(other)
|
||||
stack.append(other)
|
||||
component.append(other)
|
||||
components.append(component)
|
||||
return components
|
||||
|
||||
|
||||
class ProfileDecorator:
|
||||
installed = None
|
||||
|
||||
@@ -289,7 +265,7 @@ class ProfileDecorator:
|
||||
# Draw arcs
|
||||
arc_centroids = []
|
||||
arc_segments = []
|
||||
for arc in _connected_components(arcs):
|
||||
for arc in arcs.values():
|
||||
if len(arc) != 3:
|
||||
continue
|
||||
sorted_arc = [None, None, None]
|
||||
@@ -316,7 +292,7 @@ class ProfileDecorator:
|
||||
# Draw circles
|
||||
circle_centroids = []
|
||||
circle_segments = []
|
||||
for circle in _connected_components(circles):
|
||||
for circle in circles.values():
|
||||
if len(circle) != 2:
|
||||
continue
|
||||
p1 = obj.matrix_world @ circle[0].co
|
||||
|
||||
@@ -162,7 +162,7 @@ class ProjectLibraryData:
|
||||
library_file = IfcStore.library_file
|
||||
if library_file is None or library_file.schema == "IFC2X3":
|
||||
return results
|
||||
root = library_file.by_type("IfcProject")[0]
|
||||
root = tool.Project.get_root_context(library_file)
|
||||
results.append((str(root.id()), f"{root.is_a()} {root.Name or 'Unnamed'}", root.Description or ""))
|
||||
for library_id, data in cls.data["project_libraries"].items():
|
||||
results.append((str(library_id), data["Name"] or "Unnamed", data["Description"] or ""))
|
||||
|
||||
@@ -281,7 +281,7 @@ class RefreshLibrary(bpy.types.Operator):
|
||||
elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)}
|
||||
self.props.add_library_project_library("Unassigned", len(elements), 0, False)
|
||||
|
||||
root_context = library_file.by_type("IfcProject")[0]
|
||||
root_context = tool.Project.get_root_context(library_file)
|
||||
hierarchy = tool.Project.get_project_hierarchy(library_file)
|
||||
tool.Project.load_project_libraries_to_ui(root_context, hierarchy)
|
||||
return {"FINISHED"}
|
||||
@@ -761,22 +761,21 @@ class EditProjectLibrary(bpy.types.Operator):
|
||||
attributes = bonsai.bim.helper.export_attributes(props.project_library_attributes)
|
||||
ifcopenshell.api.attribute.edit_attributes(library_file, project_library, attributes)
|
||||
|
||||
# Update parent library. Tear down the old IfcRelDeclares/IfcRelNests before
|
||||
# creating the new one; a library must have exactly one of the two, never both.
|
||||
# Update parent library.
|
||||
previous_parent_library = tool.Project.get_parent_library(project_library)
|
||||
new_parent_library = library_file.by_id(int(props.parent_library))
|
||||
if previous_parent_library != new_parent_library:
|
||||
if previous_parent_library is not None:
|
||||
if previous_parent_library.is_a("IfcProject"):
|
||||
ifcopenshell.api.project.unassign_declaration(
|
||||
library_file, [project_library], previous_parent_library
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.nest.unassign_object(library_file, [project_library])
|
||||
if new_parent_library.is_a("IfcProject"):
|
||||
ifcopenshell.api.project.assign_declaration(library_file, [project_library], new_parent_library)
|
||||
else:
|
||||
if previous_parent_library is None:
|
||||
# Edited library was a root in a library-only file; nest it under the new parent.
|
||||
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
|
||||
elif previous_parent_library.is_a("IfcProject"):
|
||||
# Then new one is IfcProjectLibrary.
|
||||
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
|
||||
else: # Previous is IfcProjectLibrary.
|
||||
ifcopenshell.api.nest.unassign_object(library_file, [project_library])
|
||||
# If new one is IfcProject, then it's already assigned by default.
|
||||
if new_parent_library.is_a("IfcProjectLibrary"):
|
||||
ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library)
|
||||
|
||||
props.is_editing_project_library = False
|
||||
bpy.ops.bim.refresh_library()
|
||||
@@ -810,9 +809,12 @@ class AddProjectLibrary(bpy.types.Operator):
|
||||
props = tool.Project.get_project_props()
|
||||
library_file = IfcStore.library_file
|
||||
assert library_file
|
||||
root_context = library_file.by_type("IfcProject")[0]
|
||||
root_context = tool.Project.get_root_context(library_file)
|
||||
project_library = ifcopenshell.api.root.create_entity(library_file, "IfcProjectLibrary")
|
||||
ifcopenshell.api.project.assign_declaration(library_file, [project_library], root_context)
|
||||
if root_context.is_a("IfcProject"):
|
||||
ifcopenshell.api.project.assign_declaration(library_file, [project_library], root_context)
|
||||
else:
|
||||
ifcopenshell.api.nest.assign_object(library_file, [project_library], root_context)
|
||||
ProjectLibraryData.load() # Update enum.
|
||||
props.selected_project_library = str(project_library.id())
|
||||
props.is_editing_project_library = True
|
||||
@@ -1299,10 +1301,6 @@ class LoadProjectElements(bpy.types.Operator):
|
||||
tool.Project.set_default_modeling_dimensions()
|
||||
tool.Root.reload_grid_decorator()
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
bonsai.bim.handler.viewport_shading_changed_callback(area)
|
||||
return {"FINISHED"}
|
||||
|
||||
def get_decomposition_elements(self) -> set[ifcopenshell.entity_instance]:
|
||||
@@ -1580,13 +1578,10 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
|
||||
|
||||
def should_clear_cache() -> bool:
|
||||
# Nothing to clear if the cache file was never created (e.g. a
|
||||
# fresh link). Check this first so os.remove below is never
|
||||
# called on a non-existent path, regardless of use_cache.
|
||||
if not blend_filepath.exists():
|
||||
return False
|
||||
if not self.use_cache:
|
||||
return True
|
||||
if not blend_filepath.exists():
|
||||
return False
|
||||
data = json.loads(json_filepath.read_text())
|
||||
# Empty 'query' - model loaded without custom query.
|
||||
# Missing 'query' - model was loaded before custom queries were introduced in Bonsai.
|
||||
|
||||
@@ -54,7 +54,7 @@ class Data:
|
||||
ifc_file = tool.Ifc.get()
|
||||
results = []
|
||||
psetqtos = ifcopenshell.util.element.get_psets(
|
||||
element, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False, verbose=True
|
||||
element, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False
|
||||
)
|
||||
for name, data in sorted(psetqtos.items()):
|
||||
pset = ifc_file.by_id(data["id"])
|
||||
@@ -69,28 +69,13 @@ class Data:
|
||||
"id": data["id"],
|
||||
"Name": name,
|
||||
"is_expanded": is_expanded.get(data["id"], True),
|
||||
"Properties": [
|
||||
cls.property_display_data(ifc_file, k, v) for k, v in sorted(data.items()) if k != "id"
|
||||
],
|
||||
"Properties": [{"Name": k, "NominalValue": v} for k, v in sorted(data.items()) if k != "id"],
|
||||
"shared_pset_uses": len(pset_uses),
|
||||
"has_template": has_template,
|
||||
}
|
||||
)
|
||||
return sorted(results, key=lambda v: v["Name"])
|
||||
|
||||
@classmethod
|
||||
def property_display_data(cls, ifc_file: ifcopenshell.file, name: str, verbose_value: Any) -> dict[str, Any]:
|
||||
# Predefined property sets (e.g. IfcDoorPanelProperties) expose plain
|
||||
# attribute values even in verbose mode, since they're typed IFC
|
||||
# attributes rather than IfcProperty entities with their own id/Unit.
|
||||
if not isinstance(verbose_value, dict):
|
||||
return {"Name": name, "NominalValue": verbose_value, "UnitSymbol": ""}
|
||||
|
||||
unit_symbol = ""
|
||||
if (prop_id := verbose_value.get("id")) and (prop_entity := ifc_file.by_id(prop_id)):
|
||||
unit_symbol = tool.Pset.get_unit_symbol_for_prop(prop_entity, ifc_file)
|
||||
return {"Name": name, "NominalValue": verbose_value["value"], "UnitSymbol": unit_symbol}
|
||||
|
||||
@classmethod
|
||||
def format_pset_enum(cls, psets):
|
||||
enum_items = []
|
||||
|
||||
@@ -120,20 +120,13 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator):
|
||||
properties = json.loads(self.properties)
|
||||
else:
|
||||
for prop in props.properties:
|
||||
metadata = prop.metadata
|
||||
if prop.value_type == "IfcPropertySingleValue":
|
||||
value = metadata.get_value()
|
||||
properties[prop.metadata.name] = prop.metadata.get_value()
|
||||
elif prop.value_type == "IfcPropertyEnumeratedValue":
|
||||
value_name = metadata.get_value_name()
|
||||
value = [e[value_name] for e in prop.enumerated_value.enumerated_values if e.is_selected]
|
||||
else:
|
||||
continue
|
||||
# None (a purge/skip-creation signal, handled by edit_pset/edit_qto before any
|
||||
# unit wrapping is unpacked) must stay bare -- only wrap real values.
|
||||
if value is not None and tool.Pset.is_measurable_special_type(metadata.special_type):
|
||||
unit = self.file.by_id(metadata.unit_id) if metadata.unit_id else None
|
||||
value = {"NominalValue": value, "Unit": unit}
|
||||
properties[metadata.name] = value
|
||||
value_name = prop.metadata.get_value_name()
|
||||
properties[prop.metadata.name] = [
|
||||
e[value_name] for e in prop.enumerated_value.enumerated_values if e.is_selected
|
||||
]
|
||||
|
||||
if pset.is_a() in ("IfcPropertySet", "IfcMaterialProperties", "IfcProfileProperties"):
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
@@ -147,18 +140,10 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for key, value in properties.items():
|
||||
if value is None:
|
||||
continue
|
||||
is_wrapped = isinstance(value, dict) and "Unit" in value
|
||||
raw = value["NominalValue"] if is_wrapped else value
|
||||
if raw is None:
|
||||
continue
|
||||
if isinstance(raw, float):
|
||||
raw = round(raw, 4)
|
||||
elif not isinstance(raw, int):
|
||||
raw = 0
|
||||
if is_wrapped:
|
||||
value["NominalValue"] = raw
|
||||
else:
|
||||
properties[key] = raw
|
||||
if isinstance(value, float):
|
||||
properties[key] = round(value, 4)
|
||||
elif not isinstance(value, int):
|
||||
properties[key] = 0
|
||||
ifcopenshell.api.pset.edit_qto(
|
||||
self.file,
|
||||
qto=pset,
|
||||
|
||||
@@ -67,10 +67,6 @@ def draw_single_property(prop: IfcProperty, layout: bpy.types.UILayout, copy_ope
|
||||
if prop.metadata.special_type == "URI":
|
||||
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
|
||||
op.attribute_data_path = tool.Blender.get_full_data_path(prop.metadata)
|
||||
if tool.Pset.is_measurable_special_type(prop.metadata.special_type):
|
||||
unit_row = layout.row(align=True)
|
||||
unit_row.scale_x = 0.5
|
||||
prop_with_search(unit_row, prop.metadata, "unit_id_enum", text="")
|
||||
if prop.metadata.is_optional:
|
||||
layout.prop(prop.metadata, "is_null", icon="RADIOBUT_OFF" if prop.metadata.is_null else "RADIOBUT_ON", text="")
|
||||
if copy_operator:
|
||||
@@ -207,10 +203,9 @@ def draw_psetqto_ui(
|
||||
row = box.row(align=True)
|
||||
row.scale_y = 0.8
|
||||
row.label(text=prop["Name"])
|
||||
display_value = get_display_value(nominal_value)
|
||||
if unit_symbol := prop["UnitSymbol"]:
|
||||
display_value = f"{display_value} {unit_symbol}"
|
||||
op = row.operator("bim.select_similar", text=display_value, icon="NONE", emboss=False)
|
||||
op = row.operator(
|
||||
"bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False
|
||||
)
|
||||
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
|
||||
# calculate sum of all selected objects
|
||||
if active_operator:
|
||||
|
||||
@@ -104,7 +104,7 @@ def get_footing_length(o: bpy.types.Object) -> float:
|
||||
return get_length(o)
|
||||
if predefined_type == "FOOTING_BEAM" or predefined_type == "STRIP_FOOTING":
|
||||
return get_z(o)
|
||||
elif predefined_type == "PAD_FOOTING" or predefined_type == "PILE_CAP":
|
||||
elif predefined_type == "PAD_FOOTING":
|
||||
return max(get_x(o), get_y(o))
|
||||
else:
|
||||
return get_length(o)
|
||||
@@ -197,26 +197,12 @@ def get_footing_height(o: bpy.types.Object) -> float:
|
||||
return get_height(o)
|
||||
if predefined_type == "FOOTING_BEAM" or predefined_type == "STRIP_FOOTING":
|
||||
return get_y(o)
|
||||
elif predefined_type == "PAD_FOOTING" or predefined_type == "PILE_CAP":
|
||||
elif predefined_type == "PAD_FOOTING":
|
||||
return get_z(o)
|
||||
else:
|
||||
return get_height(o)
|
||||
|
||||
|
||||
def get_footing_width(o: bpy.types.Object) -> float:
|
||||
element = tool.Ifc.get_entity(o)
|
||||
assert element
|
||||
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if not predefined_type:
|
||||
return get_width(o)
|
||||
if predefined_type == "FOOTING_BEAM" or predefined_type == "STRIP_FOOTING":
|
||||
return get_x(o)
|
||||
elif predefined_type == "PAD_FOOTING" or predefined_type == "PILE_CAP":
|
||||
return get_width(o)
|
||||
else:
|
||||
return get_width(o)
|
||||
|
||||
|
||||
def get_height(o: bpy.types.Object) -> float:
|
||||
"""_summary_: Returns the height of the object bounding box
|
||||
|
||||
@@ -239,7 +225,7 @@ def get_opening_depth(obj: bpy.types.Object) -> float:
|
||||
if is_opening_horizontal(obj):
|
||||
return get_height(obj)
|
||||
else:
|
||||
return get_y(obj)
|
||||
return get_width(obj)
|
||||
|
||||
|
||||
def get_opening_mapping_area(obj: bpy.types.Object) -> float:
|
||||
|
||||
@@ -151,7 +151,7 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifc_file = tool.Ifc.get()
|
||||
with Profiler("Quantify function time:"):
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
ifc5d.qto.edit_qtos(ifc_file, results, target_units=tool.Qto.get_target_units(), rules=rules)
|
||||
ifc5d.qto.edit_qtos(ifc_file, results)
|
||||
|
||||
not_quantified_elements = elements - set(results.keys())
|
||||
not_quantified_message = tool.Qto.get_not_quantified_elements_message(not_quantified_elements)
|
||||
@@ -194,7 +194,7 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifc_file = tool.Ifc.get()
|
||||
with Profiler("Quantify function time:"):
|
||||
results = ifc5d.qto.quantify(ifc_file, elements, rules)
|
||||
ifc5d.qto.edit_qtos(ifc_file, results, target_units=tool.Qto.get_target_units(), rules=rules)
|
||||
ifc5d.qto.edit_qtos(ifc_file, results)
|
||||
not_quantified_elements = elements - set(results.keys())
|
||||
return not_quantified_elements
|
||||
|
||||
|
||||
@@ -27,28 +27,10 @@ from bpy.props import (
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
import bonsai.bim.prop
|
||||
import bonsai.tool as tool
|
||||
|
||||
CALCULATOR_FUNCTION_ENUM_ITEMS: list[Union[tuple[str, str, str], None]] = []
|
||||
|
||||
# Measure class (matching ifc5d.qto's Function.measure / SI2ProjectUnitConverter.project_units'
|
||||
# keys) -> (BIMQtoProperties field name, tool.Pset special_type, UI label).
|
||||
MEASURE_TO_TARGET_UNIT_FIELD: dict[str, tuple[str, str, str]] = {
|
||||
"IfcLengthMeasure": ("target_unit_length", "LENGTH", "Length"),
|
||||
"IfcAreaMeasure": ("target_unit_area", "AREA", "Area"),
|
||||
"IfcVolumeMeasure": ("target_unit_volume", "VOLUME", "Volume"),
|
||||
"IfcMassMeasure": ("target_unit_mass", "MASS", "Mass"),
|
||||
"IfcTimeMeasure": ("target_unit_time", "TIME", "Time"),
|
||||
}
|
||||
|
||||
|
||||
def _target_unit_items(special_type: str):
|
||||
def getter(self: "BIMQtoProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
return bonsai.bim.prop.get_unit_enum_items_for_special_type(special_type, tool.Ifc.get())
|
||||
|
||||
return getter
|
||||
|
||||
|
||||
def get_qto_rule(self: "BIMQtoProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
results: list[tuple[str, str, str]] = []
|
||||
@@ -110,11 +92,6 @@ class BIMQtoProperties(PropertyGroup):
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
target_unit_length: EnumProperty(items=_target_unit_items("LENGTH"), name="Length Unit")
|
||||
target_unit_area: EnumProperty(items=_target_unit_items("AREA"), name="Area Unit")
|
||||
target_unit_volume: EnumProperty(items=_target_unit_items("VOLUME"), name="Volume Unit")
|
||||
target_unit_mass: EnumProperty(items=_target_unit_items("MASS"), name="Mass Unit")
|
||||
target_unit_time: EnumProperty(items=_target_unit_items("TIME"), name="Time Unit")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
qto_rule: str
|
||||
@@ -124,8 +101,3 @@ class BIMQtoProperties(PropertyGroup):
|
||||
qto_name: str
|
||||
prop_name: str
|
||||
fallback: bool
|
||||
target_unit_length: str
|
||||
target_unit_area: str
|
||||
target_unit_volume: str
|
||||
target_unit_mass: str
|
||||
target_unit_time: str
|
||||
|
||||
@@ -17,12 +17,9 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifc5d.qto
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import prop_with_search
|
||||
from bonsai.bim.module.qto.data import QtoData
|
||||
from bonsai.bim.module.qto.prop import MEASURE_TO_TARGET_UNIT_FIELD
|
||||
|
||||
|
||||
class BIM_PT_qto(bpy.types.Panel):
|
||||
@@ -47,14 +44,6 @@ class BIM_PT_qto(bpy.types.Panel):
|
||||
row = layout.row()
|
||||
row.prop(props, "qto_rule", text="")
|
||||
row.prop(props, "fallback", text="", icon="RADIOBUT_ON" if props.fallback else "RADIOBUT_OFF")
|
||||
|
||||
box = layout.box()
|
||||
box.label(text="Target Units (optional, otherwise project default)")
|
||||
for field_name, _special_type, label in MEASURE_TO_TARGET_UNIT_FIELD.values():
|
||||
row = box.row(align=True)
|
||||
row.label(text=label)
|
||||
prop_with_search(row, props, field_name, text="")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.perform_quantity_take_off")
|
||||
|
||||
@@ -77,15 +66,6 @@ class BIM_PT_qto_manual(bpy.types.Panel):
|
||||
row = layout.row()
|
||||
row.prop(props, "calculator_function", text="Function")
|
||||
|
||||
calculator = ifc5d.qto.calculators.get(props.calculator)
|
||||
function = calculator.functions.get(props.calculator_function) if calculator else None
|
||||
target_unit_field = MEASURE_TO_TARGET_UNIT_FIELD.get(function.measure) if function else None
|
||||
if target_unit_field:
|
||||
field_name, _special_type, label = target_unit_field
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{label} Unit")
|
||||
prop_with_search(row, props, field_name, text="")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(props, "qto_name", text="")
|
||||
row.prop(props, "prop_name", text="")
|
||||
|
||||
@@ -413,13 +413,12 @@ class UnlinkObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
skip_invoke: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def _execute(self, context):
|
||||
objects: list[bpy.types.Object]
|
||||
if self.obj:
|
||||
requested_obj = bpy.data.objects.get(self.obj)
|
||||
objects = [requested_obj] if requested_obj is not None else []
|
||||
objects = [bpy.data.objects.get(self.obj)]
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
|
||||
objects: list[bpy.types.Object]
|
||||
for obj in objects:
|
||||
was_active_object = obj == context.active_object
|
||||
|
||||
|
||||
@@ -132,12 +132,5 @@ class SelectSimilarData:
|
||||
if pset.endswith("Common"):
|
||||
keys.extend([f'/.*Common/."{name}"' for name in properties.keys() if name != "id"])
|
||||
else:
|
||||
pset_part = f'"{pset}"' if " " in pset else pset
|
||||
keys.extend(
|
||||
[
|
||||
f'{pset_part}."{name}"' if " " in name else f"{pset_part}.{name}"
|
||||
for name in properties.keys()
|
||||
if name != "id"
|
||||
]
|
||||
)
|
||||
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
|
||||
return [(k, k, "") for k in keys]
|
||||
|
||||
@@ -12,7 +12,7 @@ function create_gantt_chart(json_data) {
|
||||
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ['Hour', 'Day', 'Week', 'Month', 'Quarter'], // vUseSingleCell keeps Hour usable on large charts.
|
||||
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vShowRes: true, // Disable the resource column.
|
||||
vShowComp: false, // Disable the completion column.
|
||||
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
|
||||
|
||||
@@ -45,7 +45,6 @@ class SpatialTool(WorkSpaceTool):
|
||||
("bim.spatial_hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
|
||||
("bim.spatial_hotkey", {"type": "G", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_G")]}),
|
||||
("bim.spatial_hotkey", {"type": "H", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_H")]}),
|
||||
("bim.spatial_hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, ws_tool):
|
||||
@@ -185,9 +184,3 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def hotkey_S_G(self):
|
||||
bpy.ops.bim.generate_space()
|
||||
|
||||
def hotkey_S_Q(self):
|
||||
# Mirrors BimTool.hotkey_S_Q so quantities can be (re)calculated without switching tools.
|
||||
if not bpy.context.selected_objects:
|
||||
return
|
||||
bpy.ops.bim.perform_quantity_take_off()
|
||||
|
||||
@@ -45,9 +45,7 @@ classes = (
|
||||
operator.SelectByStyle,
|
||||
operator.SelectStyleInStylesUI,
|
||||
operator.SetAssetMaterialToExternalStyle,
|
||||
operator.SuggestShadeFromExternalStyle,
|
||||
operator.UnlinkStyle,
|
||||
operator.TogglePreferIfcShading,
|
||||
operator.UpdateCurrentStyle,
|
||||
operator.UpdateStyleColours,
|
||||
operator.UpdateStyleTextures,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import colorsys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
@@ -239,15 +238,13 @@ class UpdateCurrentStyle(bpy.types.Operator):
|
||||
if not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve)):
|
||||
continue
|
||||
for mat in obj.data.materials:
|
||||
if not mat:
|
||||
continue
|
||||
msprops_ = tool.Style.get_material_style_props(mat)
|
||||
if msprops_.ifc_definition_id == 0:
|
||||
continue
|
||||
if mat in updated_materials:
|
||||
continue
|
||||
msprops_.active_style_type = current_style_type
|
||||
updated_materials.add(mat)
|
||||
if (
|
||||
mat
|
||||
and mat not in updated_materials
|
||||
and (msprops_ := tool.Style.get_material_style_props(mat)).ifc_definition_id != 0
|
||||
):
|
||||
msprops_.active_style_type = current_style_type
|
||||
updated_materials.add(mat)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -460,14 +457,10 @@ class ActivateExternalStyle(bpy.types.Operator):
|
||||
self.report({"ERROR"}, f"Error loading external style for \"{material.name}\" - {db['msg']}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
ext_mat = db["data_block"]
|
||||
self.copy_material_attributes(ext_mat, material)
|
||||
self.copy_material_attributes(db["data_block"], material)
|
||||
if tool.Style.get_use_nodes(material):
|
||||
if material.get("bim_dual_branch"):
|
||||
tool.Style.update_external_branch(material, ext_mat)
|
||||
else:
|
||||
tool.Style.setup_dual_branch(material, ext_mat)
|
||||
bpy.data.materials.remove(ext_mat)
|
||||
tool.Blender.copy_node_graph(material, db["data_block"])
|
||||
bpy.data.materials.remove(db["data_block"])
|
||||
return {"FINISHED"}
|
||||
|
||||
def copy_material_attributes(self, source, target):
|
||||
@@ -510,218 +503,6 @@ class ActivateExternalStyle(bpy.types.Operator):
|
||||
set_prop(prop_name)
|
||||
|
||||
|
||||
class TogglePreferIfcShading(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_prefer_ifc_shading"
|
||||
bl_label = "Toggle Flat/Pretty"
|
||||
bl_description = (
|
||||
"Toggle between Flat (IFC-native shading) and Pretty (external .blend style) for ALL styles.\n\n"
|
||||
"SHIFT+CLICK to apply to this style only"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material_name: bpy.props.StringProperty(name="Material Name", default="", options={"SKIP_SAVE"})
|
||||
single_only: bpy.props.BoolProperty(name="Single Only", default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.shift:
|
||||
self.single_only = True
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
wm = context.window_manager
|
||||
space = tool.Blender.get_view3d_space()
|
||||
is_solid = space and space.shading.type == "SOLID"
|
||||
|
||||
if is_solid:
|
||||
if space.shading.color_type == "TEXTURE":
|
||||
space.shading.color_type = "MATERIAL"
|
||||
else:
|
||||
meshes_needing_uv = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if not isinstance(obj.data, bpy.types.Mesh):
|
||||
continue
|
||||
for slot in obj.material_slots:
|
||||
mat = slot.material
|
||||
if not mat or not tool.Blender.get_ifc_definition_id(mat):
|
||||
continue
|
||||
style_elements = tool.Style.get_style_elements(mat)
|
||||
if style_elements.get("IfcSurfaceStyleWithTextures") and not obj.data.uv_layers:
|
||||
meshes_needing_uv.append(obj.data)
|
||||
break
|
||||
wm.progress_begin(0, max(len(meshes_needing_uv), 1))
|
||||
try:
|
||||
for i, mesh in enumerate(meshes_needing_uv):
|
||||
tool.Loader.load_generated_uv_map(mesh)
|
||||
wm.progress_update(i)
|
||||
finally:
|
||||
wm.progress_end()
|
||||
space.shading.color_type = "TEXTURE"
|
||||
return {"FINISHED"}
|
||||
|
||||
if self.single_only:
|
||||
mat = bpy.data.materials.get(self.material_name)
|
||||
if not mat:
|
||||
return {"CANCELLED"}
|
||||
msprops = tool.Style.get_material_style_props(mat)
|
||||
msprops.prefer_ifc_shading = not msprops.prefer_ifc_shading
|
||||
else:
|
||||
# Default: apply to all IFC materials
|
||||
source_mat = bpy.data.materials.get(self.material_name)
|
||||
new_value = not tool.Style.get_material_style_props(source_mat).prefer_ifc_shading if source_mat else True
|
||||
ifc_mats = [m for m in bpy.data.materials if tool.Blender.get_ifc_definition_id(m)]
|
||||
for mat in ifc_mats:
|
||||
tool.Style.get_material_style_props(mat).prefer_ifc_shading = new_value
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SuggestShadeFromExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.suggest_shade_from_external_style"
|
||||
bl_label = "Suggest Shade from External Style"
|
||||
bl_description = (
|
||||
"Generate a Shade style (Surface Colour + Transparency) from the external .blend style.\n\n"
|
||||
"ALT+CLICK to apply to all styles with an external .blend style"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
material_name: bpy.props.StringProperty(name="Material Name", default="", options={"SKIP_SAVE"})
|
||||
all_styles: bpy.props.BoolProperty(name="All Styles", default=False, options={"SKIP_SAVE"})
|
||||
value_offset: bpy.props.FloatProperty(
|
||||
name="Value",
|
||||
description="Offset added to the colour's value (-1 = fully dark, 0 = unchanged, +1 = fully light)",
|
||||
default=0.0,
|
||||
min=-1.0,
|
||||
max=1.0,
|
||||
step=1,
|
||||
precision=2,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
saturation_factor: bpy.props.FloatProperty(
|
||||
name="Saturation",
|
||||
description="Scale applied to the colour's saturation (0 = greyscale, 1 = unchanged, >1 = more saturated)",
|
||||
default=1.0,
|
||||
min=0.0,
|
||||
max=2.0,
|
||||
step=1,
|
||||
precision=2,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.alt:
|
||||
self.all_styles = True
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "value_offset", slider=True)
|
||||
layout.prop(self, "saturation_factor", slider=True)
|
||||
if self.all_styles:
|
||||
layout.label(text="Will apply to all external styles", icon="INFO")
|
||||
|
||||
def _execute(self, context):
|
||||
if self.all_styles:
|
||||
candidates = [
|
||||
(mat, tool.Style.get_style_elements(mat))
|
||||
for mat in bpy.data.materials
|
||||
if tool.Blender.get_ifc_definition_id(mat)
|
||||
]
|
||||
candidates = [(mat, se) for mat, se in candidates if tool.Style.has_blender_external_style(se)]
|
||||
wm = context.window_manager
|
||||
wm.progress_begin(0, max(len(candidates), 1))
|
||||
count = 0
|
||||
color_cache: dict[tuple[str, str, str], tuple | None] = {}
|
||||
try:
|
||||
for i, (mat, style_elements) in enumerate(candidates):
|
||||
wm.progress_update(i)
|
||||
if self._apply_to_material(
|
||||
mat, style_elements, self.value_offset, self.saturation_factor, color_cache
|
||||
):
|
||||
count += 1
|
||||
finally:
|
||||
wm.progress_end()
|
||||
self.report({"INFO"}, f"Shade style generated for {count} style(s).")
|
||||
else:
|
||||
mat = bpy.data.materials.get(self.material_name)
|
||||
if not mat:
|
||||
return {"CANCELLED"}
|
||||
style_elements = tool.Style.get_style_elements(mat)
|
||||
if not tool.Style.has_blender_external_style(style_elements):
|
||||
self.report({"ERROR"}, "No external .blend style assigned. Please assign an external style first.")
|
||||
return {"CANCELLED"}
|
||||
self._apply_to_material(mat, style_elements, self.value_offset, self.saturation_factor)
|
||||
props = tool.Style.get_style_props()
|
||||
if props.is_editing:
|
||||
core.load_styles(tool.Style, style_type=props.style_type)
|
||||
|
||||
def _apply_to_material(
|
||||
self,
|
||||
material: bpy.types.Material,
|
||||
style_elements: dict,
|
||||
value_offset: float = 0.0,
|
||||
saturation_factor: float = 1.0,
|
||||
color_cache: "dict[tuple[str, str, str], tuple | None] | None" = None,
|
||||
) -> bool:
|
||||
external_style = style_elements["IfcExternallyDefinedSurfaceStyle"]
|
||||
style_path = Path(tool.Ifc.resolve_uri(external_style.Location))
|
||||
data_block_type, data_block = external_style.Identification.split("/")
|
||||
|
||||
cache_key = (str(style_path), data_block_type, data_block)
|
||||
if color_cache is not None and cache_key in color_cache:
|
||||
cached = color_cache[cache_key]
|
||||
if cached is None:
|
||||
return False # previously failed for this path
|
||||
surface_colour, transparency = cached
|
||||
else:
|
||||
try:
|
||||
db = tool.Blender.append_data_block(str(style_path), data_block_type, data_block)
|
||||
except OSError as e:
|
||||
self.report({"WARNING"}, f'Could not open blend file for "{material.name}": {e}')
|
||||
if color_cache is not None:
|
||||
color_cache[cache_key] = None
|
||||
return False
|
||||
if not db["data_block"]:
|
||||
self.report({"WARNING"}, f'Could not load external style for "{material.name}": {db["msg"]}')
|
||||
if color_cache is not None:
|
||||
color_cache[cache_key] = None
|
||||
return False
|
||||
|
||||
ext_mat = db["data_block"]
|
||||
surface_colour, transparency = tool.Style.get_representative_material_color(ext_mat)
|
||||
bpy.data.materials.remove(ext_mat)
|
||||
if color_cache is not None:
|
||||
color_cache[cache_key] = (surface_colour, transparency)
|
||||
|
||||
if value_offset != 0.0 or saturation_factor != 1.0:
|
||||
h, s, v = colorsys.rgb_to_hsv(*surface_colour)
|
||||
v = max(0.0, min(1.0, v + value_offset))
|
||||
s = max(0.0, min(1.0, s * saturation_factor))
|
||||
surface_colour = colorsys.hsv_to_rgb(h, s, v)
|
||||
|
||||
ifc_style = tool.Ifc.get_entity(material)
|
||||
attributes: dict = {
|
||||
"SurfaceColour": {
|
||||
"Name": None,
|
||||
"Red": surface_colour[0],
|
||||
"Green": surface_colour[1],
|
||||
"Blue": surface_colour[2],
|
||||
},
|
||||
}
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
attributes["Transparency"] = transparency
|
||||
|
||||
shading_style = style_elements.get("IfcSurfaceStyleShading")
|
||||
if shading_style:
|
||||
tool.Ifc.run("style.edit_surface_style", style=shading_style, attributes=attributes)
|
||||
else:
|
||||
tool.Ifc.run(
|
||||
"style.add_surface_style",
|
||||
style=ifc_style,
|
||||
ifc_class="IfcSurfaceStyleShading",
|
||||
attributes=attributes,
|
||||
)
|
||||
material.diffuse_color = (*surface_colour, 1.0 - transparency)
|
||||
tool.Style.sync_flat_branch_shading(material, surface_colour, transparency)
|
||||
return True
|
||||
|
||||
|
||||
class DisableEditingStyles(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_editing_styles"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@@ -372,16 +372,6 @@ def update_shading_style(self: "BIMStyleProperties", context: bpy.types.Context)
|
||||
tool.Style.switch_shading(blender_material, self.active_style_type)
|
||||
|
||||
|
||||
def update_prefer_ifc_shading(self: "BIMStyleProperties", context: bpy.types.Context) -> None:
|
||||
style_elements = tool.Style.get_style_elements(self.id_data)
|
||||
has_external = tool.Style.has_blender_external_style(style_elements)
|
||||
if self.prefer_ifc_shading or not has_external:
|
||||
self.active_style_type = "Shading"
|
||||
else:
|
||||
self.active_style_type = "External"
|
||||
self.id_data.update_tag()
|
||||
|
||||
|
||||
class BIMStyleProperties(PropertyGroup):
|
||||
ifc_definition_id: IntProperty(name="IFC Definition ID")
|
||||
active_style_type: EnumProperty(
|
||||
@@ -391,19 +381,9 @@ class BIMStyleProperties(PropertyGroup):
|
||||
default="Shading",
|
||||
update=update_shading_style,
|
||||
)
|
||||
prefer_ifc_shading: BoolProperty(
|
||||
name="Flat / Pretty",
|
||||
description=(
|
||||
"Toggle between Flat (IFC-native shading) and Pretty (external .blend style). "
|
||||
"When set to Flat, viewport switches to Material Preview or Rendered will not activate the external style."
|
||||
),
|
||||
default=False,
|
||||
update=update_prefer_ifc_shading,
|
||||
)
|
||||
is_renaming: BoolProperty(description="Used to prevent triggering handler callback.", default=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
ifc_definition_id: int
|
||||
active_style_type: tool.Style.StyleType
|
||||
prefer_ifc_shading: bool
|
||||
is_renaming: bool
|
||||
|
||||
@@ -110,11 +110,6 @@ class BIM_PT_styles(Panel):
|
||||
op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="")
|
||||
op.style_id = style.ifc_definition_id
|
||||
|
||||
if active_style and self.props.style_type == "IfcSurfaceStyle":
|
||||
if material := style.blender_material:
|
||||
msprops = tool.Style.get_material_style_props(material)
|
||||
self.draw_style_status_row(material, msprops)
|
||||
|
||||
if self.props.style_type == "IfcSurfaceStyle":
|
||||
self.layout.label(text="Surface Style Element:")
|
||||
col = self.layout.column(align=True)
|
||||
@@ -166,66 +161,6 @@ class BIM_PT_styles(Panel):
|
||||
edit_label = "Save Lighting Style"
|
||||
self.draw_edit_ui(edit_label)
|
||||
|
||||
def draw_style_status_row(self, material: bpy.types.Material, msprops) -> None:
|
||||
space = tool.Blender.get_view3d_space()
|
||||
box = self.layout.box()
|
||||
|
||||
obj = bpy.context.active_object
|
||||
|
||||
parts = []
|
||||
if space:
|
||||
shading_type = space.shading.type
|
||||
shading_labels = {
|
||||
"SOLID": "Solid",
|
||||
"MATERIAL": "Material Preview",
|
||||
"RENDERED": "Rendered",
|
||||
"WIREFRAME": "Wireframe",
|
||||
}
|
||||
parts.append(f"Viewport: {shading_labels.get(shading_type, shading_type)}")
|
||||
else:
|
||||
parts.append("No 3D viewport")
|
||||
shading_type = None
|
||||
|
||||
if obj:
|
||||
obj_has_uv = isinstance(obj.data, bpy.types.Mesh) and bool(obj.data.uv_layers)
|
||||
uv_label = "UV \u2713" if obj_has_uv else "UV \u2717"
|
||||
parts.append(f"Selected Object: {obj.name} {uv_label}")
|
||||
else:
|
||||
parts.append("Selected Object: None")
|
||||
|
||||
if shading_type == "SOLID":
|
||||
is_flat = space.shading.color_type != "TEXTURE"
|
||||
mode_label = "Flat"
|
||||
dep_label = "Shade"
|
||||
if not is_flat:
|
||||
mode_label = "Pretty"
|
||||
dep_label = "Texture \u2192 Shade"
|
||||
elif shading_type in ("MATERIAL", "RENDERED"):
|
||||
is_flat = msprops.prefer_ifc_shading
|
||||
mode_label = "Flat"
|
||||
dep_label = "Render+Texture \u2192 Render \u2192 Shade"
|
||||
if not is_flat:
|
||||
mode_label = "Pretty"
|
||||
dep_label = "External \u2192 Render+Texture \u2192 Render \u2192 Shade"
|
||||
else:
|
||||
is_flat = False
|
||||
mode_label = ""
|
||||
dep_label = ""
|
||||
|
||||
row1 = box.row(align=True)
|
||||
row1.label(text=" | ".join(parts))
|
||||
|
||||
if mode_label:
|
||||
row2 = box.row(align=True)
|
||||
row2.label(text=f"Current Mode: {mode_label} \u2014 {dep_label}")
|
||||
|
||||
row3 = box.row(align=True)
|
||||
row3.alignment = "RIGHT"
|
||||
op = row3.operator("bim.suggest_shade_from_external_style", text="", icon="BRUSHES_ALL")
|
||||
op.material_name = material.name
|
||||
op = row3.operator("bim.toggle_prefer_ifc_shading", text="", icon="UV_SYNC_SELECT")
|
||||
op.material_name = material.name
|
||||
|
||||
def draw_surface_style_shading(self):
|
||||
row = self.layout.row()
|
||||
row.prop(self.props, "surface_colour")
|
||||
|
||||
@@ -117,7 +117,9 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
related_object: str
|
||||
|
||||
@staticmethod
|
||||
def _reattach_styles(file: ifcopenshell.file, copied_entities: dict[int, ifcopenshell.entity_instance]) -> None:
|
||||
def _reattach_styles(
|
||||
file: ifcopenshell.file, copied_entities: dict[int, ifcopenshell.entity_instance]
|
||||
) -> None:
|
||||
"""copy_deep only follows forward references, so IfcStyledItem (an inverse,
|
||||
``StyledByItem``) is not carried onto the copied geometry. Re-create a
|
||||
styled item on each copy that points at the same presentation styles as
|
||||
|
||||
@@ -21,6 +21,7 @@ import os
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union, assert_never, get_args
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.unit
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
@@ -33,8 +34,6 @@ from bpy.props import (
|
||||
)
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
import bonsai.bim
|
||||
import bonsai.bim.handler
|
||||
import bonsai.tool as tool
|
||||
@@ -123,57 +122,6 @@ def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) ->
|
||||
return items
|
||||
|
||||
|
||||
def get_unit_enum_items_for_special_type(
|
||||
special_type: str, ifc_file: Union[ifcopenshell.file, None]
|
||||
) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
"""Items for a unit-override picker: "Default (<symbol>)" plus every candidate unit
|
||||
matching `special_type`, filtered per-caller since candidates depend on the measure type
|
||||
in question (unlike the globally-shared lists in `bonsai.bim.ui.EnumData`).
|
||||
"""
|
||||
if not ifc_file or not tool.Pset.is_measurable_special_type(special_type):
|
||||
return [(cache_string("0"), cache_string("Default"), "")]
|
||||
|
||||
default_symbol = tool.Pset.get_unit_symbol_for_special_type(special_type, ifc_file)
|
||||
items: list[tuple[str, str, str]] = [
|
||||
(cache_string("0"), cache_string(f"Default ({default_symbol})" if default_symbol else "Default"), "")
|
||||
]
|
||||
for unit in tool.Pset.get_candidate_units_for_special_type(special_type, ifc_file):
|
||||
name = getattr(unit, "Name", None) or unit.is_a()
|
||||
symbol = ifcopenshell.util.unit.get_unit_symbol(unit)
|
||||
label = f"{name} ({symbol})" if symbol else name
|
||||
items.append((cache_string(str(unit.id())), cache_string(label), ""))
|
||||
return items
|
||||
|
||||
|
||||
def get_attribute_unit_enum_items(prop: "Attribute", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
|
||||
"""Items for `Attribute.unit_id_enum`. Wraps `get_unit_enum_items_for_special_type` with a
|
||||
defensive addition: real-world files sometimes carry a Unit that doesn't cleanly match our
|
||||
candidate-matching logic (e.g. a mismatched UnitType). Always keep the attribute's own
|
||||
current override selectable/representable, however unusual, so setting unit_id_enum to
|
||||
match an already-seeded unit_id can never raise "enum not found".
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
items = get_unit_enum_items_for_special_type(prop.special_type, ifc_file)
|
||||
|
||||
if prop.unit_id and prop.unit_id not in {int(i[0]) for i in items}:
|
||||
own_unit = ifc_file.by_id(prop.unit_id)
|
||||
name = getattr(own_unit, "Name", None) or own_unit.is_a()
|
||||
symbol = ifcopenshell.util.unit.get_unit_symbol(own_unit)
|
||||
label = f"{name} ({symbol})" if symbol else name
|
||||
items.append((cache_string(str(prop.unit_id)), cache_string(label), ""))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def update_attribute_unit_id(self: "Attribute", context: bpy.types.Context) -> None:
|
||||
new_unit_id = int(tool.Blender.get_enum_safe(self, "unit_id_enum") or "0")
|
||||
if ifc_file := tool.Ifc.get():
|
||||
# Must run before self.unit_id is overwritten: convert_attribute_unit needs the OLD
|
||||
# unit_id to know what unit the current value is expressed in.
|
||||
tool.Pset.convert_attribute_unit(self, new_unit_id, ifc_file)
|
||||
self.unit_id = new_unit_id
|
||||
|
||||
|
||||
def update_schema_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
|
||||
import bonsai.bim.schema
|
||||
|
||||
@@ -302,33 +250,44 @@ def set_numerical_value(self: "Attribute", value_name: str, new_value: Union[flo
|
||||
self[value_name] = new_value
|
||||
|
||||
|
||||
def get_length_value(self: "Attribute") -> float:
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
return self.float_value * si_conversion
|
||||
|
||||
|
||||
def set_length_value(self: "Attribute", value: float) -> None:
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
self.float_value = value / si_conversion
|
||||
|
||||
|
||||
def get_display_name(self: "Attribute") -> str:
|
||||
DISPLAY_UNIT_TYPES = ("AREA", "VOLUME", "FORCE")
|
||||
name = self.name
|
||||
if not self.unit_symbol:
|
||||
if not self.special_type or self.special_type not in DISPLAY_UNIT_TYPES:
|
||||
return name
|
||||
return f"{name}, {self.unit_symbol}"
|
||||
|
||||
unit_type = f"{self.special_type}UNIT"
|
||||
project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), unit_type)
|
||||
if not project_unit:
|
||||
return name
|
||||
|
||||
def get_unit_symbol(self: "Attribute") -> str:
|
||||
"""The symbol for whatever unit the value is currently expressed in: this property's own
|
||||
override (`unit_id`) if set, else the project default for `special_type`. Computed fresh on
|
||||
every access (rather than cached at import time) so it stays correct immediately after the
|
||||
unit picker changes `unit_id`, and after the project's own default units are edited.
|
||||
"""
|
||||
if not tool.Pset.is_measurable_special_type(self.special_type):
|
||||
return ""
|
||||
if not (ifc_file := tool.Ifc.get()):
|
||||
return ""
|
||||
unit = tool.Pset.resolve_effective_unit(self.special_type, self.unit_id, ifc_file)
|
||||
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
|
||||
unit_symbol = ifcopenshell.util.unit.get_unit_symbol(project_unit)
|
||||
return f"{name}, {unit_symbol}"
|
||||
|
||||
|
||||
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
|
||||
# Either "", "DATE", "DATETIME", "LOGICAL", "URI", "DURATION", or an
|
||||
# IfcUnitEnum/IfcDerivedUnitEnum value with the "UNIT" suffix stripped (e.g.
|
||||
# "LENGTH", "PRESSURE", "MODULUSOFELASTICITY") as returned by
|
||||
# tool.Pset.get_special_type_for_prop().
|
||||
AttributeSpecialType = str
|
||||
AttributeSpecialType = Literal[
|
||||
"",
|
||||
"DATE",
|
||||
"DATETIME",
|
||||
"LENGTH",
|
||||
"AREA",
|
||||
"VOLUME",
|
||||
"FORCE",
|
||||
"LOGICAL",
|
||||
"URI",
|
||||
"DURATION",
|
||||
]
|
||||
|
||||
|
||||
class Attribute(PropertyGroup):
|
||||
@@ -359,6 +318,9 @@ class Attribute(PropertyGroup):
|
||||
get=lambda self: float(self.get("float_value", 0.0)),
|
||||
set=set_float_value,
|
||||
)
|
||||
length_value: FloatProperty(
|
||||
name="Value", description=tooltip, get=get_length_value, set=set_length_value, unit="LENGTH"
|
||||
)
|
||||
enum_items: StringProperty(name="Value")
|
||||
"""Json serialized mapping of enum items:
|
||||
Typically a dictionary of string identifiers to item names.
|
||||
@@ -380,10 +342,6 @@ class Attribute(PropertyGroup):
|
||||
value_max: FloatProperty(description="This is used to validate int_value and float_value")
|
||||
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
|
||||
special_type: StringProperty(name="Special Value Type", default="")
|
||||
unit_symbol: StringProperty(name="Unit Symbol", get=get_unit_symbol)
|
||||
unit_id: IntProperty(name="Unit Override", default=0)
|
||||
"""STEP id of this property/quantity's own Unit override. 0 means "use the project default"."""
|
||||
unit_id_enum: EnumProperty(items=get_attribute_unit_enum_items, name="Unit", update=update_attribute_unit_id)
|
||||
use_explorer_ui: BoolProperty()
|
||||
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
|
||||
update: StringProperty(name="Update", description="Custom update function to be executed")
|
||||
@@ -399,6 +357,7 @@ class Attribute(PropertyGroup):
|
||||
bool_value: bool
|
||||
int_value: int
|
||||
float_value: float
|
||||
length_value: float
|
||||
enum_items: str
|
||||
enum_items_dynamic: str
|
||||
enum_descriptions: bpy.types.bpy_prop_collection_idprop[StrProperty]
|
||||
@@ -414,9 +373,6 @@ class Attribute(PropertyGroup):
|
||||
value_min_constraint: bool
|
||||
value_max: float
|
||||
value_max_constraint: bool
|
||||
unit_symbol: str
|
||||
unit_id: int
|
||||
unit_id_enum: str
|
||||
use_explorer_ui: bool
|
||||
metadata: str
|
||||
update: str
|
||||
@@ -474,6 +430,8 @@ class Attribute(PropertyGroup):
|
||||
elif data_type == "integer":
|
||||
return "int_value"
|
||||
elif data_type == "float":
|
||||
if display_only and self.special_type == "LENGTH":
|
||||
return "length_value"
|
||||
return "float_value"
|
||||
elif data_type == "enum":
|
||||
return "enum_value"
|
||||
|
||||
@@ -41,7 +41,6 @@ import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import is_cache_locked_by_other_process
|
||||
from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty
|
||||
from bonsai.bim.module.material.operator import SelectByMaterial
|
||||
from bonsai.bim.module.model import prop as _model_prop
|
||||
from bonsai.bim.module.model import ui as _model_ui
|
||||
from bonsai.bim.module.pset.prop import IfcProperty
|
||||
@@ -1855,21 +1854,6 @@ def draw_statusbar(self, context):
|
||||
|
||||
def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None:
|
||||
# https://blender.stackexchange.com/a/275555/86891
|
||||
|
||||
# Context menu for material name buttons (e.g. `bim.select_by_material`),
|
||||
# offering a quick "Rename Material" entry instead of having to look up
|
||||
# the material in the scene Materials panel to rename it.
|
||||
button_operator = getattr(context, "button_operator", None)
|
||||
if button_operator is not None and button_operator.bl_rna.identifier == SelectByMaterial.bl_rna.identifier:
|
||||
ifc_file = tool.Ifc.get()
|
||||
material = ifc_file.by_id(button_operator.material) if ifc_file else None
|
||||
if material is not None and material.is_a("IfcMaterial"):
|
||||
assert self.layout
|
||||
self.layout.separator()
|
||||
op = self.layout.operator("bim.rename_material", text="Rename Material", icon="GREASEPENCIL")
|
||||
op.material = material.id()
|
||||
return
|
||||
|
||||
if (
|
||||
not hasattr(context, "button_pointer")
|
||||
or not hasattr(context, "button_prop")
|
||||
|
||||
@@ -411,37 +411,6 @@ def duplicate_drawing(
|
||||
return new_drawing
|
||||
|
||||
|
||||
def copy_annotations_to_drawing(
|
||||
ifc: type[tool.Ifc],
|
||||
collector: type[tool.Collector],
|
||||
drawing_tool: type[tool.Drawing],
|
||||
geometry: type[tool.Geometry],
|
||||
annotations: list[ifcopenshell.entity_instance],
|
||||
target_drawing: ifcopenshell.entity_instance,
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
"""Duplicate annotations into another drawing, leaving the originals untouched."""
|
||||
target_group = drawing_tool.get_drawing_group(target_drawing)
|
||||
if not target_group:
|
||||
return []
|
||||
annotations = [a for a in annotations if drawing_tool.get_annotation_drawing(a) != target_drawing]
|
||||
annotation_objs = [obj for a in annotations if (obj := ifc.get_object(a))]
|
||||
if not annotation_objs:
|
||||
return []
|
||||
camera = ifc.get_object(target_drawing) or drawing_tool.import_drawing(target_drawing)
|
||||
old_to_new, _ = geometry.duplicate_ifc_objects(annotation_objs)
|
||||
copied: list[ifcopenshell.entity_instance] = []
|
||||
for new_elements in old_to_new.values():
|
||||
for new_element in new_elements:
|
||||
if old_group := drawing_tool.get_drawing_group(new_element):
|
||||
ifc.run("group.unassign_group", group=old_group, products=[new_element])
|
||||
ifc.run("group.assign_group", group=target_group, products=[new_element])
|
||||
new_obj = ifc.get_object(new_element)
|
||||
drawing_tool.ensure_annotation_in_drawing_plane(new_obj, camera)
|
||||
collector.assign(new_obj, should_clean_users_collection=True)
|
||||
copied.append(new_element)
|
||||
return copied
|
||||
|
||||
|
||||
def remove_drawing(
|
||||
ifc: type[tool.Ifc], drawing_tool: type[tool.Drawing], drawing: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
|
||||
@@ -107,15 +107,6 @@ def disable_editing_material(material_tool: type[tool.Material]) -> None:
|
||||
material_tool.disable_editing_material()
|
||||
|
||||
|
||||
def rename_material(
|
||||
ifc: type[tool.Ifc], material_tool: type[tool.Material], material: ifcopenshell.entity_instance, name: str
|
||||
) -> None:
|
||||
ifc.run("material.edit_material", material=material, attributes={"Name": name})
|
||||
if material_tool.is_editing_materials():
|
||||
material_tool.import_material_definitions(material_tool.get_active_material_type())
|
||||
material_tool.refresh()
|
||||
|
||||
|
||||
def assign_material(
|
||||
ifc: type[tool.Ifc],
|
||||
material_tool: type[tool.Material],
|
||||
|
||||
@@ -222,6 +222,7 @@ def generate_space(
|
||||
|
||||
if element and element.is_a("IfcSpace"):
|
||||
spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True)
|
||||
spatial.translate_obj_to_z_location(active_obj, z)
|
||||
else:
|
||||
if relating_type:
|
||||
name = model.generate_occurrence_name(relating_type, "IfcSpace")
|
||||
|
||||
@@ -354,7 +354,6 @@ class Drawing:
|
||||
def enable_editing_schedules(cls): pass
|
||||
def enable_editing_sheets(cls): pass
|
||||
def enable_editing_text(cls, obj): pass
|
||||
def ensure_annotation_in_drawing_plane(cls, obj, camera=None): pass
|
||||
def ensure_drawings_parent_document(cls): pass
|
||||
def ensure_drawings_parent_group(cls): pass
|
||||
def ensure_unique_drawing_name(cls, name): pass
|
||||
@@ -368,7 +367,6 @@ class Drawing:
|
||||
def generate_reference_attributes(cls, reference, **attributes): pass
|
||||
def generate_sheet_identification(cls): pass
|
||||
def get_annotation_context(cls, target_view, object_type=None): pass
|
||||
def get_annotation_drawing(cls, element): pass
|
||||
def get_annotation_representation(cls, element_type): pass
|
||||
def get_assigned_product(cls, element): pass
|
||||
def get_assigned_product_workaround(cls, element): pass
|
||||
@@ -386,7 +384,6 @@ class Drawing:
|
||||
def get_drawing_group(cls, drawing): pass
|
||||
def get_drawing_references(cls, drawing): pass
|
||||
def get_drawing_target_view(cls, drawing): pass
|
||||
def get_group_drawing(cls, group): pass
|
||||
def get_group_elements(cls, group): pass
|
||||
def get_ifc_representation_class(cls, object_type): pass
|
||||
def get_name(cls, element): pass
|
||||
@@ -400,7 +397,6 @@ class Drawing:
|
||||
def get_unit_system(cls): pass
|
||||
def import_assigned_product(cls, obj): pass
|
||||
def import_documents(cls, document_type): pass
|
||||
def import_drawing(cls, drawing): pass
|
||||
def import_drawings(cls): pass
|
||||
def import_sheets(cls): pass
|
||||
def import_text_attributes(cls, obj): pass
|
||||
@@ -667,7 +663,6 @@ class Material:
|
||||
def is_editing_materials(cls): pass
|
||||
def is_material_used_in_sets(cls, material): pass
|
||||
def load_material_attributes(cls, material): pass
|
||||
def refresh(cls): pass
|
||||
def replace_material_with_material_profile(cls, element): pass
|
||||
def update_elements_using_material(cls, material): pass
|
||||
|
||||
|
||||
@@ -803,40 +803,6 @@ class Blender(bonsai.core.tool.Blender):
|
||||
# restore shader editor settings
|
||||
shader_editor.pin = previous_pin_setting
|
||||
|
||||
@classmethod
|
||||
def copy_node_graph_additive(
|
||||
cls, material_to: bpy.types.Material, material_from: bpy.types.Material
|
||||
) -> bpy.types.ShaderNodeOutputMaterial | None:
|
||||
"""Paste nodes from material_from alongside the existing nodes in material_to.
|
||||
|
||||
Unlike copy_node_graph this does NOT clear the existing node tree first.
|
||||
Returns the OUTPUT_MATERIAL node that was added from material_from, or None.
|
||||
"""
|
||||
temp_override = cls.get_shader_editor_context()
|
||||
shader_editor = temp_override["space"]
|
||||
|
||||
before_names = {n.name for n in material_to.node_tree.nodes}
|
||||
|
||||
previous_pin_setting = shader_editor.pin
|
||||
shader_editor.pin = True
|
||||
shader_editor.node_tree = material_from.node_tree
|
||||
|
||||
for node in material_from.node_tree.nodes:
|
||||
node.select = True
|
||||
with bpy.context.temp_override(**temp_override):
|
||||
bpy.ops.node.clipboard_copy()
|
||||
|
||||
shader_editor.node_tree = material_to.node_tree
|
||||
with bpy.context.temp_override(**temp_override):
|
||||
bpy.ops.node.clipboard_paste(offset=(0, 0))
|
||||
|
||||
shader_editor.pin = previous_pin_setting
|
||||
|
||||
for node in material_to.node_tree.nodes:
|
||||
if node.name not in before_names and node.type == "OUTPUT_MATERIAL":
|
||||
return node
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_material_node(
|
||||
cls, blender_material: bpy.types.Material, node_type: str, kwargs: Optional[dict] = {}
|
||||
|
||||
@@ -138,6 +138,7 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
||||
def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]:
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
baseurl = getattr(prefs, "bsdd_baseurl", "https://api.bsdd.buildingsmart.org/api/")
|
||||
cls.client = bsdd.Client()
|
||||
if hasattr(cls.client, "baseurl"):
|
||||
cls.client.baseurl = baseurl
|
||||
response = cls.client.get_dictionary(include_test_dictionaries=prefs.bsdd_load_test_dictionaries)
|
||||
|
||||
@@ -1270,17 +1270,6 @@ class ClipBox:
|
||||
def on_depsgraph_update_caps(cls, scene, depsgraph) -> None:
|
||||
"""Depsgraph entry-point — guard, then delegate to the
|
||||
modal-aware debounce in :meth:`_handle_cap_tick`."""
|
||||
# Same file-load danger window as on_depsgraph_update: a real
|
||||
# depsgraph tick during load (between load_pre and the new file's
|
||||
# first paint) must not re-arm a cap-rebuild timer. _on_load_pre
|
||||
# already cancels any in-flight timer via _cancel_pending_cap_rebuild;
|
||||
# without this gate, a depsgraph_update_post event firing later in
|
||||
# the same load (Blender fires these while building the new file's
|
||||
# scene) would immediately reschedule one via _handle_cap_tick,
|
||||
# undoing that cancellation and re-arming against regions whose GPU
|
||||
# state is not yet wired.
|
||||
if cls._file_loading:
|
||||
return
|
||||
if getattr(bpy.context, "screen", None) is None:
|
||||
return
|
||||
if cls._active_scene_props(scene) is None:
|
||||
|
||||
@@ -756,17 +756,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
|
||||
return rel.RelatingGroup
|
||||
|
||||
@classmethod
|
||||
def get_group_drawing(cls, group: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Get the drawing that owns this group, if the group represents a drawing."""
|
||||
if group.ObjectType != "DRAWING":
|
||||
return None
|
||||
for rel in group.IsGroupedBy or []:
|
||||
for related_object in rel.RelatedObjects:
|
||||
if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING":
|
||||
return related_object
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_drawing_document(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
for rel in drawing.HasAssociations:
|
||||
@@ -2887,50 +2876,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
break
|
||||
return sheet_references
|
||||
|
||||
@classmethod
|
||||
def get_sheeted_drawing_ids(cls) -> set[int]:
|
||||
"""Get the IFC ids of all drawings that are placed on at least one sheet."""
|
||||
ifc_file = tool.Ifc.get()
|
||||
sheet_locations: set[Union[str, None]] = set()
|
||||
for sheet in ifc_file.by_type("IfcDocumentInformation"):
|
||||
if sheet.Scope != "SHEET":
|
||||
continue
|
||||
for reference in cls.get_document_references(sheet):
|
||||
sheet_locations.add(reference.Location)
|
||||
if not sheet_locations:
|
||||
return set()
|
||||
result: set[int] = set()
|
||||
for drawing in ifc_file.by_type("IfcAnnotation"):
|
||||
if drawing.ObjectType != "DRAWING":
|
||||
continue
|
||||
drawing_document = cls.get_drawing_document(drawing)
|
||||
if drawing_document and drawing_document.Location in sheet_locations:
|
||||
result.add(drawing.id())
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_visible_drawings_in_category(cls, target_view: str) -> list[DrawingProperties]:
|
||||
"""Get the drawing items in a target view category that are currently visible in the drawing list.
|
||||
|
||||
Grouping is positional: individual drawing items don't carry their own ``target_view``, they belong to
|
||||
the most recent header item above them. Only expanded categories contribute drawing items to the
|
||||
collection, so a collapsed category yields an empty list. Respects the ``show_drawings_on_sheets_only``
|
||||
filter so that select-all only affects visible drawings.
|
||||
"""
|
||||
props = cls.get_document_props()
|
||||
drawings: list[DrawingProperties] = []
|
||||
in_category = False
|
||||
for item in props.drawings:
|
||||
if not item.is_drawing:
|
||||
# Header row: we're inside the requested category until the next header.
|
||||
in_category = item.target_view == target_view
|
||||
elif in_category:
|
||||
drawings.append(item)
|
||||
if props.show_drawings_on_sheets_only:
|
||||
sheeted_ids = cls.get_sheeted_drawing_ids()
|
||||
drawings = [d for d in drawings if d.ifc_definition_id in sheeted_ids]
|
||||
return drawings
|
||||
|
||||
@classmethod
|
||||
def get_camera_matrix(cls, camera: bpy.types.Object) -> Matrix:
|
||||
matrix_world = camera.matrix_world.copy().normalized()
|
||||
|
||||
@@ -1151,9 +1151,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
settings.set("layerset-first", True)
|
||||
settings.set("keep-bounding-boxes", True)
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
settings.set("mesher-linear-deflection", ifc_import_settings.deflection_tolerance)
|
||||
settings.set("mesher-angular-deflection", ifc_import_settings.angular_tolerance)
|
||||
geometry_library = ifc_import_settings.geometry_library
|
||||
|
||||
ifc_importer = bonsai.bim.import_ifc.IfcImporter(ifc_import_settings)
|
||||
ifc_importer.file = tool.Ifc.get()
|
||||
@@ -1165,11 +1162,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
shape = None
|
||||
if elements:
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
settings,
|
||||
tool.Ifc.get(),
|
||||
multiprocessing.cpu_count(),
|
||||
include=elements,
|
||||
geometry_library=geometry_library,
|
||||
settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=elements
|
||||
)
|
||||
else:
|
||||
iterator = None # For example, when switching representation of a type with no occurrences
|
||||
@@ -1224,9 +1217,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
for element in element_types:
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
||||
geometry = ifcopenshell.geom.create_shape(
|
||||
settings, representation, geometry_library=geometry_library
|
||||
)
|
||||
geometry = ifcopenshell.geom.create_shape(settings, representation)
|
||||
mesh_name = tool.Loader.get_mesh_name_from_shape(geometry)
|
||||
mesh = meshes.get(mesh_name)
|
||||
if mesh is None:
|
||||
|
||||
@@ -146,12 +146,6 @@ class Material(bonsai.core.tool.Material):
|
||||
|
||||
MaterialsData.data["material_styles_data"] = MaterialsData.material_styles_data()
|
||||
|
||||
@classmethod
|
||||
def refresh(cls) -> None:
|
||||
from bonsai.bim.module.material.data import refresh as refresh_material_data
|
||||
|
||||
refresh_material_data()
|
||||
|
||||
@classmethod
|
||||
def is_editing_materials(cls) -> bool:
|
||||
props = tool.Material.get_material_props()
|
||||
|
||||
@@ -2292,18 +2292,32 @@ class Model(bonsai.core.tool.Model):
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
|
||||
# Sanity check
|
||||
group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
if deform_layer:
|
||||
for vert in bm.verts:
|
||||
vert_group_indices = tool.Blender.bmesh_get_vertex_groups(vert, deform_layer)
|
||||
is_circle = any(gi in groups["IFCCIRCLE"] for gi in vert_group_indices)
|
||||
is_arc = any(gi in groups["IFCARCINDEX"] for gi in vert_group_indices)
|
||||
if (is_circle or is_arc) and not vert.link_edges:
|
||||
return (False, "CIRCLE" if is_circle else "3POINT_ARC")
|
||||
is_circle = False
|
||||
for group_index in vert_group_indices:
|
||||
group_type = "IFCARCINDEX" if group_index in groups["IFCARCINDEX"] else "IFCCIRCLE"
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
if is_circle:
|
||||
pass # Circles are allowed to be unclosed
|
||||
elif len(vert.link_edges) != 2: # Unclosed loop or forked loop
|
||||
return (False, "UNCLOSED_LOOP")
|
||||
|
||||
for group_type, group_counts in group_verts.items():
|
||||
if group_type == "IFCARCINDEX":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
elif group_type == "IFCCIRCLE":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
loop_edges = list(bm.edges)
|
||||
|
||||
# Create loops from edges
|
||||
@@ -2326,28 +2340,6 @@ class Model(bonsai.core.tool.Model):
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
# Sanity check, per loop rather than across the whole mesh
|
||||
if deform_layer:
|
||||
for loop in loops:
|
||||
loop_group_counts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
loop_verts = {v for edge in loop for v in edge.verts}
|
||||
for vert in loop_verts:
|
||||
for group_index in tool.Blender.bmesh_get_vertex_groups(vert, deform_layer):
|
||||
if group_index in groups["IFCARCINDEX"]:
|
||||
group_type = "IFCARCINDEX"
|
||||
elif group_index in groups["IFCCIRCLE"]:
|
||||
group_type = "IFCCIRCLE"
|
||||
else:
|
||||
continue
|
||||
loop_group_counts[group_type].setdefault(group_index, 0)
|
||||
loop_group_counts[group_type][group_index] += 1
|
||||
for group_count in loop_group_counts["IFCARCINDEX"].values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
for group_count in loop_group_counts["IFCCIRCLE"].values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
|
||||
|
||||
def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool:
|
||||
@@ -2528,11 +2520,27 @@ class Model(bonsai.core.tool.Model):
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
|
||||
# Sanity check
|
||||
group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
if deform_layer:
|
||||
for vert in bm.verts:
|
||||
vert_group_indices = tool.Blender.bmesh_get_vertex_groups(vert, deform_layer)
|
||||
for group_index in vert_group_indices:
|
||||
group_type = "IFCARCINDEX" if group_index in groups["IFCARCINDEX"] else "IFCCIRCLE"
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
if len(vert.link_edges) > 2: # Forked loop
|
||||
return (False, "FORKED_LOOP")
|
||||
|
||||
for group_type, group_counts in group_verts.items():
|
||||
if group_type == "IFCARCINDEX":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
elif group_type == "IFCCIRCLE":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
loop_edges = list(bm.edges)
|
||||
|
||||
# Create loops from edges
|
||||
@@ -2555,28 +2563,6 @@ class Model(bonsai.core.tool.Model):
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
# Sanity check, per loop rather than across the whole mesh
|
||||
if deform_layer:
|
||||
for loop in loops:
|
||||
loop_group_counts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
loop_verts = {v for edge in loop for v in edge.verts}
|
||||
for vert in loop_verts:
|
||||
for group_index in tool.Blender.bmesh_get_vertex_groups(vert, deform_layer):
|
||||
if group_index in groups["IFCARCINDEX"]:
|
||||
group_type = "IFCARCINDEX"
|
||||
elif group_index in groups["IFCCIRCLE"]:
|
||||
group_type = "IFCCIRCLE"
|
||||
else:
|
||||
continue
|
||||
loop_group_counts[group_type].setdefault(group_index, 0)
|
||||
loop_group_counts[group_type][group_index] += 1
|
||||
for group_count in loop_group_counts["IFCARCINDEX"].values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
for group_count in loop_group_counts["IFCCIRCLE"].values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
|
||||
|
||||
def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool:
|
||||
|
||||
@@ -47,15 +47,7 @@ class Nest(bonsai.core.tool.Nest):
|
||||
return False
|
||||
if relating_object == related_object:
|
||||
return False
|
||||
# IfcRelNests.RelatingObject/RelatedObjects are typed as the general
|
||||
# IfcObjectDefinition, so nesting is schema-legal both between element
|
||||
# occurrences (the common case, e.g. a faucet nested into a sink) and
|
||||
# between element types (e.g. an assembly type nesting its component
|
||||
# types). Mixing an occurrence with a type isn't a real modeling
|
||||
# pattern, so only allow same-kind pairs. See #2283.
|
||||
is_compatible_class = (relating_object.is_a("IfcElement") and related_object.is_a("IfcElement")) or (
|
||||
relating_object.is_a("IfcTypeProduct") and related_object.is_a("IfcTypeProduct")
|
||||
)
|
||||
is_compatible_class = relating_object.is_a("IfcElement") and related_object.is_a("IfcElement")
|
||||
if not is_compatible_class:
|
||||
return False
|
||||
# Prevent cyclic references: walk up the full hierarchy from the
|
||||
|
||||
@@ -168,12 +168,12 @@ class Polyline(bonsai.core.tool.Polyline):
|
||||
distance = (mouse_vector - last_point).length
|
||||
if distance < 0:
|
||||
return
|
||||
angle, orientation_angle = None, None
|
||||
angle_round_threshold = 1000 # Avoids rounding when distance is too big
|
||||
angle, orientation_angle, angle_round_threshold = None, None
|
||||
if distance > 0:
|
||||
angle = tool.Cad.angle_3_vectors(
|
||||
second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
|
||||
)
|
||||
angle_round_threshold = 1000 # Avoids rounding when distance is too big
|
||||
|
||||
# Round angle to the nearest 0.05
|
||||
angle = round(angle / 0.05) * 0.05 if distance < angle_round_threshold else angle
|
||||
@@ -189,7 +189,7 @@ class Polyline(bonsai.core.tool.Polyline):
|
||||
angle = 0
|
||||
orientation_angle = 0
|
||||
if input_ui:
|
||||
assert angle is not None and orientation_angle is not None
|
||||
assert angle is not None and orientation_angle is not None and angle_round_threshold is not None
|
||||
if should_round:
|
||||
angle_snap = tool.Snap.get_angle_snap_value(context)
|
||||
angle = angle_snap * round(angle / angle_snap) if distance < angle_round_threshold else angle
|
||||
|
||||
@@ -391,14 +391,29 @@ class Project(bonsai.core.tool.Project):
|
||||
def get_parent_library(
|
||||
cls, project_library: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Return the IfcContext that declares or nests ``project_library``, or ``None``
|
||||
if neither relationship is present."""
|
||||
"""Return the IfcContext that declares or nests ``project_library``.
|
||||
|
||||
Returns ``None`` when ``project_library`` is itself the root of a
|
||||
library-only file (no IfcRelNests, no IfcRelDeclares).
|
||||
"""
|
||||
if nests := project_library.Nests:
|
||||
return nests[0].RelatingObject
|
||||
if has_context := project_library.HasContext:
|
||||
return has_context[0].RelatingContext
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_root_context(cls, ifc_file: ifcopenshell.file) -> ifcopenshell.entity_instance:
|
||||
"""Return the file's root IfcContext.
|
||||
|
||||
Prefers IfcProject if present, otherwise falls back to IfcProjectLibrary —
|
||||
library-only files are valid per IFC4+ and contain no IfcProject. Caller is
|
||||
responsible for the IFC2X3 guard; IfcContext does not exist in that schema.
|
||||
"""
|
||||
if projects := ifc_file.by_type("IfcProject"):
|
||||
return projects[0]
|
||||
return ifc_file.by_type("IfcProjectLibrary")[0]
|
||||
|
||||
@classmethod
|
||||
def get_project_hierarchy(cls, ifc_file: ifcopenshell.file) -> HiearchyDict:
|
||||
"""Get project hierarchy in the following form:
|
||||
|
||||
+33
-147
@@ -26,7 +26,6 @@ import ifcopenshell
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
import bonsai.bim.helper
|
||||
import bonsai.bim.schema
|
||||
@@ -169,144 +168,42 @@ class Pset(bonsai.core.tool.Pset):
|
||||
pset_id=0, pset_name=cls.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
|
||||
)
|
||||
|
||||
# Templates for quantities can specify their kind via TemplateType (e.g.
|
||||
# "Q_LENGTH") instead of PrimaryMeasureType. IfcQuantityCount has no
|
||||
# associated measure/unit, so it is intentionally absent here.
|
||||
QUANTITY_TEMPLATE_TYPE_TO_SPECIAL_TYPE = {
|
||||
"Q_LENGTH": "LENGTH",
|
||||
"Q_AREA": "AREA",
|
||||
"Q_VOLUME": "VOLUME",
|
||||
"Q_WEIGHT": "MASS",
|
||||
"Q_TIME": "TIME",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_special_type_for_measure_class(cls, measure_class: str) -> str:
|
||||
"""Get the ``special_type`` (an IfcUnitEnum value with "UNIT" stripped) for an IFC measure class.
|
||||
|
||||
:param measure_class: An IFC measure class name, e.g. "IfcLengthMeasure".
|
||||
:return: E.g. "LENGTH", or "" if the class has no associated unit type.
|
||||
"""
|
||||
if not measure_class.endswith("Measure"):
|
||||
return ""
|
||||
unit_type = ifcopenshell.util.unit.get_measure_unit_type(measure_class)
|
||||
return unit_type[: -len("UNIT")] if unit_type.endswith("UNIT") else ""
|
||||
|
||||
@classmethod
|
||||
def get_special_type_for_unit(cls, unit: ifcopenshell.entity_instance) -> str:
|
||||
"""Get the ``special_type`` (an IfcUnitEnum value with "UNIT" stripped) directly from
|
||||
a Unit entity, for properties whose NominalValue is a generic numeric type (e.g.
|
||||
IfcReal) rather than a proper measure class, but which still carry a real Unit.
|
||||
"""
|
||||
unit_type = getattr(unit, "UnitType", None)
|
||||
if unit_type and unit_type != "USERDEFINED":
|
||||
return unit_type[: -len("UNIT")] if unit_type.endswith("UNIT") else ""
|
||||
dimension_type = ifcopenshell.util.unit.identify_unit_dimensions(unit)
|
||||
return dimension_type[: -len("UNIT")] if dimension_type else ""
|
||||
|
||||
@classmethod
|
||||
def get_special_type_for_prop(cls, prop_or_prop_template: ifcopenshell.entity_instance) -> str:
|
||||
"""Classify a property/quantity/template by its measure type.
|
||||
|
||||
:return: An IfcUnitEnum value with the "UNIT" suffix stripped (e.g.
|
||||
"LENGTH", "PRESSURE"), "URI" for IfcURIReference, or "" if the
|
||||
value has no associated unit type.
|
||||
"""
|
||||
def get_special_type_for_prop(
|
||||
cls, prop_or_prop_template: ifcopenshell.entity_instance
|
||||
) -> Literal["LENGTH"] | Literal["AREA"] | Literal["VOLUME"] | Literal["URI"] | Literal[""]:
|
||||
special_type = ""
|
||||
if prop_or_prop_template.is_a("IfcPropertyTemplate"):
|
||||
primary_measure_type = prop_or_prop_template.PrimaryMeasureType
|
||||
if primary_measure_type == "IfcURIReference":
|
||||
return "URI"
|
||||
if primary_measure_type:
|
||||
return cls.get_special_type_for_measure_class(primary_measure_type)
|
||||
return cls.QUANTITY_TEMPLATE_TYPE_TO_SPECIAL_TYPE.get(prop_or_prop_template.TemplateType, "")
|
||||
elif prop_or_prop_template.is_a("IfcPropertySingleValue"):
|
||||
value = prop_or_prop_template.NominalValue
|
||||
if value is not None:
|
||||
special_type = cls.get_special_type_for_measure_class(value.is_a())
|
||||
if special_type:
|
||||
return special_type
|
||||
# Some property sets declare a generic numeric type (e.g. IfcReal) rather
|
||||
# than a proper measure class, relying on an explicit Unit attribute alone to
|
||||
# convey the dimension. Still measurable -- derive special_type from the Unit
|
||||
# itself rather than (fruitlessly) from NominalValue's declared type.
|
||||
if value.is_a() in ("IfcReal", "IfcInteger"):
|
||||
if unit := getattr(prop_or_prop_template, "Unit", None):
|
||||
return cls.get_special_type_for_unit(unit)
|
||||
elif prop_or_prop_template.is_a("IfcPhysicalSimpleQuantity"):
|
||||
entity = prop_or_prop_template.wrapped_data.declaration().as_entity()
|
||||
measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name()
|
||||
return cls.get_special_type_for_measure_class(measure_class)
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def get_unit_symbol_for_special_type(cls, special_type: str, ifc_file: ifcopenshell.file) -> str:
|
||||
"""Get the project's default unit symbol for a `special_type` (see `get_special_type_for_prop`).
|
||||
|
||||
Used where there's no property instance to check for a `Unit` override
|
||||
(e.g. a template, or a native IFC entity attribute, neither of which
|
||||
can carry one).
|
||||
"""
|
||||
if not special_type or special_type == "URI":
|
||||
return ""
|
||||
unit = ifcopenshell.util.unit.get_project_unit(ifc_file, f"{special_type}UNIT")
|
||||
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
|
||||
|
||||
@classmethod
|
||||
def get_unit_symbol_for_prop(cls, prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file) -> str:
|
||||
"""Get the unit symbol for an existing property/quantity, respecting its own `Unit` override.
|
||||
|
||||
Gated on the property being classified as measurable (see `get_special_type_for_prop`,
|
||||
which already accounts for a Unit attached to a generic numeric value) -- this only
|
||||
excludes a Unit attached to a property whose value has no numeric/measure semantics at
|
||||
all (e.g. text), where a stray Unit shouldn't be surfaced as a resolved unit.
|
||||
"""
|
||||
if not cls.is_measurable_special_type(cls.get_special_type_for_prop(prop)):
|
||||
return ""
|
||||
unit = ifcopenshell.util.unit.get_property_unit(prop, ifc_file)
|
||||
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
|
||||
|
||||
# special_type values that don't denote a real unit-bearing measure (see get_special_type_for_prop).
|
||||
NON_MEASURABLE_SPECIAL_TYPES = frozenset({"", "DATE", "DATETIME", "LOGICAL", "URI", "DURATION"})
|
||||
|
||||
@classmethod
|
||||
def is_measurable_special_type(cls, special_type: str) -> bool:
|
||||
"""True if `special_type` (see `get_special_type_for_prop`) denotes a real unit-bearing measure."""
|
||||
return special_type not in cls.NON_MEASURABLE_SPECIAL_TYPES
|
||||
|
||||
@classmethod
|
||||
def get_candidate_units_for_special_type(
|
||||
cls, special_type: str, ifc_file: ifcopenshell.file
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
"""All units in the file usable as an override for a `special_type` (see `get_special_type_for_prop`)."""
|
||||
if not cls.is_measurable_special_type(special_type):
|
||||
return []
|
||||
return ifcopenshell.util.unit.get_candidate_units(ifc_file, f"{special_type}UNIT")
|
||||
|
||||
@classmethod
|
||||
def resolve_effective_unit(
|
||||
cls, special_type: str, unit_id: int, ifc_file: ifcopenshell.file
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""The unit a value is currently expressed in: its own override (`unit_id`, a STEP id,
|
||||
0 meaning "no override"), or the project default for `special_type` otherwise."""
|
||||
if unit_id:
|
||||
return ifc_file.by_id(unit_id)
|
||||
return ifcopenshell.util.unit.get_project_unit(ifc_file, f"{special_type}UNIT")
|
||||
|
||||
@classmethod
|
||||
def convert_attribute_unit(cls, metadata: "Attribute", new_unit_id: int, ifc_file: ifcopenshell.file) -> None:
|
||||
"""Rescale `metadata.float_value` in place so its physical quantity is preserved when
|
||||
switching from its current effective unit to the unit named by `new_unit_id` (0 = project
|
||||
default). No-op for non-measurable attributes or when old and new resolve to the same unit.
|
||||
"""
|
||||
if not cls.is_measurable_special_type(metadata.special_type):
|
||||
return
|
||||
old_unit = cls.resolve_effective_unit(metadata.special_type, metadata.unit_id, ifc_file)
|
||||
new_unit = cls.resolve_effective_unit(metadata.special_type, new_unit_id, ifc_file)
|
||||
if old_unit is None or new_unit is None or old_unit == new_unit:
|
||||
return
|
||||
old_scale = ifcopenshell.util.unit.get_unit_scale(old_unit)
|
||||
new_scale = ifcopenshell.util.unit.get_unit_scale(new_unit)
|
||||
metadata.float_value = metadata.float_value * old_scale / new_scale
|
||||
template_type = prop_or_prop_template.TemplateType
|
||||
if primary_measure_type in ("IfcPositiveLengthMeasure", "IfcLengthMeasure") or template_type == "Q_LENGTH":
|
||||
special_type = "LENGTH"
|
||||
elif primary_measure_type == "IfcAreaMeasure" or template_type == "Q_AREA":
|
||||
special_type = "AREA"
|
||||
elif primary_measure_type == "IfcVolumeMeasure" or template_type == "Q_VOLUME":
|
||||
special_type = "VOLUME"
|
||||
elif primary_measure_type == "IfcURIReference":
|
||||
special_type = "URI"
|
||||
else:
|
||||
if prop_or_prop_template.is_a("IfcPropertySingleValue"):
|
||||
value = prop_or_prop_template.NominalValue
|
||||
if value is not None:
|
||||
value_type = value.is_a()
|
||||
if value_type in ("IfcLengthMeasure", "IfcPositiveLengthMeasure"):
|
||||
special_type = "LENGTH"
|
||||
elif value_type == "IfcAreaMeasure":
|
||||
special_type = "AREA"
|
||||
elif value_type == "IfcVolumeMeasure":
|
||||
special_type = "VOLUME"
|
||||
elif prop_or_prop_template.is_a("IfcPhysicalSimpleQuantity"):
|
||||
prop_class = prop_or_prop_template.is_a()
|
||||
if prop_class == "IfcQuantityArea":
|
||||
special_type = "AREA"
|
||||
elif prop_class == "IfcQuantityVolume":
|
||||
special_type = "VOLUME"
|
||||
elif prop_class == "IfcQuantityLength":
|
||||
special_type = "LENGTH"
|
||||
return special_type
|
||||
|
||||
@classmethod
|
||||
def import_pset_from_existing(
|
||||
@@ -386,15 +283,6 @@ class Pset(bonsai.core.tool.Pset):
|
||||
metadata.is_null = value is None
|
||||
metadata.is_optional = True
|
||||
metadata.special_type = cls.get_special_type_for_prop(prop)
|
||||
# The prop's OWN Unit override only -- metadata.unit_symbol is computed fresh from
|
||||
# special_type/unit_id on every access (see Attribute.get_unit_symbol), so it
|
||||
# already accounts for the project-default fallback once unit_id is set below.
|
||||
# Some real-world files (e.g. certain exporters) set Unit on properties that
|
||||
# aren't actually measures -- ignore it there, since we only ever treat Unit as
|
||||
# meaningful for measurable special_types (matching the UI picker's own gating).
|
||||
own_unit = getattr(prop, "Unit", None) if cls.is_measurable_special_type(metadata.special_type) else None
|
||||
metadata.unit_id = own_unit.id() if own_unit else 0
|
||||
metadata.unit_id_enum = str(metadata.unit_id)
|
||||
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
|
||||
process_prop_description(metadata)
|
||||
|
||||
@@ -521,8 +409,6 @@ class Pset(bonsai.core.tool.Pset):
|
||||
cls.import_single_value_from_template(pset_template, prop_template, simplified_data, props)
|
||||
|
||||
elif prop_template.TemplateType.startswith("Q_"):
|
||||
if prop_data:
|
||||
continue # Existing quantity will be added later by import_pset_from_existing.
|
||||
cls.import_single_value_from_template(pset_template, prop_template, simplified_data, props)
|
||||
|
||||
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user