mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ece3790aa | |||
| 6585f0ee2b | |||
| 4ec042595e | |||
| 48f6e2908b | |||
| 1cd7e52c49 | |||
| 2e6f17ed0f |
@@ -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 }}
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
|
||||
|
||||
# Download Blender.
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.2/blender-5.2.0-linux-x64.tar.xz
|
||||
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.1/blender-5.1.0-linux-x64.tar.xz
|
||||
tar -xf blender.tar.xz
|
||||
|
||||
# Setup Blender.
|
||||
@@ -179,7 +179,8 @@ jobs:
|
||||
blender --online-mode --command extension install --enable --sync sun_position
|
||||
|
||||
cd IfcOpenShell/src/bonsai
|
||||
pip install -r requirements-dev.txt
|
||||
pip install pytest-blender
|
||||
pip install pytest-bdd
|
||||
blender --background --python scripts/setup_pytest.py
|
||||
blender --python-expr "import bonsai; print(bonsai.bbim_semver); import ifcopenshell; print(ifcopenshell.version)" --background
|
||||
make test
|
||||
|
||||
@@ -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,22 +12,25 @@ 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 }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
cat requirements-tools.txt | xargs -L1 uv tool install
|
||||
uv tool install ruff
|
||||
uv tool install black
|
||||
uv tool install poethepoet
|
||||
uv tool install ty==0.0.34
|
||||
|
||||
# black doesn't catch all syntax errors, so we check them explicitly.
|
||||
- name: Check syntax errors
|
||||
@@ -55,17 +58,11 @@ jobs:
|
||||
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
|
||||
continue-on-error: true
|
||||
|
||||
- name: ty check (venv setup)
|
||||
run: poe ty-venv
|
||||
|
||||
- name: ty check (bonsai)
|
||||
id: ty-bonsai
|
||||
run: poe ty-bonsai
|
||||
continue-on-error: true
|
||||
|
||||
- name: ty check (ios)
|
||||
id: ty-ios
|
||||
run: poe ty-ios
|
||||
- name: ty check
|
||||
id: ty
|
||||
run: |
|
||||
poe ty-venv
|
||||
poe ty
|
||||
continue-on-error: true
|
||||
|
||||
- name: Ruff check
|
||||
@@ -115,10 +112,7 @@ jobs:
|
||||
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
|
||||
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
|
||||
fi
|
||||
if [ "${{ steps.ty-bonsai.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check (bonsai) failed, see 'ty check (bonsai)' step for the details." && ERROR=1
|
||||
fi
|
||||
if [ "${{ steps.ty-ios.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check (ios) failed, see 'ty check (ios)' step for the details." && ERROR=1
|
||||
if [ "${{ steps.ty.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check failed, see 'ty check' step for the details." && ERROR=1
|
||||
fi
|
||||
exit $ERROR
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
/_deps-vs*-x*-installed/
|
||||
/_installed-vs*-x*/
|
||||
/build/
|
||||
/build.log
|
||||
/output/
|
||||
/src/examples/build/
|
||||
# ifctester docs output
|
||||
/src/ifctester/test/build/
|
||||
@@ -24,7 +22,6 @@
|
||||
__pycache__
|
||||
*.py.bak
|
||||
venv
|
||||
uv.lock
|
||||
|
||||
# Visual Studio Code files
|
||||
.vscode
|
||||
@@ -130,7 +127,6 @@ src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||
|
||||
# temp files from AI coding tools
|
||||
*.claude
|
||||
CLAUDE.local.md
|
||||
*.py.tmp*
|
||||
*.json.tmp*
|
||||
|
||||
|
||||
@@ -129,6 +129,21 @@ on CI to catch formatting issues.
|
||||
within each package under `src/`.
|
||||
- Run the existing test suite for the package you modified before submitting.
|
||||
|
||||
## In-Progress Feature Notes
|
||||
|
||||
Living design and working notes for unmerged feature branches live in
|
||||
[`docs/dev-notes/`](docs/dev-notes/), one Markdown file per feature, named after the
|
||||
branch. They capture the problem, the design decisions and the *why*, and what still
|
||||
needs testing — so collaborators (and their AI agents) can pick up the context behind a
|
||||
branch. Because the note is committed on the branch, it travels with the PR.
|
||||
|
||||
- Before working on a feature branch, read its note in `docs/dev-notes/` if one exists.
|
||||
- Keep the note current as the PR is refined.
|
||||
- These are not user documentation; at merge they are removed or their durable parts
|
||||
promoted to code comments / permanent docs.
|
||||
|
||||
See [`docs/dev-notes/README.md`](docs/dev-notes/README.md) for details.
|
||||
|
||||
## Architecture Quick Reference
|
||||
|
||||
### Directory Structure
|
||||
|
||||
@@ -18,7 +18,7 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil
|
||||
|
||||
For more information, see:
|
||||
|
||||
* [IfcOpenShell Website](https://ifcopenshell.org)
|
||||
* [IfcOpenShell Website](http://ifcopenshell.org)
|
||||
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
|
||||
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
|
||||
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+11
-21
@@ -27,14 +27,13 @@ endif()
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# The VERSION file in the repository root is the single source of truth for the
|
||||
# release version. Read it unconditionally so a plain source build reports the
|
||||
# real version through buildinfo.cpp instead of the stale hardcoded 0.8.0
|
||||
# fallback (see #8164). VERSION_OVERRIDE still controls the branch name used
|
||||
# when ADD_COMMIT_SHA embeds a commit sha.
|
||||
file(READ "../VERSION" "RELEASE_VERSION_")
|
||||
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
|
||||
message(STATUS "Detected version '${RELEASE_VERSION}'")
|
||||
if(VERSION_OVERRIDE)
|
||||
file(READ "../VERSION" "RELEASE_VERSION_")
|
||||
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
|
||||
message(STATUS "Detected version '${RELEASE_VERSION}'")
|
||||
else()
|
||||
set(RELEASE_VERSION "0.8.0")
|
||||
endif()
|
||||
|
||||
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
|
||||
|
||||
@@ -314,12 +313,8 @@ if(WASM_BUILD)
|
||||
else()
|
||||
# @todo review this, shouldn't this be all possible header-only now?
|
||||
# ... or rewritten using C++17 features?
|
||||
# Boost.System has been header-only since 1.69 and its compiled stub library
|
||||
# was dropped in newer Boost, so requesting it as a component makes
|
||||
# find_package fail on Boost 1.70 and up (for example Boost 1.90). It is
|
||||
# still pulled in transitively by thread / iostreams where needed, so do not
|
||||
# request it explicitly.
|
||||
set(BOOST_COMPONENTS
|
||||
system
|
||||
program_options
|
||||
regex
|
||||
thread
|
||||
@@ -563,8 +558,8 @@ if(COMPILE_SCHEMA)
|
||||
# Bootstrap the parser
|
||||
message(STATUS "Compiling schema, this will take a while...")
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py
|
||||
WORKING_DIRECTORY ../src/ifcopenshell-python/ifcopenshell/express
|
||||
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py express.bnf
|
||||
WORKING_DIRECTORY ../src/ifcexpressparser
|
||||
OUTPUT_FILE express_parser.py
|
||||
RESULT_VARIABLE SUCCESS
|
||||
)
|
||||
@@ -575,7 +570,7 @@ if(COMPILE_SCHEMA)
|
||||
|
||||
# Generate code
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} ../ifcopenshell-python/ifcopenshell/express/express_parser.py ../../${COMPILE_SCHEMA}
|
||||
COMMAND ${PYTHON_EXECUTABLE} ../ifcexpressparser/express_parser.py ../../${COMPILE_SCHEMA}
|
||||
WORKING_DIRECTORY ../src/ifcparse
|
||||
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME
|
||||
)
|
||||
@@ -665,11 +660,6 @@ if(ADD_COMMIT_SHA)
|
||||
endif()
|
||||
endif(ADD_COMMIT_SHA)
|
||||
|
||||
# Always expose the release version (from the VERSION file) to buildinfo.cpp so
|
||||
# that a build without commit-sha info reports the correct version instead of a
|
||||
# stale hardcoded fallback. See #8164.
|
||||
target_compile_definitions(IfcParse PRIVATE IFCOPENSHELL_VERSION_STRING=${RELEASE_VERSION})
|
||||
|
||||
if(MSVC)
|
||||
# @todo still needs to be understood better, but the cgal and cgal-simple kernel cause multiply defined boost lambda placeholders _1 ... _3
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.env
|
||||
*.pyc
|
||||
__pycache__
|
||||
@@ -1,3 +0,0 @@
|
||||
.env
|
||||
*.pyc
|
||||
__pycache__
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# .ifcos_env
|
||||
# register autocompletes. just source the file in your shell, i.e.
|
||||
# source .ifcos_env
|
||||
|
||||
.ifcos_env() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
opts="create update up down restart build attach logs ps config remove help"
|
||||
|
||||
# Basic static completion
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Register the completion for the command "ifcos_env"
|
||||
complete -F .ifcos_env ./ifcos_env
|
||||
@@ -1,67 +0,0 @@
|
||||
FROM rockylinux:9
|
||||
|
||||
# Update system, enable CRB (needed by some EPEL packages) and install EPEL,
|
||||
# then install required packages + some common tools for a bit of command
|
||||
# line comfort. Combined into one layer so a later `create` always installs
|
||||
# against packages from the same dnf update, rather than layering fresh
|
||||
# installs on top of a stale cached "update" layer.
|
||||
RUN dnf update -y && \
|
||||
dnf install -y epel-release && \
|
||||
dnf config-manager --set-enabled crb && \
|
||||
dnf install -y --allowerasing --setopt=install_weak_deps=False --setopt=tsflags=nodocs \
|
||||
bash-completion vim git curl wget which tree htop sudo \
|
||||
gcc gcc-c++ autoconf automake bison make zip cmake \
|
||||
python3 python3-pip \
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libuuid-devel git-lfs \
|
||||
findutils xz byacc ccache && \
|
||||
git lfs install --system && \
|
||||
dnf clean all && \
|
||||
rm -rf /var/cache/dnf
|
||||
|
||||
# Trust bind-mounted repos regardless of which user (root or builder) or host
|
||||
# UID owns them, rather than a per-user config that only one of them sees.
|
||||
RUN git config --system --add safe.directory '*'
|
||||
|
||||
# Configure ccache. CCACHE_MAXSIZE (not `ccache -M`) because /ccache is a
|
||||
# volume mount point at runtime - anything `ccache -M` writes to a config
|
||||
# file under it during this build gets shadowed once the real volume is
|
||||
# mounted, so the size cap only actually takes effect via the env var.
|
||||
# 2G is generous: a full build (IfcParse+IfcGeom+IfcConvert+wrapper, one
|
||||
# Python version) measures ~300MB, and the volume is now shared across all
|
||||
# checkouts (see compose.yaml), so this covers several diverging branches.
|
||||
ENV CCACHE_DIR=/ccache
|
||||
ENV CCACHE_MAXSIZE=2G
|
||||
ENV PATH="/usr/lib/ccache:$PATH"
|
||||
|
||||
# Non-root user matching the host UID/GID that bind-mounts the repo (default
|
||||
# 1000:1000, the common single-user-Linux-box case), so files the build
|
||||
# creates under the mount keep sane, non-root ownership on the host side.
|
||||
# Override with --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g)
|
||||
# if your host user has a different UID/GID.
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=1000
|
||||
# groupadd fails outright if USER_GID is already taken by an existing
|
||||
# system group - which happens whenever a host's primary GID collides with
|
||||
# one baked into the rockylinux9 base image. The main real-world case is
|
||||
# macOS, where the default user's primary group is "staff" at GID 20, and
|
||||
# GID 20 is "games" on RHEL-family images. Only create the "builder" group
|
||||
# when that GID is actually free; otherwise useradd just attaches to
|
||||
# whichever group already owns it. Either way the builder user ends up
|
||||
# with the right GID for bind-mount ownership, which is all that matters.
|
||||
RUN (getent group "${USER_GID}" >/dev/null || groupadd -g "${USER_GID}" builder) \
|
||||
&& useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash builder \
|
||||
&& echo "builder ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/builder
|
||||
|
||||
# Copied while still root: /bin is not writable by the builder user.
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.11.27 /uv /uvx /bin/
|
||||
|
||||
USER builder
|
||||
WORKDIR /__w/IfcOpenShell/IfcOpenShell
|
||||
|
||||
# Installed as builder so managed Python interpreters land under builder's
|
||||
# $HOME, matching the user that actually runs the build.
|
||||
RUN uv python install
|
||||
|
||||
CMD ["sleep", "infinity"]
|
||||
@@ -1,78 +0,0 @@
|
||||
Docker build environment
|
||||
========================
|
||||
|
||||
This is a small utility to make it easy to compile a perfect `_ifcopenshell_wrapper.cpython-*-x86_64-linux-gnu.so`
|
||||
files.
|
||||
|
||||
The reason for this tool is that I was trying to follow the web page directions, and my build was behaving differently
|
||||
to the release builds. Eventually I concluded that the differences between toolchains on the RHEL based rocky9 image
|
||||
and Ubuntu were just too great. Getting the build setup was already a lot of trial and error, so I thought I'd spend
|
||||
more time trying to reuse the github actions that perform the build, using a utility called `act`. I learnt a lot, in
|
||||
particular how much time, energy, and bandwidth Github waste. I also realised I was most of the way to a regular docker
|
||||
setup anyway, so I might as well just do that. So I've deconstructed all the github action steps, and turned it into
|
||||
a local docker build environment that uses the exact same base, tools, libraries, and build command/flags etc.
|
||||
|
||||
Right now a Github action will:
|
||||
- launch the rocky9 base
|
||||
- upgrade all the packages
|
||||
- install a bunch of extra tools
|
||||
- do a recursive checkout of your repo
|
||||
- checkout the build repository
|
||||
- unpack dependencies
|
||||
- run the build script, making all python versions (5? right now I think)
|
||||
- create the .zip release files
|
||||
|
||||
And it does _all_ of that _every_ time. This is not a fault of the action writers - it's just how Github seems to work.
|
||||
|
||||
These dockers tools do the following differently, and it's actually a bit more powerful too:
|
||||
- build the base image once.
|
||||
- update the packages once.
|
||||
- install the extra tools once.
|
||||
- the repository is the one on your host, that gets bind mounted in the container as the working directory.
|
||||
- by adding an environment variable to .env, restricts to compiling for just a single python version.
|
||||
- when the build is finished the created files are right there under your local repositry (but not added to git) for
|
||||
ease of access
|
||||
- each repository can have it's own build environment container.
|
||||
- the image is shared between those environments.
|
||||
- the containers share the ccache, so additional envs should get a helping hand.
|
||||
- it has a simple set of user friendly commands to drive it all.
|
||||
|
||||
For example:
|
||||
``` bash
|
||||
# To see the commands (a superset of docker compose commands)
|
||||
./ifcos_env
|
||||
|
||||
# Enable autocomplete of commands
|
||||
source .ifcos_env
|
||||
|
||||
# First time commands
|
||||
./ifcos_env create
|
||||
./ifcos_env up
|
||||
./ifcos_env build
|
||||
|
||||
# install and test library
|
||||
# find an issue
|
||||
# edit code
|
||||
./ifcos_env build
|
||||
|
||||
# and so on. When done stop and optionally delete the container
|
||||
./ifcos_env stop
|
||||
./ifcos_env remove
|
||||
```
|
||||
|
||||
To limit the build to one python version just add
|
||||
``` bash
|
||||
PY_TGT=py-311
|
||||
```
|
||||
or whichever version your Blender requires.
|
||||
|
||||
You might see UNIQUE_ID in the .env file too. This keeps containers for separate folders, separate.
|
||||
|
||||
System requirements
|
||||
1. Linux-x64 only at this time.
|
||||
2. Docker and docker-compose need to be installed.
|
||||
3. Have a good amount of disk space. (image is in /var (typically the root partition) and will be about 1.7 GB)
|
||||
4. The build action will create about 10GB in your repository folder. Make sure this partition is spacious
|
||||
particularly if you intent on having multiple clones building.
|
||||
5. ... I think that covers most of it.
|
||||
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
---
|
||||
name: ifcopenshell-docker-build
|
||||
description: >-
|
||||
Build a real ifcopenshell_wrapper (.so + .py) and IfcConvert locally via
|
||||
the docker/ifcos_env toolchain, then wire them into a checkout for
|
||||
running C++-dependent parts of the test suite (geometry, the SWIG
|
||||
wrapper stub, the C++ parser). Use whenever a task needs to compile
|
||||
IfcOpenShell's C++ core rather than just read/patch source - e.g.
|
||||
reproducing or fixing a bug in src/ifcgeom, src/ifcparse, src/ifcwrap,
|
||||
or validating util/scripts/validate_stub.py against the actual
|
||||
generated wrapper.
|
||||
---
|
||||
|
||||
# Building IfcOpenShell locally with docker/ifcos_env
|
||||
|
||||
`docker/` mirrors the project's GitHub Actions build environment locally,
|
||||
in a persistent, non-root container with ccache so repeat builds are fast.
|
||||
See `docker/README.md` for the design rationale. Pure-Python changes don't
|
||||
need any of this - only reach for it when you need a real compiled
|
||||
`_ifcopenshell_wrapper*.so` or `IfcConvert` binary.
|
||||
|
||||
## Placement
|
||||
|
||||
This `docker/` folder must live as a direct child of the repo root you want
|
||||
to build (sibling of `src/`, `cmake/`, etc.) - `compose.yaml` and
|
||||
`ifcos_env` resolve the repo via `../` relative to wherever `docker/`
|
||||
itself sits, and bind-mount it into the container. If you're setting this
|
||||
up in a fresh clone, copy the whole `docker/` directory there first.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
./ifcos_env create # build the image (shared by name across all your clones/checkouts, so usually instant after the first time anywhere)
|
||||
./ifcos_env up # create + start the container, clone/unpack the third-party dependency cache (~10GB, one-time per container)
|
||||
./ifcos_env build # full build: all deps + IfcParse + IfcGeom + IfcConvert + the Python wrapper, for one Python version
|
||||
```
|
||||
|
||||
`PY_TGT` and `UNIQUE_ID` live in `docker/.env` - `PY_TGT` (e.g. `py-311`)
|
||||
restricts the build to one Python version instead of building five;
|
||||
`UNIQUE_ID` is a hash of the folder path, recalculated on every `up`, so
|
||||
each checkout gets its own container/volumes automatically.
|
||||
|
||||
A full first build takes ~1.5 hours (mostly compiling IfcOpenShell's own
|
||||
C++, not the cached third-party deps). After that, ccache makes incremental
|
||||
rebuilds of a couple of touched `.cpp` files **under a minute**.
|
||||
|
||||
## Container lifecycle
|
||||
|
||||
The container is long-lived (`sleep infinity`) so exec'd commands and
|
||||
ccache state persist between builds. Commands map directly onto Docker
|
||||
Compose's own container-vs-image distinction:
|
||||
|
||||
```bash
|
||||
./ifcos_env up # create the container if it doesn't exist, then start it (runs ready_repo too)
|
||||
./ifcos_env stop # stop the container, keep it around
|
||||
./ifcos_env start # start it back up (same container, same filesystem layer)
|
||||
./ifcos_env restart # stop, then start
|
||||
./ifcos_env down # remove the container (and its network) entirely
|
||||
./ifcos_env recreate # down, then up - a fresh container
|
||||
```
|
||||
|
||||
Named volumes (`ccache`) and the bind-mounted repo/`build/` are unaffected
|
||||
by `down`/`recreate` - only the container itself goes away, and `up`
|
||||
recreates it from the image.
|
||||
|
||||
## Fast iteration
|
||||
|
||||
Pass a target to `build` to skip the parts you don't need:
|
||||
|
||||
```bash
|
||||
./ifcos_env build IfcConvert # only the executables (IfcConvert, IfcGeomServer) - skips the Python wrapper entirely
|
||||
./ifcos_env build IfcOpenShell-Python # only the SWIG Python wrapper - skips executables entirely
|
||||
./ifcos_env build # no target = everything (needed the first time, or after touching shared headers)
|
||||
```
|
||||
|
||||
Use this to keep the edit -> rebuild -> test loop fast when debugging: if
|
||||
you're only touching `src/ifcgeom/`, build `IfcConvert`; if you're only
|
||||
exercising the Python API, build `IfcOpenShell-Python`.
|
||||
|
||||
## Where the artifacts land
|
||||
|
||||
Build output goes to `<repo_root>/build/Linux/x86_64/install/` on the host
|
||||
(bind-mounted, not just inside the container), owned by you (see
|
||||
"Container user" below):
|
||||
|
||||
- `ifcopenshell/bin/IfcConvert` - the CLI binary
|
||||
- `python-<version>/lib/python<X.Y>/site-packages/ifcopenshell/_ifcopenshell_wrapper*.so`
|
||||
and `ifcopenshell_wrapper.py` - the compiled wrapper + its generated
|
||||
Python glue
|
||||
|
||||
## Testing against a checkout (automated / AI-driven)
|
||||
|
||||
`_ifcopenshell_wrapper*.so` and `ifcopenshell_wrapper.py` are already
|
||||
gitignored under `src/ifcopenshell-python/ifcopenshell/`, which is exactly
|
||||
where a normal in-tree build would put them - copy the two files there:
|
||||
|
||||
```bash
|
||||
SRC=build/Linux/x86_64/install/python-3.11.8/lib/python3.11/site-packages/ifcopenshell
|
||||
cp "$SRC/_ifcopenshell_wrapper.cpython-311-x86_64-linux-gnu.so" src/ifcopenshell-python/ifcopenshell/
|
||||
cp "$SRC/ifcopenshell_wrapper.py" src/ifcopenshell-python/ifcopenshell/
|
||||
```
|
||||
|
||||
Then, to run the test suite against it:
|
||||
|
||||
```bash
|
||||
export PATH="$PWD/build/Linux/x86_64/install/ifcopenshell/bin:$PATH" # for IfcConvert-dependent tests
|
||||
cd src/ifcopenshell-python/test
|
||||
PYTHONPATH="$PWD/.." python3.11 -m pytest -p no:pytest-blender .
|
||||
```
|
||||
|
||||
(`-p no:pytest-blender` avoids the pytest-blender plugin trying to find a
|
||||
`blender` executable and failing collection entirely, even for non-Blender
|
||||
tests.) You'll need the matching Python version's `pip install`s too
|
||||
(numpy, shapely, isodate, lark, tabulate, pytest, ... - whatever the
|
||||
modules under test import) since this is a bare interpreter, not the
|
||||
project's pixi env.
|
||||
|
||||
**This is the pattern to use for automated or AI-driven verification.**
|
||||
Don't use `try` (below) for that - it overwrites files in a real, live
|
||||
Blender installation, which isn't something an automated/AI workflow
|
||||
should ever do without the human explicitly asking for it in the moment.
|
||||
|
||||
## Testing in Blender itself (human only)
|
||||
|
||||
`try` copies the built wrapper straight into your actual Blender/Bonsai
|
||||
extension install, for manual in-Blender testing:
|
||||
|
||||
```bash
|
||||
./ifcos_env try
|
||||
```
|
||||
|
||||
It reads `BLENDER_USER_RESOURCE` from `.env` - set this to wherever
|
||||
Blender's user resource folder for the Bonsai extension actually lives on
|
||||
your system, which depends on your own Blender setup:
|
||||
|
||||
```bash
|
||||
# in docker/.env
|
||||
BLENDER_USER_RESOURCE=~/.config/blender/bonsai/
|
||||
```
|
||||
|
||||
`try` figures out the built Python version from `build/.../install/`
|
||||
(disambiguating with `PY_TGT` if more than one version was built) and
|
||||
copies the wrapper to
|
||||
`$BLENDER_USER_RESOURCE/extensions/.local/lib/python<X.Y>/site-packages/ifcopenshell/`.
|
||||
|
||||
## Container user
|
||||
|
||||
The image runs as a non-root `builder` user, UID/GID matching your host
|
||||
account (passed as `--build-arg` by `create` from `id -u`/`id -g`, so it
|
||||
adjusts automatically - no manual flag needed even if you're not 1000:1000).
|
||||
Files the build creates under the bind mount come out owned by you, not
|
||||
root. Passwordless `sudo` is available inside the container (e.g. via
|
||||
`attach`) for the rare case you need root for something ad hoc.
|
||||
|
||||
If you're picking up an existing checkout that was previously built with
|
||||
an older, root-based image, you may hit `Permission denied` the first time
|
||||
you run `up`/`build` under the new image - `build/`, `.git/modules/`, the
|
||||
`ccache` volume, `output/`, and `build.log` can all be left root-owned from
|
||||
before. Fix it once via the container's own root (no host `sudo` needed):
|
||||
|
||||
```bash
|
||||
docker exec -u root -w /__w/IfcOpenShell/IfcOpenShell <container-name> \
|
||||
chown -R "$(id -u)":"$(id -g)" .git/modules build output build.log /ccache
|
||||
```
|
||||
|
||||
(`<container-name>` is `ifcopenshell-<UNIQUE_ID>` - see `docker ps -a`.)
|
||||
|
||||
## Other things worth knowing
|
||||
|
||||
- **Linux x64 only.** `compose.yaml` pins `platform: linux/amd64`; on an
|
||||
ARM host (e.g. Apple Silicon) this build isn't available.
|
||||
- **The final "Package .zip archives" step of `build()` has a pre-existing
|
||||
bash syntax error**, unrelated to compilation - the actual build already
|
||||
succeeded by that point (look for `Built IfcOpenShell...` in the output),
|
||||
so this is safe to ignore if you only need the raw artifacts under
|
||||
`build/.../install/`, not packaged release zips.
|
||||
- **`test_mmaped_stream` and similar `USE_MMAP`-dependent tests will fail**
|
||||
against this build - `nix/build-all.py` is invoked with `USE_MMAP=OFF`
|
||||
here. Not a bug in your code if you see it fail.
|
||||
- Only the bind-mounted `<repo>/build` lives on the host filesystem your
|
||||
repo is checked out on. Anything the container writes *outside* that
|
||||
mount lives in the container's own writable layer under Docker's data
|
||||
root (commonly `/var/lib/docker`, i.e. usually your root partition) -
|
||||
keep an eye on `df -h /` if you're running several of these containers
|
||||
at once.
|
||||
@@ -1,15 +0,0 @@
|
||||
name: ifcopenshell-${UNIQUE_ID}
|
||||
services:
|
||||
ifcopenshell:
|
||||
container_name: ifcopenshell-${UNIQUE_ID}
|
||||
image: ifcopenshell-build-env:updated
|
||||
platform: linux/amd64
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ../
|
||||
target: /__w/IfcOpenShell/IfcOpenShell
|
||||
- ccache:/ccache
|
||||
|
||||
volumes:
|
||||
ccache:
|
||||
name: ifcopenshell-ccache-shared
|
||||
@@ -1,339 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ================== CONFIG ==================
|
||||
SCRIPT_NAME=$(basename "$0")
|
||||
ENV_FILE=".env"
|
||||
WORKDIR="/__w/IfcOpenShell/IfcOpenShell"
|
||||
NAMEPREFIX=ifcopenshell
|
||||
|
||||
function set_env() {
|
||||
# Load .env file if it exists
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
echo "✅ Loaded environment variables from $ENV_FILE"
|
||||
else
|
||||
echo "⚠️ No $ENV_FILE found, proceeding without it."
|
||||
fi
|
||||
}
|
||||
|
||||
set_env
|
||||
|
||||
# ================ FUNCTIONS =================
|
||||
|
||||
function create() {
|
||||
echo "⭐ Creating image: ifcopenshell-build-env"
|
||||
docker build -f Dockerfile \
|
||||
--build-arg USER_UID="$(id -u)" --build-arg USER_GID="$(id -g)" \
|
||||
-t ifcopenshell-build-env:updated .
|
||||
}
|
||||
|
||||
function update() {
|
||||
# The Dockerfile always builds FROM a clean rockylinux:9 and does
|
||||
# `dnf update -y` as its first step, so re-running create() is enough
|
||||
# to get fresh packages.
|
||||
echo "⚡ Updating image: ifcopenshell-build-env"
|
||||
create
|
||||
}
|
||||
|
||||
function up() {
|
||||
# Creates the container if it doesn't exist yet (and starts it either
|
||||
# way) - this is the one that needs ready_repo, since a freshly created
|
||||
# container has no submodules/dependency cache in place yet.
|
||||
echo "🚀 Creating/starting stack: ifcopenshell-${UNIQUE_ID}"
|
||||
unique # Update UNIQUE_ID first
|
||||
docker compose up -d "$@" # Container must exist before ready_repo can exec into it.
|
||||
ready_repo # Ensure repo is recursive, and the build repo is in place.
|
||||
}
|
||||
|
||||
function down() {
|
||||
# Removes the container (and its network) entirely. Named volumes
|
||||
# (ccache) and the bind-mounted repo/build/ survive; up() will recreate
|
||||
# the container from scratch next time.
|
||||
echo "🔥 Removing stack: ifcopenshell-${UNIQUE_ID}"
|
||||
docker compose down "$@"
|
||||
}
|
||||
|
||||
function stop() {
|
||||
# Stops the existing container without removing it - the container,
|
||||
# its filesystem layer, and its exec history all remain intact.
|
||||
echo "🛑 Stopping stack: ifcopenshell-${UNIQUE_ID}"
|
||||
docker compose stop "$@"
|
||||
}
|
||||
|
||||
function start() {
|
||||
# Starts a previously-stopped container back up. Does nothing (and
|
||||
# won't create anything) if the container doesn't exist - use up() for
|
||||
# that.
|
||||
echo "▶️ Starting stack: ifcopenshell-${UNIQUE_ID}"
|
||||
docker compose start "$@"
|
||||
}
|
||||
|
||||
function restart() {
|
||||
echo "🔄 Restarting stack (stop, then start)..."
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
function recreate() {
|
||||
echo "♻️ Recreating stack (down, then up)..."
|
||||
down
|
||||
up
|
||||
}
|
||||
|
||||
function logs() {
|
||||
echo "📜 Showing logs..."
|
||||
docker compose logs -f "$@"
|
||||
}
|
||||
|
||||
function ps() {
|
||||
docker compose ps
|
||||
}
|
||||
|
||||
function config() {
|
||||
echo "🔍 Validated compose configuration:"
|
||||
docker compose config
|
||||
}
|
||||
|
||||
function remove() {
|
||||
# Lower-level than down(): removes already-stopped containers without
|
||||
# touching the compose network. Mostly useful after a plain stop().
|
||||
echo "🗑️ Removing stopped containers: ifcopenshell-${UNIQUE_ID}"
|
||||
docker compose rm "$@"
|
||||
}
|
||||
|
||||
function unique() {
|
||||
echo "🔧 Making stack name folder specific..."
|
||||
|
||||
REGEX="^UNIQUE_ID="
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]] || ! grep -qE "$REGEX" "$ENV_FILE"; then
|
||||
echo -e "\nUNIQUE_ID=dummy\n" >> "$ENV_FILE"
|
||||
fi
|
||||
|
||||
export UNIQUE_ID="$(pwd | sha256sum | cut -c -8)"
|
||||
|
||||
# `sed -i` takes incompatible syntax between GNU sed (Linux) and BSD sed
|
||||
# (macOS) - `-si` is GNU-only and errors as "illegal option -- s" under
|
||||
# BSD/macOS sed. Avoid -i altogether and do the in-place edit via a temp
|
||||
# file + mv instead, which behaves identically with either sed.
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp "${ENV_FILE}.XXXXXX")"
|
||||
sed "s/^UNIQUE_ID=.*$/UNIQUE_ID=${UNIQUE_ID}/" "$ENV_FILE" > "$tmp_file"
|
||||
mv "$tmp_file" "$ENV_FILE"
|
||||
|
||||
set_env
|
||||
}
|
||||
|
||||
function ready_repo() {
|
||||
echo "👍 Getting the repo ready to build..."
|
||||
docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
|
||||
set -euo pipefail # Recommended for robustness
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
if [[ ! -d "build" ]]; then
|
||||
git clone -b rockylinux9-x64 https://github.com/IfcOpenShell/build-outputs.git build
|
||||
else
|
||||
cd build
|
||||
git pull
|
||||
cd ..
|
||||
fi
|
||||
|
||||
if [[ ! -d "build/Linux/x86_64/install/boost-1.86.0/" ]]; then
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
cd ..
|
||||
fi
|
||||
'
|
||||
}
|
||||
|
||||
function build() {
|
||||
echo "☕ Execute the build, go make yourself a cuppa... I'll be a while"
|
||||
local BUILD_TARGET="$1"
|
||||
|
||||
docker exec -i -w "${WORKDIR}" -e PY_TGT="${PY_TGT}" -e BUILD_TARGET="${BUILD_TARGET}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v ${PY_TGT:+-$PY_TGT} --diskcleanup ${BUILD_TARGET} 2>&1 | tee build.log
|
||||
'
|
||||
echo "🎒 Pack Dependencies"
|
||||
docker exec -i -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
'
|
||||
|
||||
echo "🎁 Package .zip archives"
|
||||
docker exec -i -w "${WORKDIR}" -e GITHUB_SHA="$(git rev-parse HEAD)" "${NAMEPREFIX}-${UNIQUE_ID}" bash -c '
|
||||
OUTPUT_DIR=${PWD}/output
|
||||
VERSION=v`cat VERSION`
|
||||
mkdir -p ${OUTPUT_DIR}
|
||||
cd ./build/`uname`/*/install/ifcopenshell
|
||||
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
numbers=`echo $py_version | grep -oE "[0-9]+\.[0-9]+" | tr -d "."`
|
||||
py_version_major=python-${numbers}$postfix
|
||||
pushd . > /dev/null
|
||||
cd $py_version
|
||||
if [ ! -d ifcopenshell ]; then
|
||||
mkdir ../ifcopenshell_
|
||||
mv * ../ifcopenshell_
|
||||
mv ../ifcopenshell_ ifcopenshell
|
||||
fi
|
||||
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
|
||||
find ifcopenshell -name "*.pyc" -delete
|
||||
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/*
|
||||
mv *.zip ${OUTPUT_DIR}/
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
cd bin
|
||||
if compgen -G "./*.zip" > /dev/null; then
|
||||
rm *.zip 2>&1 >/dev/null || true
|
||||
ls | while read exe; do
|
||||
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe
|
||||
done
|
||||
mv *.zip ${OUTPUT_DIR}/
|
||||
cd ..
|
||||
'
|
||||
}
|
||||
|
||||
function attach() {
|
||||
echo "🔦 Connect to interactive shell"
|
||||
docker exec -it -w "${WORKDIR}" "${NAMEPREFIX}-${UNIQUE_ID}" /bin/bash
|
||||
}
|
||||
|
||||
function try() {
|
||||
# Copies the freshly built wrapper into your actual Blender/Bonsai
|
||||
# installation for manual, in-Blender testing. This is a human-only
|
||||
# convenience: it overwrites files in your live Blender setup, so it's
|
||||
# not something that should run unattended as part of an automated or
|
||||
# AI-driven build/test loop (which should instead copy the wrapper into
|
||||
# the repo's own src/ifcopenshell-python/ifcopenshell/ - see SKILL.md).
|
||||
echo "🚴 Copying build artifacts into your Blender resource folder for testing"
|
||||
|
||||
if [[ -z "${BLENDER_USER_RESOURCE:-}" ]]; then
|
||||
echo "❌ BLENDER_USER_RESOURCE is not set in .env."
|
||||
echo " Add a line pointing at wherever Blender's user resource folder for"
|
||||
echo " the Bonsai extension actually is on your system, e.g.:"
|
||||
echo " BLENDER_USER_RESOURCE=~/.config/blender/bonsai/"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Normalise: expand a leading ~ (in case it was quoted in .env and so
|
||||
# never went through shell tilde-expansion when set_env sourced it),
|
||||
# then resolve to an absolute, symlink-free path.
|
||||
local resource="${BLENDER_USER_RESOURCE/#\~/$HOME}"
|
||||
resource="$(realpath -m "$resource")"
|
||||
|
||||
local install_dir="../build/Linux/x86_64/install"
|
||||
local py_dirs=("$install_dir"/python-*)
|
||||
if [[ ${#py_dirs[@]} -gt 1 && -n "${PY_TGT:-}" ]]; then
|
||||
# PY_TGT is compact (py-311); the install dirs are dotted
|
||||
# (python-3.11.8) - reinsert the dot (assumes a single-digit major
|
||||
# version, true for the Python 3.x line) before matching.
|
||||
local py_tgt_digits="${PY_TGT#py-}"
|
||||
local py_tgt_dotted="${py_tgt_digits:0:1}.${py_tgt_digits:1}"
|
||||
local filtered=() d
|
||||
for d in "${py_dirs[@]}"; do
|
||||
[[ "$(basename "$d")" == "python-${py_tgt_dotted}."* ]] && filtered+=("$d")
|
||||
done
|
||||
[[ ${#filtered[@]} -gt 0 ]] && py_dirs=("${filtered[@]}")
|
||||
fi
|
||||
if [[ ${#py_dirs[@]} -ne 1 || ! -d "${py_dirs[0]}" ]]; then
|
||||
echo "❌ Expected exactly one built python-* dir under $install_dir, found ${#py_dirs[@]}."
|
||||
echo " Run 'build' first, or set PY_TGT in .env to disambiguate a multi-version build."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local py_minor
|
||||
py_minor="$(basename "${py_dirs[0]}" | grep -oE '[0-9]+\.[0-9]+')"
|
||||
local wrapper_dir="${py_dirs[0]}/lib/python${py_minor}/site-packages/ifcopenshell"
|
||||
if [[ ! -f "$wrapper_dir/ifcopenshell_wrapper.py" ]]; then
|
||||
echo "❌ Built wrapper not found at $wrapper_dir - run 'build' first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local target="$resource/extensions/.local/lib/python${py_minor}/site-packages/ifcopenshell"
|
||||
mkdir -p "$target"
|
||||
cp "$wrapper_dir"/_ifcopenshell_wrapper*.so "$target/"
|
||||
cp "$wrapper_dir"/ifcopenshell_wrapper.py "$target/"
|
||||
echo "✅ Copied wrapper into $target"
|
||||
}
|
||||
|
||||
function clean() {
|
||||
# Host-side only - doesn't touch the container, image, or ccache volume.
|
||||
echo "💎 Clean the build and output folder up"
|
||||
if [[ -d "../build" ]]; then
|
||||
rm -rf ../build
|
||||
fi
|
||||
if [[ -d "../output" ]]; then
|
||||
rm -rf ../output
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
function help() {
|
||||
cat <<EOF
|
||||
Usage: ./$SCRIPT_NAME <command>
|
||||
|
||||
Available commands:
|
||||
create Build the rocky9-based image
|
||||
update Rebuild the image fresh, picking up OS package updates
|
||||
up Create the container if it doesn't exist yet, and start it
|
||||
down Remove the container entirely (docker compose down)
|
||||
stop Stop the container without removing it
|
||||
start Start a previously-stopped container
|
||||
restart stop, then start (same container, no recreation)
|
||||
recreate down, then up (fresh container)
|
||||
build Execute the IfcOpenShell build
|
||||
attach Connect to an interactive shell in the container
|
||||
try Copy the built wrapper into your Blender resource folder
|
||||
(human-only - see BLENDER_USER_RESOURCE below, and SKILL.md
|
||||
for the AI/automated-testing equivalent)
|
||||
clean Remove the build and output folders
|
||||
logs Follow container logs
|
||||
ps Show running containers
|
||||
config Validate and show compose config
|
||||
remove Remove stopped containers (docker compose rm)
|
||||
help Show this help
|
||||
|
||||
Environment variables from .env are automatically loaded, including:
|
||||
PY_TGT Restrict the build to one Python version, e.g. py-311
|
||||
UNIQUE_ID Recalculated automatically on every 'up', don't set by hand
|
||||
BLENDER_USER_RESOURCE Where 'try' copies the wrapper for manual testing, e.g.
|
||||
~/.config/blender/bonsai/
|
||||
EOF
|
||||
}
|
||||
|
||||
# ================= MAIN =================
|
||||
|
||||
case "$1" in
|
||||
create) create ;;
|
||||
update) update ;;
|
||||
up) up "${@:2}" ;;
|
||||
down) down "${@:2}" ;;
|
||||
stop) stop "${@:2}" ;;
|
||||
start) start "${@:2}" ;;
|
||||
restart) restart ;;
|
||||
recreate) recreate ;;
|
||||
build) build "${@:2}" ;;
|
||||
attach) attach ;;
|
||||
try) try ;;
|
||||
clean) clean ;;
|
||||
logs) logs "${@:2}" ;;
|
||||
ps) ps ;;
|
||||
config) config ;;
|
||||
remove) remove ;;
|
||||
help|-h|--help) help ;;
|
||||
"")
|
||||
echo "❌ No command provided."
|
||||
help
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown command: $1"
|
||||
echo "Type './$SCRIPT_NAME help' for available commands."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- This file was generated with the assistance of an AI coding tool. -->
|
||||
|
||||
# Developer notes (in-progress features)
|
||||
|
||||
This directory holds **living design/working notes for unmerged feature branches**,
|
||||
one Markdown file per feature, named after its branch (e.g.
|
||||
`opening-template-on-type.md`).
|
||||
|
||||
## Purpose
|
||||
|
||||
A shared scratchpad so collaborators — and the AI agents they work with — can pick up
|
||||
the context behind an in-progress branch: the problem, the design decisions and the
|
||||
*why*, dead ends already ruled out, and what still needs testing. Because the note is
|
||||
committed on the branch, it travels with the PR and shows up in the diff, so it is
|
||||
discoverable without anyone being told where to look.
|
||||
|
||||
## How to use it (humans and agents)
|
||||
|
||||
- **Before working on a feature branch**, read its note here if one exists.
|
||||
- **As the PR is refined**, keep the note current — append decisions, correct things
|
||||
that changed, update the test checklist.
|
||||
- **One file per feature**, named after the branch.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
These are *not* permanent user documentation. When a PR merges, either remove its note
|
||||
or promote the durable parts (the load-bearing "why") into code comments or the regular
|
||||
docs, so stale notes do not accumulate on the default branch.
|
||||
@@ -0,0 +1,165 @@
|
||||
<!-- This file was generated with the assistance of an AI coding tool. -->
|
||||
|
||||
# Opening template on type — preserving custom openings across duplicate_type / append
|
||||
|
||||
> **Living dev note** for the `opening-template-on-type` branch/PR. Read before working
|
||||
> on the feature; append decisions and findings as the PR is refined. This is *not* user
|
||||
> documentation — at merge it is removed or its durable parts promoted to code comments.
|
||||
> See [README.md](README.md) for the convention.
|
||||
|
||||
## Problem
|
||||
|
||||
`bpy.ops.bim.duplicate_type` and `bpy.ops.bim.append_library_element` lose a custom
|
||||
`IfcOpeningElement` body (e.g. an `IfcPolygonalFaceSet`/tessellation) and replace it
|
||||
with a generated extrusion. Root cause: the only mechanism that preserved a custom
|
||||
opening was "copy it from a sibling occurrence of the same type"
|
||||
(`get_existing_opening_occurrence_if_any`), which returns nothing for a brand-new
|
||||
type. `generate_opening_from_filling` then always builds an extrusion (profile or
|
||||
bbox), discarding the custom geometry.
|
||||
|
||||
## Key facts established
|
||||
|
||||
- IFC-level `root.copy_class` already `copy_deep`s opening representations; the loss
|
||||
happens on the Bonsai side (the `regenerate_from_type` listener on
|
||||
`type.assign_type`, and placement-time generation).
|
||||
- Opening occurrences of one type already **share** a single `IfcRepresentationMap`
|
||||
via mapped representations — that is why editing one void edits them all
|
||||
(see `tool.Model.unshare_opening_representation` docstring). Bonsai shares, it does
|
||||
not copy. The shared map just has no durable home (it is hosted implicitly by
|
||||
whichever occurrence exists), so it does not survive to a new type.
|
||||
- IFC4 ADD2 TC1 `IfcShapeRepresentation`: identifier **`Reference`** = "3D
|
||||
representation that is **not part of the Body representation** ... used, e.g., for
|
||||
opening geometries ... excluded from an implicit Boolean operation." Schema-valid;
|
||||
`IfcTypeProduct` has no uniqueness rule on `RepresentationMaps` (only
|
||||
`ApplicableOccurrence`). So a `Reference` map can sit beside the `Body` map.
|
||||
- The geometry kernel selects an opening's geometry **by context, not by
|
||||
`RepresentationIdentifier`** (`mapping::representation_of`, `ifcgeom/mapping/mapping.cpp`).
|
||||
So a `Reference`-identified opening in the Body context still booleans correctly.
|
||||
Nothing in Bonsai reads `"Reference"` to *skip* applying an opening.
|
||||
- Caveat: IFC has no type-level void (`IfcRelVoidsElement` is occurrence-only). The
|
||||
"opening template on type" is therefore a Bonsai convention using a spec-valid
|
||||
identifier; other tools see a harmless extra `Reference` rep they ignore. The
|
||||
regeneration smarts are Bonsai-only by necessity.
|
||||
|
||||
## Design
|
||||
|
||||
Store the shared opening body on the **type** as a `Reference` representation map.
|
||||
Because `bim.duplicate_type` (`tool.Root.copy_representation`) and
|
||||
`append_type_product` both copy a type's `RepresentationMaps`, the template survives
|
||||
both. Occurrence openings map over the same map, so editing a void rewrites the
|
||||
shared map = updates the type template in one stroke (no separate write-back needed).
|
||||
|
||||
`map_type_representations` must skip `Reference` maps so the window/door occurrence
|
||||
does not receive the opening shape as its own Body (the kernel would otherwise pick
|
||||
arbitrarily between the real Body and the opening rep). The skip is both required and
|
||||
spec-endorsed ("not part of the Body representation").
|
||||
|
||||
### Body-context coexistence (Option A)
|
||||
|
||||
The template lives in the **Body** subcontext (required: the instance opening that maps
|
||||
over it must resolve in Body context for the geometry kernel to subtract it). So the
|
||||
type holds two reps in one context: the `Body` window body and the `Reference` opening
|
||||
template. Per IFC, `Reference` is a *RepresentationIdentifier value used within the Body
|
||||
context*, not a separate context - so we keep it there and disambiguate elsewhere:
|
||||
|
||||
- The representations panel now shows `RepresentationIdentifier` as its own column
|
||||
(`geometry/data.py`, `geometry/ui.py`) so the two Body-context reps are
|
||||
distinguishable (`Model | Body | MODEL_VIEW | Reference | Tessellation`). The panel
|
||||
column previously read "Body" because it shows `ContextOfItems.ContextIdentifier`,
|
||||
not the representation's identifier.
|
||||
- `Geometry.reimport_element_representations` type branch now renders the requested
|
||||
`base_representation` instead of `get_representation(element, context)`, which matched
|
||||
only by context and returned the window body when switching to the `Reference` rep.
|
||||
This is what makes "switch to the Reference row" actually show the void on the type.
|
||||
|
||||
### Precedence in `generate_opening_from_filling`
|
||||
type `Reference` template → (existing sibling occurrence, checked by callers) →
|
||||
type `Profile` extrusion → bbox extrusion.
|
||||
|
||||
### Type switching (assign_type)
|
||||
|
||||
On `type.assign_type` the opening is rebuilt to reflect the **assigned** type's void.
|
||||
Two listeners in `model/handler.py`:
|
||||
|
||||
- **pre** `Bonsai.Opening.PreserveOnTypeChange` → `preserve_opening_on_type_change`:
|
||||
before the filling moves to the new type, `promote_opening_to_type(old_type)` anchors
|
||||
the old type's custom void as a template, so it isn't lost when (possibly the last)
|
||||
occurrence is regenerated. Idempotent; custom voids only.
|
||||
- **post** `Bonsai.Opening.RegenerateFromType` → `regenerate_from_type` →
|
||||
`_regenerate_from_type`: rebuilds from the new type's template / sibling / extrusion.
|
||||
The old PR1 "preserve custom" guard was **removed** here — it kept the previous type's
|
||||
void on a switch (wrong), and the template now makes preservation unnecessary.
|
||||
|
||||
NOTE: upstream `v0.8.0` landed `assign_type` changes + new `test_assign_type_*` tests
|
||||
(merged under this branch's base). The listeners ride on top of that — re-test the
|
||||
switch/edit round-trips against the new `assign_type`.
|
||||
|
||||
### Write-back on void edit
|
||||
|
||||
Editing an occurrence's void writes the new geometry back to the type's `Reference`
|
||||
template via `update_type_template_from_opening` (creates the template if absent), then
|
||||
**re-maps every occurrence's opening onto the template** and reloads the affected host walls
|
||||
(`switch_representation`) so they re-boolean. The re-map (`_remap_opening_to_template`) is the
|
||||
key part: an earlier version only re-pointed a *pre-existing* shared map, so siblings whose
|
||||
openings were **independent** (their own `IfcRepresentationMap`, never sharing the template)
|
||||
didn't follow — the common real-world case. Now they do. Hooked at both commit paths:
|
||||
`UpdateRepresentation._execute` (the `edited_objs` path) and
|
||||
`OverrideModeSetObject` after `edit_representation_item` (the in-place item edit). The
|
||||
older `edit_openings`/`is_edited` path also calls it. `set_type_opening_representation`
|
||||
has replace semantics (one `Reference` map per type).
|
||||
|
||||
### Preserving adjusted extrusions (duplicate_type)
|
||||
|
||||
`is_opening_representation_custom` only flags *non-extrusion* geometry (tessellation, brep,
|
||||
CSG) as worth preserving — a proxy for "not regenerable". That mis-classifies a *manually
|
||||
adjusted* extrusion, which is still an `IfcExtrudedAreaSolid`, so a hand-tweaked extrusion
|
||||
opening was reset to the default on `duplicate_type`.
|
||||
|
||||
`promote_opening_to_type` now gates on `should_preserve_opening` = custom **or**
|
||||
`_is_adjusted_extrusion`. The latter generates the default (`generate_opening_from_filling`,
|
||||
which yields the default since no template exists at promote time) *transiently*, compares the
|
||||
two bodies' axis-aligned bounding boxes (1 mm tolerance) via the geom engine, then removes the
|
||||
temporary default. Divergence ⇒ the extrusion was adjusted ⇒ promote it; a plain default
|
||||
matches ⇒ left regenerable (not frozen — see the "freeze" discussion). Scoped to the duplicate
|
||||
path so the generate-and-compare stays out of the hot predicate. Limitation: bbox comparison
|
||||
misses a shape change that preserves the bbox (upgrade to a vertex-set compare if needed).
|
||||
|
||||
## Status — implemented (manually verified in Blender)
|
||||
|
||||
- core `map_type_representations.py`: skip `Reference` maps.
|
||||
- `model/opening.py`: `get_/set_type_opening_representation`, `promote_opening_to_type`,
|
||||
`update_type_template_from_opening` (+ `_remap_opening_to_template`),
|
||||
`preserve_opening_on_type_change`, `should_preserve_opening` (+ `_is_adjusted_extrusion`,
|
||||
`_representation_bbox`); `generate_opening_from_filling` consults the template; PR1 guard
|
||||
removed from `_regenerate_from_type`.
|
||||
- `model/handler.py`: pre + post assign_type listeners.
|
||||
- `type/operator.py` `DuplicateType`: promote before copy.
|
||||
- `project/operator.py` `AppendLibraryElement`: `harvest_opening_template`.
|
||||
- `geometry/operator.py`: write-back hooks in `UpdateRepresentation` and
|
||||
`OverrideModeSetObject`; `reimport_element_representations` renders the requested rep.
|
||||
- `geometry/data.py` + `geometry/ui.py`: `RepresentationIdentifier` column + headers.
|
||||
|
||||
Branch `opening-template-on-type` (#8200): initial feature commit + the #7916 build-conflict
|
||||
ancestry-merge + void-propagation-to-all-occurrences + adjusted-extrusion preservation. The
|
||||
`docs/dev-notes/` convention itself lives on the stacked branch `dev-notes-system` (#8201).
|
||||
|
||||
Still **deferred:** explicit "Apply/Reset to type" operators + a "diverges from type"
|
||||
indicator; import never auto-writes back. `update_simple_openings` still keeps its
|
||||
`is_opening_representation_custom` guard (array propagation, same type — left as-is).
|
||||
|
||||
## Things to test / verify
|
||||
|
||||
- Duplicated/appended type's new occurrence gets the faceset void and it **cuts** the
|
||||
wall (kernel selects opening geom by context, so a `Reference`-id rep still booleans).
|
||||
- `harvest_opening_template` cross-file `file.add`: no duplicate
|
||||
`IfcGeometricRepresentationContext` left behind; units (kernel doesn't rescale rep
|
||||
coords — same assumption as `append_asset`).
|
||||
- Switch X→Y→X round-trip restores each type's void; switching to a plain (template-less)
|
||||
type gives its default extrusion, not the previous faceset.
|
||||
- Edit a void → type's `Reference` row updates; **all** occurrences follow (including ones
|
||||
that had independent openings) and their host walls re-boolean; survives duplicate.
|
||||
- `duplicate_type` on a type whose extrusion opening was **manually adjusted** → Type B keeps
|
||||
the adjusted extrusion; a type with a plain/default extrusion stays regenerable (not frozen).
|
||||
- Three write-back hooks are intentional (different commit paths) — candidate for
|
||||
consolidation in review.
|
||||
- Re-test against upstream's new `assign_type` (see NOTE under "Type switching").
|
||||
+7
-20
@@ -50,7 +50,7 @@ Used environment variables:
|
||||
- ``NO_CLEAN`` - do not clean `ifcopenshell` build directories but continue working on current build
|
||||
(installed dependencies are never cleared).
|
||||
By default option is disabled, to enable pass any value from `1`, `on`, `true`.
|
||||
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (8 schemas), to be supplied as `2x3;4;4x3_add2`
|
||||
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (IFC2X3; IFC4; IFC4X3_ADD2) - to be supplied as `2x3;4`
|
||||
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
|
||||
(`true` by default, any other value is considered `false`)
|
||||
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
|
||||
@@ -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
|
||||
@@ -155,7 +155,7 @@ MPFR_VERSION = "3.1.6" # latest is 4.1.0
|
||||
CGAL_VERSION = "v5.6.3"
|
||||
USD_VERSION = "23.05"
|
||||
TBB_VERSION = "2021.9.0"
|
||||
ROCKSDB_VERSION = "10.4.2"
|
||||
ROCKSDB_VERSION = "9.11.2"
|
||||
ZSTD_VERSION = "1.5.7"
|
||||
# binaries
|
||||
cp = "cp"
|
||||
@@ -627,10 +627,9 @@ def build_dependency(
|
||||
build_tool_args: "list[str]",
|
||||
download_url: str,
|
||||
download_name: str,
|
||||
*,
|
||||
download_tool: Literal["py", "git"] = download_tool_default,
|
||||
revision: "Union[str, None]" = None,
|
||||
patch: list[str] | None = None,
|
||||
patch: "Union[str, list[str], None]" = None,
|
||||
shell=None,
|
||||
pre_compile_subs: "Sequence[tuple[str, str, str]]" = (),
|
||||
additional_files: "Union[dict[str, str], None]" = None,
|
||||
@@ -697,10 +696,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
|
||||
@@ -718,6 +714,8 @@ def build_dependency(
|
||||
urlretrieve(url, os.path.join(extract_dir, path))
|
||||
|
||||
if patch is not None:
|
||||
if isinstance(patch, str):
|
||||
patch = [patch]
|
||||
for p in patch:
|
||||
patch_abs = (SCRIPT_PATH / p).absolute().__str__()
|
||||
if os.path.exists(patch_abs):
|
||||
@@ -726,8 +724,6 @@ def build_dependency(
|
||||
except Exception as e:
|
||||
# Assert that the patch has already been applied
|
||||
run(["patch", "-p1", "--batch", "--reverse", "--dry-run", "-i", patch_abs], cwd=extract_dir)
|
||||
else:
|
||||
raise FileNotFoundError(patch_abs)
|
||||
|
||||
if shell is not None:
|
||||
sp.run(shell, shell=True, check=True, cwd=extract_dir)
|
||||
@@ -1175,14 +1171,6 @@ if "cgal" in targets:
|
||||
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
|
||||
gmp_args.extend(MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS)
|
||||
|
||||
# Fixes configure failing to find a working compiler under GCC 15's default -std=gnu23.
|
||||
# Issue presumably will be resolved in any next gmp version, but currently the last one is 6.3.0.
|
||||
# Patch is just applying fix from upstream meantion below:
|
||||
# https://gmplib.org/list-archives/gmp-bugs/2025-February/005561.html
|
||||
gmp_patches = ["./patches/gmp/001-fix-std23.patch"]
|
||||
if GMP_VERSION != "6.3.0":
|
||||
raise Exception(f"GMP_VERSION changed to {GMP_VERSION}, check whether {gmp_patches} is still needed.")
|
||||
|
||||
build_dependency(
|
||||
name=f"gmp-{GMP_VERSION}",
|
||||
mode="autoconf",
|
||||
@@ -1190,7 +1178,6 @@ if "cgal" in targets:
|
||||
pre_compile_subs=(
|
||||
[("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []
|
||||
),
|
||||
patch=gmp_patches,
|
||||
# Sometimes ftp.gnu.org is very slow, use ftpmirror.gnu.org as a workaround.
|
||||
download_url="https://ftpmirror.gnu.org/gnu/gmp/",
|
||||
download_name=f"gmp-{GMP_VERSION}.tar.bz2",
|
||||
|
||||
@@ -68,7 +68,6 @@ def unpack_dependencies(install_dir: Path) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
action = None
|
||||
if len(sys.argv) != 2 or (action := sys.argv[1].lower()) not in ("pack", "unpack"):
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
Fixes configure failing to find a working compiler under GCC 15's default
|
||||
-std=gnu23 (upstream fix: https://gmplib.org/repo/gmp/rev/8e7bb4ae7a18).
|
||||
|
||||
Upstream fix is patching `acinclude.m4`, but since in the release tarball
|
||||
all macros are already expanded to `configure` script, so we're patching
|
||||
all occurrences of that macro.
|
||||
|
||||
--- a/configure
|
||||
+++ b/configure
|
||||
@@ -6568,7 +6568,7 @@
|
||||
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
@@ -8187,7 +8187,7 @@
|
||||
|
||||
#if defined (__GNUC__) && ! defined (__cplusplus)
|
||||
typedef unsigned long long t1;typedef t1*t2;
|
||||
-void g(){}
|
||||
+void g(int,t1 const*,t1,t2,t1 const*,int){}
|
||||
void h(){}
|
||||
static __inline__ t1 e(t2 rp,t2 up,int n,t1 v0)
|
||||
{t1 c,x,r;int i;if(v0){c=1;for(i=1;i<n;i++){x=up[i];r=x+1;rp[i]=r;}}return c;}
|
||||
@@ -0,0 +1,32 @@
|
||||
http://git.dev.opencascade.org/gitweb/?p=occt.git;a=commitdiff;h=0ab4e621833f4eae945a3762c9a29ee12e2eec53#patch1
|
||||
diff --git a/src/HLRBRep/HLRBRep_InternalAlgo.cxx b/src/HLRBRep/HLRBRep_InternalAlgo.cxx
|
||||
index ca885ca..c13cb06 100644 (file)
|
||||
--- a/src/HLRBRep/HLRBRep_InternalAlgo.cxx
|
||||
+++ b/src/HLRBRep/HLRBRep_InternalAlgo.cxx
|
||||
@@ -165,7 +165,7 @@ void HLRBRep_InternalAlgo::Update ()
|
||||
SB.Bounds(v1,v2,e1,e2,f1,f2);
|
||||
|
||||
for (Standard_Integer e = e1; e <= e2; e++) {
|
||||
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
|
||||
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
|
||||
HLRAlgo::DecodeMinMax(ed.MinMax(), TheMin, TheMax);
|
||||
if (FirstTime) {
|
||||
FirstTime = Standard_False;
|
||||
@@ -307,7 +307,7 @@ void HLRBRep_InternalAlgo::InitEdgeStatus ()
|
||||
Standard_Integer nf = myDS->NbFaces();
|
||||
|
||||
for (Standard_Integer e = 1; e <= ne; e++) {
|
||||
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
|
||||
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
|
||||
if (ed.Selected()) ed.Status().ShowAll();
|
||||
}
|
||||
// for (Standard_Integer f = 1; f <= nf; f++) {
|
||||
@@ -368,7 +368,7 @@ void HLRBRep_InternalAlgo::Select ()
|
||||
Standard_Integer nf = myDS->NbFaces();
|
||||
|
||||
for (Standard_Integer e = 1; e <= ne; e++) {
|
||||
- HLRBRep_EdgeData ed = aEDataArray.ChangeValue(e);
|
||||
+ HLRBRep_EdgeData& ed = aEDataArray.ChangeValue(e);
|
||||
ed.Selected(Standard_True);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
From a0deb4ce8b43cf3c8b8c0a4225c6be5296446dbd Mon Sep 17 00:00:00 2001
|
||||
From: Adam Eri <adam.eri@blackmirror.media>
|
||||
Date: Tue, 3 Sep 2019 23:30:20 +0200
|
||||
Subject: [PATCH] Resolves compile error on macOS
|
||||
|
||||
Resolves "no member named 'isnan' in namespace 'std'" on macOS
|
||||
---
|
||||
GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
|
||||
diff --git a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
|
||||
index 1f9a3eef..dd6f5c59 100644
|
||||
--- a/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
|
||||
+++ b/GeneratedSaxParser/src/GeneratedSaxParserUtils.cpp
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "GeneratedSaxParserUtils.h"
|
||||
#include <math.h>
|
||||
+#include <cmath>
|
||||
#include <memory>
|
||||
#include <string.h>
|
||||
#include <limits>
|
||||
+89
-41
@@ -1,8 +1,13 @@
|
||||
[project]
|
||||
name = "IfcOpenShell"
|
||||
version = "0.0.0"
|
||||
# Don't provide requires-python explicitly
|
||||
# allowing pyprojects to set their own (e.g. bonsai and general ifcopenshell version differ).
|
||||
dependencies = [
|
||||
"black==26.3.1",
|
||||
"ruff==0.15.12",
|
||||
"poethepoet",
|
||||
"ty==0.0.32",
|
||||
"gersemi==0.26.1",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
@@ -38,7 +43,6 @@ exclude = [
|
||||
# then they will be inherited by projects' .toml files.
|
||||
# This allows using assuming different Python version for different projects.
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
exclude = [
|
||||
# Submodules.
|
||||
"src/ifcopenshell-python/ifcopenshell/express",
|
||||
@@ -79,36 +83,92 @@ ignore = [
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
all = "error"
|
||||
all = "ignore"
|
||||
|
||||
# Structural rules (no deep type inference needed, easier to adapt).
|
||||
# Maybe later, requires to specify element types for all generics.
|
||||
missing-type-argument = "ignore"
|
||||
# Conflicts with `bpy` props defined using annotations.
|
||||
invalid-type-form = "ignore"
|
||||
abstract-method-in-final-class = "error"
|
||||
ambiguous-protocol-member = "error"
|
||||
conflicting-declarations = "error"
|
||||
conflicting-metaclass = "error"
|
||||
cyclic-class-definition = "error"
|
||||
cyclic-type-alias-definition = "error"
|
||||
dataclass-field-order = "error"
|
||||
duplicate-base = "error"
|
||||
duplicate-kw-only = "error"
|
||||
empty-body = "error"
|
||||
escape-character-in-forward-annotation = "error"
|
||||
final-on-non-method = "error"
|
||||
final-without-value = "error"
|
||||
ignore-comment-unknown-rule = "error"
|
||||
implicit-concatenated-string-type-annotation = "error"
|
||||
inconsistent-mro = "error"
|
||||
ineffective-final = "error"
|
||||
instance-layout-conflict = "error"
|
||||
invalid-dataclass = "error"
|
||||
invalid-dataclass-override = "error"
|
||||
invalid-enum-member-annotation = "error"
|
||||
invalid-explicit-override = "error"
|
||||
invalid-frozen-dataclass-subclass = "error"
|
||||
invalid-generic-class = "error"
|
||||
invalid-generic-enum = "error"
|
||||
invalid-ignore-comment = "error"
|
||||
invalid-legacy-positional-parameter = "error"
|
||||
invalid-legacy-type-variable = "error"
|
||||
invalid-named-tuple = "error"
|
||||
invalid-newtype = "error"
|
||||
invalid-overload = "error"
|
||||
invalid-paramspec = "error"
|
||||
invalid-protocol = "error"
|
||||
invalid-syntax-in-forward-annotation = "error"
|
||||
invalid-total-ordering = "error"
|
||||
invalid-type-alias-type = "error"
|
||||
invalid-type-checking-constant = "error"
|
||||
invalid-type-guard-definition = "error"
|
||||
invalid-type-variable-bound = "error"
|
||||
invalid-type-variable-constraints = "error"
|
||||
invalid-typed-dict-header = "error"
|
||||
invalid-typed-dict-statement = "error"
|
||||
override-of-final-method = "error"
|
||||
override-of-final-variable = "error"
|
||||
possibly-missing-import = "error"
|
||||
possibly-missing-submodule = "error"
|
||||
# Has false positives due to ty walrus operator bug.
|
||||
# possibly-unresolved-reference = "error"
|
||||
raw-string-type-annotation = "error"
|
||||
redundant-final-classvar = "error"
|
||||
shadowed-type-variable = "error"
|
||||
subclass-of-final-class = "error"
|
||||
super-call-in-named-tuple-method = "error"
|
||||
unavailable-implicit-super-arguments = "error"
|
||||
unbound-type-variable = "error"
|
||||
undefined-reveal = "error"
|
||||
unresolved-global = "error"
|
||||
unresolved-import = "error"
|
||||
unresolved-reference = "error"
|
||||
unused-ignore-comment = "error"
|
||||
unused-type-ignore-comment = "error"
|
||||
useless-overload-body = "error"
|
||||
|
||||
# Non-structural rules:
|
||||
deprecated = "error"
|
||||
zero-stepsize-in-slice = "error"
|
||||
possibly-missing-implicit-call = "error"
|
||||
unused-awaitable = "error"
|
||||
|
||||
# Function argument rules:
|
||||
# Conflicts with `ifcopenshell.api.geometry.add_representation` type of callables we have, confusing them with a module.
|
||||
call-non-callable = "ignore"
|
||||
# bpy is missing some context manager implementations.
|
||||
invalid-context-manager = "ignore"
|
||||
# Doesn't go well with `bpy.ops.xxx.yyy`.
|
||||
unresolved-attribute = "ignore"
|
||||
# call-non-callable = "error"
|
||||
conflicting-argument-forms = "error"
|
||||
# Too many false positives.
|
||||
invalid-argument-type = "ignore"
|
||||
invalid-method-override = "ignore"
|
||||
invalid-assignment = "ignore"
|
||||
invalid-parameter-default = "ignore"
|
||||
missing-override-decorator = "ignore"
|
||||
invalid-yield = "ignore"
|
||||
invalid-return-type = "ignore"
|
||||
non-callable-init-subclass = "ignore"
|
||||
not-iterable = "ignore"
|
||||
possibly-missing-attribute = "ignore"
|
||||
no-matching-overload = "ignore"
|
||||
not-subscriptable = "ignore"
|
||||
unsupported-dynamic-base = "ignore"
|
||||
unsupported-operator = "ignore"
|
||||
# invalid-argument-type = "error"
|
||||
missing-argument = "error"
|
||||
parameter-already-assigned = "error"
|
||||
positional-only-parameter-as-kwarg = "error"
|
||||
too-many-positional-arguments = "error"
|
||||
unknown-argument = "error"
|
||||
# Has a lot of warnings due to current ty walrus operator issues.
|
||||
# index-out-of-bounds = "error"
|
||||
# unresolved-attribute = "error"
|
||||
|
||||
[tool.ty.environment]
|
||||
extra-paths = [
|
||||
@@ -155,19 +215,6 @@ 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 pip install -e ./src/bsdd/"},
|
||||
{cmd = "uv pip install -e ./src/ifcopenshell-python/[advanced,dev]"},
|
||||
{cmd = "uv pip install -e ./src/ifcedit/"},
|
||||
{cmd = "uv pip install -e ./src/ifcpatch/"},
|
||||
{cmd = "uv pip install -e ./src/ifcquery/"},
|
||||
{cmd = "uv pip install -e './src/ifcmcp/[mcp]'"},
|
||||
{cmd = "uv pip install -r src/bonsai/requirements-dev.txt"},
|
||||
]
|
||||
dev-setup.help = "Install repo packages in editable mode"
|
||||
|
||||
ruff = "ruff check"
|
||||
|
||||
black = "black ."
|
||||
@@ -193,9 +240,9 @@ format.sequence = ["black", "ruff"]
|
||||
cmake-format = "gersemi . --in-place"
|
||||
|
||||
[tool.poe.tasks.ty-ios]
|
||||
# --ignore unresolved-reference: walrus operator false positives in ty.
|
||||
cmd = """
|
||||
ty check
|
||||
nix/
|
||||
src/bcf
|
||||
src/bsdd
|
||||
src/ifc2ca
|
||||
@@ -210,6 +257,7 @@ cmd = """
|
||||
src/ifcpatch
|
||||
src/ifctester
|
||||
--python=src/ifcopenshell-python/.venv
|
||||
--ignore unresolved-reference
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.bonsai-deps]
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
black==26.3.1
|
||||
ruff==0.16.0
|
||||
poethepoet
|
||||
ty==0.0.63
|
||||
gersemi==0.28.0
|
||||
@@ -188,8 +188,6 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
@@ -208,8 +206,6 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
@@ -226,8 +222,6 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
|
||||
@@ -24,28 +24,10 @@ a text, a tspan { fill: blue !important; text-decoration: underline;}
|
||||
a:hover { cursor: pointer; }
|
||||
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
|
||||
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
/* SVG edge classification (issue #3668): see edge-classification.md. These select directly on
|
||||
the <path> element (each classified projection edge carries its own class), so they win over
|
||||
the inherited .projection rule above regardless of specificity. */
|
||||
path.outline { stroke: black; stroke-width: 0.35; stroke-opacity: 1; }
|
||||
path.boundary { stroke: black; stroke-width: 0.3; stroke-opacity: 0.9; }
|
||||
path.crease { stroke: black; stroke-width: 0.25; stroke-opacity: 0.85; }
|
||||
path.sharp { stroke: black; stroke-width: 0.18; stroke-opacity: 0.7; }
|
||||
path.flush { stroke: black; stroke-width: 0.1; stroke-opacity: 0.4; }
|
||||
|
||||
/* Debug CSS for troubleshooting edge classification */
|
||||
/*
|
||||
path.outline { stroke: black; stroke-width: 0.35; stroke-opacity: 1; }
|
||||
path.boundary { stroke: orange; stroke-width: 0.3; stroke-opacity: 0.9; }
|
||||
path.crease { stroke: green; stroke-width: 0.25; stroke-opacity: 0.85; }
|
||||
path.sharp { stroke: red; stroke-width: 0.18; stroke-opacity: 0.7; }
|
||||
path.flush { stroke: blue; stroke-width: 0.1; stroke-opacity: 0.4; }
|
||||
*/
|
||||
|
||||
.surface {fill: white; stroke-width: 0.1;}
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
|
||||
/* .IfcGeographicElement { fill: none; stroke: rgb(150, 150, 150); stroke-linecap: 'round'; stroke-dasharray: 1, 2;} */
|
||||
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }
|
||||
.PredefinedType-LINEWORK { stroke: black; stroke-width: 0.25; }
|
||||
.PredefinedType-LINEWORK.dashed { stroke-dasharray: 3, 2; }
|
||||
.PredefinedType-LINEWORK.fine { stroke-width: 0.18; stroke: #777777; }
|
||||
|
||||
@@ -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));
|
||||
#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.);
|
||||
@@ -35,12 +35,5 @@ DATA;
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#29=IFCSIMPLEPROPERTYTEMPLATE('0lP6Y8q9v2QhDnR4sT7uVx',$,'PerspectiveShiftX','Horizontal perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#30=IFCSIMPLEPROPERTYTEMPLATE('2mR8b1NcW5EoFyG7hJ9kLp',$,'PerspectiveShiftY','Vertical perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#31=IFCSIMPLEPROPERTYTEMPLATE('1cFVJnqT13m8ItkMHaI1tp',$,'UseEdgeClassification','Enable the boundary/outline/sharp/crease/flush SVG edge classification scheme (issue #3668). When false, drawings use the original unclassified linework.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#32=IFCSIMPLEPROPERTYTEMPLATE('2kB$mxBgnBUvhjh0Ti0c4P',$,'RenderCreases','Whether to render ''crease'' (concave) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#33=IFCSIMPLEPROPERTYTEMPLATE('3MSIJNW$T8r9Hl12kk0BY$',$,'ValleyAngleMinDegrees','Minimum concave dihedral deviation from flat, in degrees, for a projection edge to be classified as ''crease'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#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>
|
||||
|
||||
@@ -320,11 +320,9 @@ def loadIfcStore(scene: bpy.types.Scene) -> None:
|
||||
IfcStore.purge()
|
||||
refresh_ui_data()
|
||||
if not tool.Ifc.get():
|
||||
tool.Autosave.cancel_timer()
|
||||
return
|
||||
tool.Ifc.schema()
|
||||
IfcStore.relink_all_objects()
|
||||
tool.Autosave.reset_timer()
|
||||
|
||||
|
||||
@persistent
|
||||
@@ -418,10 +416,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():
|
||||
|
||||
@@ -46,7 +46,7 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
|
||||
class OperationData(TypedDict):
|
||||
id: int
|
||||
guid: NotRequired[str]
|
||||
obj: NotRequired[str]
|
||||
obj: str
|
||||
|
||||
|
||||
class EditObjectOperationData(TypedDict):
|
||||
|
||||
@@ -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"]:
|
||||
@@ -976,13 +980,8 @@ class IfcImporter:
|
||||
if unit.Name == "METRE":
|
||||
if not unit.Prefix:
|
||||
bpy.context.scene.unit_settings.length_unit = "METERS"
|
||||
elif f"{unit.Prefix}METERS" in ("KILOMETERS", "CENTIMETERS", "MILLIMETERS", "MICROMETERS"):
|
||||
bpy.context.scene.unit_settings.length_unit = f"{unit.Prefix}METERS"
|
||||
else:
|
||||
# Blender's length_unit enum has no entry for other
|
||||
# SI prefixes (e.g. DECIMETERS), so fall back to
|
||||
# adaptive display instead of failing to open.
|
||||
bpy.context.scene.unit_settings.length_unit = "ADAPTIVE"
|
||||
bpy.context.scene.unit_settings.length_unit = f"{unit.Prefix}METERS"
|
||||
else:
|
||||
bpy.context.scene.unit_settings.system = "IMPERIAL"
|
||||
name = unit.Name.lower()
|
||||
@@ -1099,14 +1098,12 @@ class IfcImporter:
|
||||
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
polyline = None
|
||||
for edge in edges:
|
||||
v1 = vertices[edge[0]]
|
||||
if v1 != v2:
|
||||
polyline = curve.splines.new("POLY")
|
||||
polyline.points[-1].co = mathutils.Vector(v1)
|
||||
v2 = vertices[edge[1]]
|
||||
assert polyline is not None
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = mathutils.Vector(v2)
|
||||
edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist()
|
||||
|
||||
@@ -843,6 +843,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, opening)
|
||||
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
|
||||
mat.translation = (0, 0, 0)
|
||||
opening_bm = bmesh.new()
|
||||
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
|
||||
for vert in verts:
|
||||
@@ -1059,7 +1060,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return tool.Ifc.get().createIfcConnectionSurfaceGeometry(surface)
|
||||
|
||||
def export_surface(self, polygon, target_face_matrix):
|
||||
ifc_file = tool.Ifc.get()
|
||||
x_axis = target_face_matrix.col[0][:3]
|
||||
z_axis = target_face_matrix.col[2][:3]
|
||||
p1 = target_face_matrix.translation
|
||||
@@ -1072,20 +1072,18 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
placement = builder.create_axis2_placement_3d([o / self.unit_scale for o in p1], z_axis, x_axis)
|
||||
surface.BasisSurface = tool.Ifc.get().create_entity("IfcPlane", placement)
|
||||
|
||||
schema = ifc_file.schema
|
||||
if schema != "IFC2X3":
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in polygon.exterior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
outer_boundary = tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False)
|
||||
|
||||
inner_boundaries: list[ifcopenshell.entity_instance] = []
|
||||
inner_boundaries = []
|
||||
for interior in polygon.interiors:
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in interior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
inner_boundaries.append(tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False))
|
||||
else:
|
||||
# TODO:
|
||||
raise NotImplementedError(schema)
|
||||
pass # TODO
|
||||
|
||||
surface.OuterBoundary = outer_boundary
|
||||
surface.InnerBoundaries = inner_boundaries
|
||||
|
||||
@@ -156,13 +156,16 @@ class BrickschemaReferencesData:
|
||||
for rel in getattr(tool.Ifc.get_entity(bpy.context.active_object), "HasAssociations", []):
|
||||
if rel.is_a("IfcRelAssociatesLibrary"):
|
||||
reference = rel.RelatingLibrary
|
||||
identification = tool.Document.get_external_reference_id(reference)
|
||||
if not identification or "#" not in identification:
|
||||
if tool.Ifc.get_schema() == "IFC2X3" and "#" not in reference.ItemReference:
|
||||
continue
|
||||
if tool.Ifc.get_schema() != "IFC2X3" and "#" not in reference.Identification:
|
||||
continue
|
||||
results.append(
|
||||
{
|
||||
"id": reference.id(),
|
||||
"identification": identification,
|
||||
"identification": (
|
||||
reference.ItemReference if tool.Ifc.get_schema() == "IFC2X3" else reference.Identification
|
||||
),
|
||||
"name": reference.Name or "Unnamed",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -400,7 +400,6 @@ class CadOffset(bpy.types.Operator):
|
||||
[verts.update(e.verts) for e in edges]
|
||||
|
||||
# Use the viewport angle to determine the offset direction
|
||||
wp = None
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
# Don't ask me, I don't know.
|
||||
@@ -410,7 +409,6 @@ class CadOffset(bpy.types.Operator):
|
||||
z = area.spaces.active.region_3d.view_rotation @ Vector((0, 0, 1))
|
||||
wp = Matrix([x, y, z, Vector((0, 0, 0))]).to_4x4().transposed()
|
||||
break
|
||||
assert wp is not None
|
||||
|
||||
rotation = Matrix.Rotation(pi / 2, 2, "Z")
|
||||
rotation_i = Matrix.Rotation(-pi / 2, 2, "Z")
|
||||
|
||||
@@ -478,7 +478,6 @@ class ChangeClassificationLevel(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Classification.get_classification_props()
|
||||
props.available_library_references.clear()
|
||||
reference = None
|
||||
for reference in IfcStore.classification_file.by_id(self.parent_id).HasReferences:
|
||||
new = props.available_library_references.add()
|
||||
new.identification = reference.Identification or ""
|
||||
@@ -486,7 +485,6 @@ class ChangeClassificationLevel(bpy.types.Operator):
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.has_references = bool(reference.HasReferences)
|
||||
new.referenced_source
|
||||
assert reference
|
||||
if reference.ReferencedSource.is_a("IfcClassificationReference"):
|
||||
props.active_library_referenced_source = reference.ReferencedSource.ReferencedSource.id()
|
||||
else:
|
||||
|
||||
@@ -156,8 +156,6 @@ class CostSchedulesData:
|
||||
values = root_element.CostValues
|
||||
elif root_element.is_a("IfcConstructionResource"):
|
||||
values = root_element.BaseCosts
|
||||
else:
|
||||
assert False, root_element
|
||||
for cost_value in values or []:
|
||||
cls._load_cost_value(root_element, data, cost_value)
|
||||
# data["CostValues"].append(cost_value.id())
|
||||
|
||||
@@ -127,25 +127,36 @@ class ObjectDocumentData:
|
||||
identification = None
|
||||
|
||||
if is_information:
|
||||
identification = tool.Document.get_document_information_id(relating_document)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
identification = relating_document.DocumentId
|
||||
else:
|
||||
identification = relating_document.Identification
|
||||
|
||||
location = getattr(relating_document, "Location", None)
|
||||
description = getattr(relating_document, "Description", "No description")
|
||||
else:
|
||||
description = relating_document.Description
|
||||
referenced_document = tool.Document.get_reference_document(relating_document)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
reference_to_document = relating_document.ReferenceToDocument
|
||||
if not name and reference_to_document:
|
||||
name = reference_to_document[0].Name
|
||||
|
||||
if not name and referenced_document:
|
||||
name = referenced_document.Name
|
||||
identification = relating_document.ItemReference
|
||||
if not identification and reference_to_document:
|
||||
identification = reference_to_document[0].DocumentId
|
||||
location = relating_document.Location
|
||||
else:
|
||||
referenced_document = relating_document.ReferencedDocument
|
||||
if not name and referenced_document:
|
||||
name = referenced_document.Name
|
||||
|
||||
identification = tool.Document.get_external_reference_id(relating_document)
|
||||
if not identification and referenced_document:
|
||||
identification = tool.Document.get_document_information_id(referenced_document)
|
||||
identification = relating_document.Identification
|
||||
if not identification and referenced_document:
|
||||
identification = referenced_document.Identification
|
||||
|
||||
location = relating_document.Location
|
||||
# IFC2X3 IfcDocumentInformation has no Location to fall back to.
|
||||
if location is None and referenced_document and tool.Ifc.get_schema() != "IFC2X3":
|
||||
location = referenced_document.Location
|
||||
location = relating_document.Location
|
||||
if location is None and referenced_document:
|
||||
location = referenced_document.Location
|
||||
|
||||
location = cls.convert_to_file_uri(location) if location else None
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -425,12 +425,10 @@ class BaseDecorator:
|
||||
|
||||
blf.size(font_id, font_size_px)
|
||||
|
||||
w, h = None, None
|
||||
if box_alignment or center or vcenter:
|
||||
w, h = blf.dimensions(font_id, text)
|
||||
|
||||
if box_alignment:
|
||||
assert w is not None and h is not None
|
||||
box_alignment_offset = Vector((0, 0))
|
||||
if "bottom" in box_alignment:
|
||||
pass
|
||||
@@ -452,12 +450,10 @@ class BaseDecorator:
|
||||
else:
|
||||
# horizontal centering
|
||||
if center:
|
||||
assert w is not None
|
||||
pos -= Vector((cos, sin)) * w * 0.5
|
||||
|
||||
# vertical centering
|
||||
if vcenter:
|
||||
assert h is not None
|
||||
pos -= Vector((-sin, cos)) * h * 0.5
|
||||
|
||||
# side-shifting
|
||||
@@ -1005,8 +1001,6 @@ class FallDecorator(BaseDecorator):
|
||||
O = A.copy()
|
||||
O.z = B.z
|
||||
run = (B - O).length
|
||||
|
||||
angle_tg = None
|
||||
if run != 0:
|
||||
angle_tg = rise / run
|
||||
angle = round(degrees(atan(angle_tg)))
|
||||
@@ -1024,7 +1018,6 @@ class FallDecorator(BaseDecorator):
|
||||
elif object_type == "SLOPE_PERCENT":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
assert angle_tg is not None
|
||||
return f"{round(angle_tg * 100)} %"
|
||||
return "NO DATA"
|
||||
|
||||
@@ -1256,7 +1249,6 @@ class SectionLevelDecorator(BaseDecorator):
|
||||
}
|
||||
|
||||
# process edges
|
||||
text_position, text_dir = None, None
|
||||
for edge in edges_original:
|
||||
v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]]
|
||||
start_i = len(output_verts)
|
||||
@@ -1562,39 +1554,32 @@ class SectionDecorator(BaseDecorator):
|
||||
v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]]
|
||||
start_i = len(output_verts)
|
||||
|
||||
circle_head = None
|
||||
if display_start_circle or display_end_circle:
|
||||
circle_head = get_circle_head(circle_size)
|
||||
|
||||
triangle_head, divider_offset, edge_dir_circle = None, None, None
|
||||
display_symbol = display_start_symbol or display_end_symbol
|
||||
if display_symbol or connect_markers:
|
||||
if display_start_symbol or display_end_symbol or connect_markers:
|
||||
edge_dir = (v1 - v0).normalized()
|
||||
side = (edge_dir.yx * Vector((1, -1))).to_3d()
|
||||
edge_dir_circle = edge_dir * circle_size
|
||||
|
||||
if display_symbol:
|
||||
triangle_head = get_triangle_head(edge_dir, -side, triangle_length, triangle_width)
|
||||
divider_offset = []
|
||||
divider_offset.append(edge_dir_circle if connect_markers else edge_dir_circle * 3)
|
||||
divider_offset.append(edge_dir_circle)
|
||||
if display_start_symbol or display_end_symbol:
|
||||
triangle_head = get_triangle_head(edge_dir, -side, triangle_length, triangle_width)
|
||||
divider_offset = []
|
||||
divider_offset.append(edge_dir_circle if connect_markers else edge_dir_circle * 3)
|
||||
divider_offset.append(edge_dir_circle)
|
||||
|
||||
if display_start_circle:
|
||||
assert circle_head is not None
|
||||
start_i = add_verts_sequence([v + v0 for v in circle_head], start_i, **out_kwargs, closed=True)
|
||||
# circle middle divider
|
||||
if not display_start_symbol:
|
||||
assert divider_offset is not None
|
||||
start_i = add_verts_sequence(
|
||||
[v0 + divider_offset[0], v0 - divider_offset[1]], start_i, **out_kwargs
|
||||
)
|
||||
|
||||
if display_start_symbol:
|
||||
assert triangle_head is not None
|
||||
start_i = add_verts_sequence([v + v0 for v in triangle_head], start_i, **out_kwargs, closed=True)
|
||||
|
||||
if display_end_circle:
|
||||
assert circle_head is not None
|
||||
start_i = add_verts_sequence([v + v1 for v in circle_head], start_i, **out_kwargs, closed=True)
|
||||
# circle middle divider
|
||||
if not display_end_symbol:
|
||||
@@ -1603,11 +1588,9 @@ class SectionDecorator(BaseDecorator):
|
||||
)
|
||||
|
||||
if display_end_symbol:
|
||||
assert triangle_head is not None
|
||||
start_i = add_verts_sequence([v + v1 for v in triangle_head], start_i, **out_kwargs, closed=True)
|
||||
|
||||
if connect_markers:
|
||||
assert edge_dir_circle is not None
|
||||
gap = []
|
||||
gap.append(edge_dir_circle if display_start_symbol else Vector((0, 0, 0)))
|
||||
gap.append(edge_dir_circle if display_end_symbol else Vector((0, 0, 0)))
|
||||
@@ -1694,12 +1677,6 @@ class CutDecorator:
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
self.fallback_colour = (0.3, 0.3, 0.3, 1)
|
||||
|
||||
# Evaluate camera movement once per redraw rather than twice per object: is_camera_moved()
|
||||
# runs eval()/numpy on the camera matrix and, as a side effect, refreshes the stored
|
||||
# checksum on the first True result - so calling it per object also made the second call
|
||||
# (fill) see an already-updated checksum and skip recalculating when it shouldn't.
|
||||
self.camera_moved = self.is_camera_moved()
|
||||
|
||||
all_vertices = []
|
||||
all_edges = []
|
||||
selected_vertices = []
|
||||
@@ -1825,35 +1802,23 @@ class CutDecorator:
|
||||
|
||||
# Currently selected objects must be recalculated as they may be being moved / edited.
|
||||
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
||||
is_selected = obj.select_get()
|
||||
recalc_cut = not has_cut_cache or is_selected or self.camera_moved
|
||||
recalc_fill = not has_fill_cache or is_selected or self.camera_moved
|
||||
if not (recalc_cut or recalc_fill):
|
||||
return
|
||||
|
||||
# The intersection test builds a bmesh and scans every vertex; both recalculations need
|
||||
# the same answer, so compute it once here rather than once in each.
|
||||
is_intersecting = tool.Drawing.is_intersecting_camera(obj, context.scene.camera)
|
||||
if recalc_cut:
|
||||
self.recalculate_cut(context, obj, element, is_intersecting)
|
||||
if recalc_fill:
|
||||
self.recalculate_fill(context, obj, element, is_intersecting)
|
||||
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_cut(context, obj, element)
|
||||
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(
|
||||
self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance, is_intersecting: bool
|
||||
) -> None:
|
||||
if is_intersecting:
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
if tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
|
||||
DecoratorData.cut_cache[element.id()] = (verts, edges)
|
||||
else:
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
|
||||
def recalculate_fill(
|
||||
self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance, is_intersecting: bool
|
||||
) -> None:
|
||||
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
element_id = element.id()
|
||||
|
||||
if not is_intersecting:
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
DecoratorData.fill_cache[element_id] = {}
|
||||
return
|
||||
|
||||
@@ -1906,8 +1871,6 @@ class CutDecorator:
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
else:
|
||||
assert False, material
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
material = layer_set.MaterialLayers[0].Material
|
||||
@@ -1934,8 +1897,6 @@ class CutDecorator:
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
else:
|
||||
assert False, usage
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
|
||||
|
||||
@@ -82,15 +82,7 @@ import math
|
||||
from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
ClassVar,
|
||||
Literal,
|
||||
Optional,
|
||||
Protocol,
|
||||
runtime_checkable,
|
||||
)
|
||||
from typing import Any, ClassVar, Literal, Optional, Protocol, runtime_checkable
|
||||
|
||||
import blf
|
||||
import bpy
|
||||
@@ -113,9 +105,6 @@ from mathutils.kdtree import KDTree
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.drawing.shaders import ExtrusionGuidesShader
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bmesh
|
||||
|
||||
SNAP_POINT_SIZE = 10.0
|
||||
SNAP_POINT_COLOR = (1.0, 0.5, 0.0, 1.0)
|
||||
SNAP_MAX_RADIUS = 50.0
|
||||
@@ -2046,9 +2035,7 @@ class TexturedQuadGizmoMixin(StaticTrisGizmoMixin):
|
||||
|
||||
def setup(self) -> None:
|
||||
super().setup()
|
||||
from bonsai.bim.module.drawing import (
|
||||
gizmo_textures, # ty: ignore[unresolved-import]
|
||||
)
|
||||
from bonsai.bim.module.drawing import gizmo_textures
|
||||
|
||||
self._quad_batch = batch_for_shader(
|
||||
gizmo_textures.get_shader(),
|
||||
@@ -2057,9 +2044,7 @@ class TexturedQuadGizmoMixin(StaticTrisGizmoMixin):
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
from bonsai.bim.module.drawing import (
|
||||
gizmo_textures, # ty: ignore[unresolved-import]
|
||||
)
|
||||
from bonsai.bim.module.drawing import gizmo_textures
|
||||
|
||||
texture = gizmo_textures.get_icon_texture(self.icon_name)
|
||||
if texture is None:
|
||||
|
||||
@@ -189,20 +189,14 @@ def format_distance(
|
||||
if hasattr(length_unit, "Prefix") and length_unit.Prefix:
|
||||
unit_length = length_unit.Prefix + length_unit.Name
|
||||
unit_length_mapping = {
|
||||
"MILE": "MILES",
|
||||
"FOOT": "FEET",
|
||||
"INCH": "INCHES",
|
||||
"KILOMETRE": "KILOMETERS",
|
||||
"METRE": "METERS",
|
||||
"DECIMETRE": "DECIMETERS",
|
||||
"CENTIMETRE": "CENTIMETERS",
|
||||
"MILLIMETRE": "MILLIMETERS",
|
||||
"MICROMETRE": "MICROMETERS",
|
||||
}
|
||||
# Fall through for units without a dedicated formatter (e.g.
|
||||
# HECTOMETRE) so they use the adaptive branch instead of a
|
||||
# KeyError (#8255).
|
||||
unit_length = unit_length_mapping.get(unit_length, unit_length)
|
||||
unit_length = unit_length_mapping[unit_length]
|
||||
# For now we only format area in IFC Units
|
||||
if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"):
|
||||
area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit)
|
||||
@@ -225,11 +219,9 @@ def format_distance(
|
||||
unit_system, unit_length, unit_fraction = unit_mapping[custom_unit]
|
||||
|
||||
value *= unit_scale
|
||||
tx_dist = None
|
||||
|
||||
# Imperial Formatting
|
||||
if unit_system == "IMPERIAL":
|
||||
toInches = None
|
||||
if in_unit_length:
|
||||
if unit_length == "INCHES":
|
||||
toInches = 1
|
||||
@@ -243,7 +235,6 @@ def format_distance(
|
||||
toInches = 1550
|
||||
inPerFoot = 144
|
||||
|
||||
assert toInches is not None
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
@@ -386,7 +377,6 @@ def format_distance(
|
||||
if precision and isinstance(precision, float):
|
||||
value = precision * round(float(value) / precision)
|
||||
|
||||
fmt = None
|
||||
if decimal_places is not None:
|
||||
fmt = "%1." + str(decimal_places) + "f"
|
||||
|
||||
@@ -469,7 +459,6 @@ def format_distance(
|
||||
assert f"Unexpected unit_system - '{unit_system}'."
|
||||
# tx_dist = fmt % value
|
||||
|
||||
assert tx_dist is not None
|
||||
return tx_dist
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -782,8 +698,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
else:
|
||||
assert False, material
|
||||
|
||||
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
||||
|
||||
@@ -808,6 +722,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
|
||||
bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges)
|
||||
|
||||
prev_co = None
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
@@ -824,8 +739,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
else:
|
||||
assert False, usage
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
@@ -993,10 +906,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
||||
return svg_path
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
semantics = None
|
||||
pairs = None
|
||||
|
||||
# in case of printing multiple drawings we need to sync just once
|
||||
if self.sync and self.drawing_index == 0:
|
||||
with profile("sync"):
|
||||
@@ -1042,12 +951,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
tree = ifcopenshell.geom.tree()
|
||||
tree.enable_face_styles(True)
|
||||
|
||||
# Accumulated across every file in the loop below (main model plus any
|
||||
# linked models) so the SHAPELY fill pass after the loop covers all of
|
||||
# them, not just whichever file happened to be processed last.
|
||||
raycast_objs = set()
|
||||
elements_with_faces = set()
|
||||
|
||||
for ifc_path, (ifc, link_matrix) in files.items():
|
||||
# Don't use draw.main() just whilst we're prototyping and experimenting
|
||||
# TODO: hash paths are never used
|
||||
@@ -1057,24 +960,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.serialiser.setFile(ifc)
|
||||
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
|
||||
|
||||
if self.cprops.fill_mode == "SHAPELY":
|
||||
for element in drawing_elements.copy():
|
||||
if element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj and obj.type == "MESH" and len(obj.data.polygons):
|
||||
elements_with_faces.add(element.GlobalId)
|
||||
raycast_objs.add(obj)
|
||||
|
||||
# Get all representation contexts to see what we're dealing with.
|
||||
# Drawings only draw bodies and annotations (and facetation, due to a Revit bug).
|
||||
# A drawing prioritises a target view context first, followed by a model view context as a fallback.
|
||||
# Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised.
|
||||
contexts = self.get_linework_contexts(ifc, target_view)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view, link_matrix)
|
||||
self.serialize_contexts_elements(
|
||||
ifc, tree, contexts, "annotation", drawing_elements, target_view, link_matrix
|
||||
)
|
||||
self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view, link_matrix)
|
||||
|
||||
if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements:
|
||||
with profile("Camera element"):
|
||||
@@ -1141,6 +1033,16 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# shapely variant
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
|
||||
raycast_objs = set()
|
||||
elements_with_faces = set()
|
||||
for element in drawing_elements.copy():
|
||||
if element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj and obj.type == "MESH" and len(obj.data.polygons):
|
||||
elements_with_faces.add(element.GlobalId)
|
||||
raycast_objs.add(obj)
|
||||
|
||||
projections = root.xpath(
|
||||
".//svg:g[contains(@class, 'projection')]", namespaces={"svg": "http://www.w3.org/2000/svg"}
|
||||
)
|
||||
@@ -1400,18 +1302,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.svg_settings = ifcopenshell.geom.settings()
|
||||
self.svg_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
self.svg_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
|
||||
# SVG edge classification (issue #3668). See edge-classification.md. Settings are
|
||||
# per-drawing, stored in EPset_Drawing and read into self.cprops by import_camera_props.
|
||||
try:
|
||||
self.svg_settings.set("svg-use-edge-classification", self.cprops.use_edge_classification)
|
||||
self.svg_settings.set("svg-render-crease-edges", self.cprops.render_creases)
|
||||
self.svg_settings.set("svg-valley-angle-min-degrees", self.cprops.valley_angle_min_degrees)
|
||||
self.svg_settings.set("svg-render-sharp-edges", self.cprops.render_sharp)
|
||||
self.svg_settings.set("svg-ridge-angle-min-degrees", self.cprops.ridge_angle_min_degrees)
|
||||
self.svg_settings.set("svg-emit-flush-edges", self.cprops.render_flush)
|
||||
except Exception:
|
||||
# Backwards compatibility with older ifcopenshell builds that don't expose these keys.
|
||||
pass
|
||||
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
|
||||
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(
|
||||
@@ -1555,15 +1445,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 +1546,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 = []
|
||||
@@ -1817,12 +1699,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
key=lambda a: (
|
||||
tool.Drawing.get_annotation_z_index(a),
|
||||
1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0,
|
||||
# Deterministic tiebreaker so equal-priority annotations keep a
|
||||
# stable order across sessions. Without it the order comes from
|
||||
# the set union above, which depends on entity hashes (and thus
|
||||
# the file pointer), shuffling annotations between Blender
|
||||
# restarts. See #6608.
|
||||
a.id(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2310,11 +2186,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"}
|
||||
@@ -2464,9 +2336,7 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
"Activates the selected drawing view.\n\n"
|
||||
+ "ALT+CLICK to keep the viewport position.\n\n"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
|
||||
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views, "
|
||||
+ "then select their cameras (the first selected drawing's camera becomes active).\n\n"
|
||||
+ "SHIFT+CTRL+ALT+CLICK to do the same but also select the annotations, not just the cameras"
|
||||
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views"
|
||||
)
|
||||
|
||||
drawing: bpy.props.IntProperty()
|
||||
@@ -2488,25 +2358,16 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
include_annotations_in_selection: bpy.props.BoolProperty(
|
||||
name="Include Annotations In Selection",
|
||||
description="Also select the loaded annotation objects, not just the drawing cameras.",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
drawing: int
|
||||
should_view_from_camera: bool
|
||||
use_quick_preview: bool
|
||||
load_selected_annotations: bool
|
||||
include_annotations_in_selection: bool
|
||||
|
||||
def invoke(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
|
||||
if event.type == "LEFTMOUSE" and event.shift and event.ctrl:
|
||||
self.load_selected_annotations = True
|
||||
if event.alt:
|
||||
self.include_annotations_in_selection = True
|
||||
return self.execute(context)
|
||||
if event.type == "LEFTMOUSE" and event.alt:
|
||||
self.should_view_from_camera = False
|
||||
@@ -2521,34 +2382,15 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
bpy.ops.bim.load_drawings()
|
||||
|
||||
if self.load_selected_annotations:
|
||||
objs_to_select = []
|
||||
active_camera = None
|
||||
for d in props.drawings:
|
||||
if not (d.is_drawing and d.is_selected):
|
||||
continue
|
||||
selected_drawing = tool.Ifc.get().by_id(d.ifc_definition_id)
|
||||
# Importing the camera (if missing) ensures the drawing's
|
||||
# collection exists so the annotations get collected into it.
|
||||
if not (camera := tool.Ifc.get_object(selected_drawing)):
|
||||
camera = tool.Drawing.import_drawing(selected_drawing)
|
||||
group = tool.Drawing.get_drawing_group(selected_drawing)
|
||||
tool.Drawing.import_annotations_in_group(group)
|
||||
|
||||
if active_camera is None:
|
||||
active_camera = camera
|
||||
objs_to_select.append(camera)
|
||||
if self.include_annotations_in_selection:
|
||||
for element in tool.Drawing.get_group_elements(group) or []:
|
||||
if element.is_a("IfcAnnotation") and element.ObjectType != "DRAWING":
|
||||
if annotation_obj := tool.Ifc.get_object(element):
|
||||
objs_to_select.append(annotation_obj)
|
||||
|
||||
# Select the checked drawings' objects, with the first drawing's camera as active.
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in objs_to_select:
|
||||
obj.select_set(True)
|
||||
if active_camera is not None:
|
||||
context.view_layer.objects.active = active_camera
|
||||
if not tool.Ifc.get_object(selected_drawing):
|
||||
tool.Drawing.import_drawing(selected_drawing)
|
||||
tool.Drawing.import_annotations_in_group(tool.Drawing.get_drawing_group(selected_drawing))
|
||||
return {"FINISHED"}
|
||||
|
||||
drawing = tool.Ifc.get().by_id(self.drawing)
|
||||
@@ -2637,9 +2479,7 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
|
||||
"Activates the selected drawing view.\n\n"
|
||||
+ "ALT+CLICK to keep the viewport position.\n\n"
|
||||
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
|
||||
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views, "
|
||||
+ "then select their cameras (the first selected drawing's camera becomes active).\n\n"
|
||||
+ "SHIFT+CTRL+ALT+CLICK to do the same but also select the annotations, not just the cameras"
|
||||
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views"
|
||||
)
|
||||
|
||||
|
||||
@@ -3701,7 +3541,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if sheet.is_a("IfcDocumentInformation"):
|
||||
self.document_type = "SHEET"
|
||||
self.name = sheet.Name
|
||||
self.identification = tool.Document.get_document_information_id(sheet)
|
||||
self.identification = sheet.DocumentId if tool.Ifc.get_schema() == "IFC2X3" else sheet.Identification
|
||||
elif sheet.is_a("IfcDocumentReference") and tool.Drawing.get_reference_description(sheet) == "TITLEBLOCK":
|
||||
self.document_type = "TITLEBLOCK"
|
||||
else:
|
||||
@@ -3874,26 +3714,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"]
|
||||
@@ -543,50 +536,6 @@ class BIMCameraProperties(PropertyGroup):
|
||||
default=True,
|
||||
update=get_update_layer_callback("has_annotation", "HasAnnotation"),
|
||||
)
|
||||
use_edge_classification: BoolProperty(
|
||||
name="Use Edge Classification",
|
||||
description="Classify projection edges into boundary/outline/sharp/crease/flush "
|
||||
"instead of drawing all linework identically. See edge-classification.md",
|
||||
default=False,
|
||||
update=get_update_layer_callback("use_edge_classification", "UseEdgeClassification"),
|
||||
)
|
||||
render_creases: BoolProperty(
|
||||
name="Render Creases",
|
||||
description="Render 'crease' (concave) projection edges",
|
||||
default=True,
|
||||
update=get_update_layer_callback("render_creases", "RenderCreases"),
|
||||
)
|
||||
valley_angle_min_degrees: FloatProperty(
|
||||
name="Valley Angle Minimum",
|
||||
description="Minimum concave dihedral deviation from flat, in degrees, for a projection "
|
||||
"edge to be classified as 'crease' rather than 'flush'",
|
||||
default=12.0,
|
||||
min=0.0,
|
||||
max=180.0,
|
||||
update=get_update_layer_callback("valley_angle_min_degrees", "ValleyAngleMinDegrees"),
|
||||
)
|
||||
render_sharp: BoolProperty(
|
||||
name="Render Sharp",
|
||||
description="Render 'sharp' (convex) projection edges",
|
||||
default=True,
|
||||
update=get_update_layer_callback("render_sharp", "RenderSharp"),
|
||||
)
|
||||
ridge_angle_min_degrees: FloatProperty(
|
||||
name="Ridge Angle Minimum",
|
||||
description="Minimum convex dihedral deviation from flat, in degrees, for a projection "
|
||||
"edge to be classified as 'sharp' rather than 'flush'",
|
||||
default=45.0,
|
||||
min=0.0,
|
||||
max=180.0,
|
||||
update=get_update_layer_callback("ridge_angle_min_degrees", "RidgeAngleMinDegrees"),
|
||||
)
|
||||
render_flush: BoolProperty(
|
||||
name="Render Flush",
|
||||
description="Render 'flush' projection edges (dihedral deviation below both ridge/valley "
|
||||
"thresholds). Omitted by default",
|
||||
default=False,
|
||||
update=get_update_layer_callback("render_flush", "RenderFlush"),
|
||||
)
|
||||
target_view: EnumProperty(
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
|
||||
@@ -110,14 +110,12 @@ class Scheduler:
|
||||
y = self.margin
|
||||
rows = list(sheet.iter_rows())
|
||||
total_rows = len(rows)
|
||||
x = None
|
||||
for i, row in enumerate(rows):
|
||||
# The last row may contain only null values
|
||||
if i == (total_rows - 1) and not [c for c in row if c.value is not None]:
|
||||
continue
|
||||
|
||||
x = self.margin
|
||||
unmerged_height = None
|
||||
for cell in row:
|
||||
if isinstance(cell, openpyxl.cell.cell.MergedCell):
|
||||
column_letter = openpyxl.utils.get_column_letter(cell.column)
|
||||
@@ -232,11 +230,8 @@ class Scheduler:
|
||||
)
|
||||
|
||||
x += unmerged_width
|
||||
|
||||
assert unmerged_height is not None
|
||||
y += unmerged_height
|
||||
|
||||
assert x is not None
|
||||
total_width = x + self.margin
|
||||
total_height = y + self.margin
|
||||
self.svg["width"] = "{}mm".format(total_width)
|
||||
@@ -380,7 +375,6 @@ class Scheduler:
|
||||
tri = 0
|
||||
stop_iterating_over_rows = False
|
||||
# TODO: row spans support?
|
||||
x = None
|
||||
for tr in table.getElementsByType(TableRow):
|
||||
if stop_iterating_over_rows:
|
||||
break
|
||||
@@ -497,7 +491,6 @@ class Scheduler:
|
||||
tri += 1
|
||||
y += height
|
||||
|
||||
assert x is not None
|
||||
total_width = x + self.margin
|
||||
total_height = y + self.margin
|
||||
self.svg["width"] = "{}mm".format(total_width)
|
||||
|
||||
@@ -102,16 +102,16 @@ void angle_circle_head(
|
||||
in vec4 circle_start, in float circle_angle,
|
||||
in bool counterclockwise,
|
||||
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
|
||||
|
||||
|
||||
// 1 added to CIRCLE_SEGS because we're number of vertices
|
||||
// for n segments is n+1
|
||||
|
||||
|
||||
float angle_d;
|
||||
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
|
||||
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
|
||||
angle_segs = max(1, ceil(circle_angle / angle_d));
|
||||
angle_d = circle_angle / angle_segs;
|
||||
|
||||
|
||||
for(int i = 0; i < (angle_segs + 1); i++) {
|
||||
float angle = angle_d * i;
|
||||
if (counterclockwise) {
|
||||
@@ -143,7 +143,7 @@ void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
|
||||
#define do_vertex(pos, e) (do_vertex_util(pos, vec2(-(e).y, (e).x) / winsize.xy))
|
||||
#define do_vertex_win(pos, e) ( do_vertex( WIN2CLIP( pos ), e ) )
|
||||
|
||||
// if vertex is shared by two segments of the line still need to emit it twice
|
||||
// if vertex is shared by two segments of the line still need to emit it twice
|
||||
// to avoid smoothing artifacts
|
||||
// don't forget to initialize `vec2 EDGE_DIR` for macro to work
|
||||
// `pos0` / `pos1` - vertex position in clip space
|
||||
@@ -197,13 +197,10 @@ void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) {
|
||||
|
||||
def add_verts_sequence(verts, start_i, output_verts, output_edges, closed=False):
|
||||
"""Add sequence of verts to output lists, returns next vertex index"""
|
||||
i = None
|
||||
for i, v in enumerate(verts[:-1], start_i):
|
||||
output_verts.append(v)
|
||||
output_edges.append((i, i + 1))
|
||||
output_verts.append(verts[-1])
|
||||
assert i is not None
|
||||
|
||||
if closed:
|
||||
output_edges.append((i + 1, start_i))
|
||||
return i + 2
|
||||
@@ -276,7 +273,7 @@ class BaseShader:
|
||||
FRAG_GLSL = """
|
||||
uniform vec4 color;
|
||||
uniform float lineWidth;
|
||||
|
||||
|
||||
in float smoothline;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
|
||||
@@ -903,8 +903,12 @@ class SvgWriter:
|
||||
continue
|
||||
sheet = tool.Drawing.get_reference_document(sheet_reference)
|
||||
if sheet:
|
||||
reference_id = tool.Document.get_external_reference_id(sheet_reference) or "-"
|
||||
sheet_id = tool.Document.get_document_information_id(sheet) or "-"
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
reference_id = sheet_reference.ItemReference or "-"
|
||||
sheet_id = sheet.DocumentId or "-"
|
||||
else:
|
||||
reference_id = sheet_reference.Identification or "-"
|
||||
sheet_id = sheet.Identification or "-"
|
||||
return (reference_id, sheet_id)
|
||||
break
|
||||
return ("-", "-")
|
||||
@@ -1449,7 +1453,6 @@ class SvgWriter:
|
||||
angle_tg = rise / run
|
||||
angle = round(degrees(atan(angle_tg)))
|
||||
else:
|
||||
angle_tg = None
|
||||
angle = 90
|
||||
|
||||
# ues SLOPE_ANGLE as default
|
||||
@@ -1463,7 +1466,6 @@ class SvgWriter:
|
||||
elif object_type == "SLOPE_PERCENT":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
assert angle_tg is not None
|
||||
return f"{round(angle_tg * 100)} %"
|
||||
|
||||
tag = element.Description or get_label_text()
|
||||
|
||||
@@ -113,19 +113,6 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(props, "fill_mode")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "cut_mode")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "use_edge_classification")
|
||||
if props.use_edge_classification:
|
||||
row = self.layout.row()
|
||||
row.prop(props, "render_creases")
|
||||
row.prop(props, "valley_angle_min_degrees")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "render_sharp")
|
||||
row.prop(props, "ridge_angle_min_degrees")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "render_flush")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "width")
|
||||
row = self.layout.row()
|
||||
@@ -332,8 +319,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 +326,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 +858,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 +880,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 +900,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):
|
||||
@@ -1031,14 +964,14 @@ class BIM_UL_sheets(bpy.types.UIList):
|
||||
|
||||
if self.filter_name:
|
||||
filter_name = self.filter_name.lower()
|
||||
active_sheet_index = None
|
||||
active_sheet = None
|
||||
for sheet in data.sheets:
|
||||
if sheet.is_sheet:
|
||||
active_sheet = sheet
|
||||
active_sheet_index = len(flt_flags)
|
||||
if filter_name in sheet.name.lower() or filter_name in sheet.identification.lower():
|
||||
flt_flags.append(self.bitflag_filter_item)
|
||||
if not sheet.is_sheet:
|
||||
assert active_sheet_index is not None
|
||||
flt_flags[active_sheet_index] = self.bitflag_filter_item
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -69,6 +69,7 @@ classes = (
|
||||
operator.RemoveRepresentation,
|
||||
operator.RemoveRepresentationItem,
|
||||
operator.RemoveRepresentationItemFromShapeAspect,
|
||||
operator.SelectByRepresentationType,
|
||||
operator.SelectConnection,
|
||||
operator.SelectRepresentationItem,
|
||||
operator.SwitchRepresentation,
|
||||
|
||||
@@ -138,6 +138,11 @@ class RepresentationsData:
|
||||
"ContextType": representation.ContextOfItems.ContextType or "",
|
||||
"ContextIdentifier": "",
|
||||
"TargetView": "",
|
||||
# The representation's own identifier (e.g. 'Body', 'Reference'), which is
|
||||
# distinct from the subcontext's ContextIdentifier above. Two reps can share
|
||||
# one context (e.g. a Body body and a Reference opening template), so showing
|
||||
# this lets them be told apart in the panel.
|
||||
"RepresentationIdentifier": representation.RepresentationIdentifier or "",
|
||||
"RepresentationType": representation_type or "",
|
||||
"is_active": is_active,
|
||||
}
|
||||
|
||||
@@ -75,13 +75,9 @@ class Helper:
|
||||
for face in bm.faces:
|
||||
if len(face.verts) > 4:
|
||||
potential_faces.append(face)
|
||||
|
||||
# TODO: replace with next(..., None)
|
||||
face = None
|
||||
for face in potential_faces:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
assert face is not None
|
||||
|
||||
profile = [l.vert.index for l in face.loops]
|
||||
extrusion = self.detect_extrusion_edge(bm, face)
|
||||
@@ -112,12 +108,10 @@ class Helper:
|
||||
if not potential_faces:
|
||||
potential_faces = bm.faces
|
||||
|
||||
# TODO: replace with next(..., None)
|
||||
face = None
|
||||
for face in potential_faces:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
assert face is not None
|
||||
|
||||
profile = [l.vert.index for l in face.loops]
|
||||
extrusion = self.detect_extrusion_edge(bm, face)
|
||||
|
||||
@@ -151,12 +145,9 @@ class Helper:
|
||||
if total_verts > 4:
|
||||
potential_faces.append(face)
|
||||
|
||||
# TODO: replace with next(..., None)
|
||||
face = None
|
||||
for face in potential_faces:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
assert face is not None
|
||||
|
||||
end_faces = []
|
||||
end_face_normal = face.normal
|
||||
|
||||
@@ -418,6 +418,55 @@ class SelectConnection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
core.select_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
|
||||
|
||||
|
||||
class SelectByRepresentationType(bpy.types.Operator):
|
||||
bl_idname = "bim.select_by_representation_type"
|
||||
bl_label = "Select By Representation Type"
|
||||
bl_description = (
|
||||
"Select objects whose active representation matches this type. "
|
||||
"Ctrl+Click to also include objects that have this type in any representation (active or not)"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
representation_type: bpy.props.StringProperty()
|
||||
select_inactive: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.select_inactive = event.ctrl
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
ifc = tool.Ifc.get()
|
||||
if not ifc:
|
||||
return {"CANCELLED"}
|
||||
# Strip the "*" suffix used for mapped/resolved representations.
|
||||
target_type = self.representation_type.rstrip("*")
|
||||
matched = 0
|
||||
for obj in context.visible_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
if self.select_inactive:
|
||||
# Ctrl: match any representation on the element, active or not.
|
||||
has_type = any(
|
||||
(ifcopenshell.util.representation.resolve_representation(rep).RepresentationType or "") == target_type
|
||||
for rep in ifcopenshell.util.representation.get_representations_iter(element)
|
||||
)
|
||||
else:
|
||||
# Default: match only the currently active (displayed) representation.
|
||||
active_rep = tool.Geometry.get_active_representation(obj)
|
||||
if active_rep is None:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
resolved = ifcopenshell.util.representation.resolve_representation(active_rep)
|
||||
has_type = (resolved.RepresentationType or "") == target_type
|
||||
obj.select_set(has_type)
|
||||
if has_type:
|
||||
matched += 1
|
||||
mode = "any representation" if self.select_inactive else "active representation"
|
||||
self.report({"INFO"}, f"Selected {matched} object(s) with RepresentationType '{target_type}' ({mode})")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RemoveConnection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_connection"
|
||||
bl_label = "Remove Connection"
|
||||
@@ -581,11 +630,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if has_openings and not self.apply_openings:
|
||||
# Meshlike things with openings can only be updated without openings applied.
|
||||
if self.from_ui:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Object '{obj.name}' has openings. "
|
||||
"ALT+click the button to bake the openings into the new representation.",
|
||||
)
|
||||
self.report({"ERROR"}, f"Object '{obj.name}' has openings - representation cannot be updated.")
|
||||
return
|
||||
|
||||
if not product.is_a("IfcGridAxis"):
|
||||
@@ -714,6 +759,16 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if mprops.ifc_parameters:
|
||||
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
|
||||
|
||||
# Persist an edited opening void onto its filling type's 'Reference' template so the
|
||||
# change survives type duplication/append/switching and propagates to siblings. This
|
||||
# catches the edited_objs commit path; the in-place item edit is caught in
|
||||
# bim.override_mode_set_object.
|
||||
edited_element = tool.Ifc.get_entity(obj)
|
||||
if edited_element and edited_element.is_a("IfcOpeningElement"):
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
FilledOpeningGenerator().update_type_template_from_opening(edited_element)
|
||||
|
||||
|
||||
class UpdateParametricRepresentation(bpy.types.Operator):
|
||||
bl_idname = "bim.update_parametric_representation"
|
||||
@@ -2493,6 +2548,15 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return bpy.ops.bim.edit_boundary_geometry()
|
||||
elif tool.Geometry.is_representation_item(context.active_object):
|
||||
self.edit_representation_item(context.active_object)
|
||||
# If we just edited an opening's void item, persist the new shape onto the
|
||||
# filling type's 'Reference' template so it survives type duplication/append/
|
||||
# switching and propagates to siblings.
|
||||
rep_obj = tool.Geometry.get_geometry_props().representation_obj
|
||||
edited_element = tool.Ifc.get_entity(rep_obj) if rep_obj else None
|
||||
if edited_element and edited_element.is_a("IfcOpeningElement"):
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
|
||||
FilledOpeningGenerator().update_type_template_from_opening(edited_element)
|
||||
tool.Root.reload_item_decorator()
|
||||
# So you can keep hitting tab to cycle out of edit mode
|
||||
context.active_object.select_set(False)
|
||||
@@ -3527,15 +3591,12 @@ class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if props.representation_item_shape_aspect == "NEW":
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
# find IfcProductRepresentationSelect based on current representation
|
||||
product_shape = None
|
||||
if hasattr(element, "Representation"): # IfcProduct
|
||||
product_shape = element.Representation
|
||||
else: # IfcTypeProduct
|
||||
for representation_map in element.RepresentationMaps:
|
||||
if representation_map.MappedRepresentation == active_representation:
|
||||
product_shape = representation_map
|
||||
assert product_shape is not None
|
||||
|
||||
previous_shape_aspect_id = props.active_item.shape_aspect_id
|
||||
# will be None if item didn't had a shape aspect
|
||||
previous_shape_aspect = tool.Ifc.get_entity_by_id(previous_shape_aspect_id)
|
||||
@@ -3885,8 +3946,6 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
curve = builder.rectangle(size=Vector((0.5, 0.5)) / unit_scale)
|
||||
elif self.shape == "CYLINDER":
|
||||
curve = builder.circle(radius=0.25 / unit_scale)
|
||||
else:
|
||||
assert False, self.shape
|
||||
item = builder.extrude(
|
||||
curve,
|
||||
magnitude=0.5 / unit_scale,
|
||||
@@ -4124,31 +4183,6 @@ class OverrideMoveSelect(bpy.types.Operator):
|
||||
self.new_active_obj = obj
|
||||
return {"FINISHED"}
|
||||
|
||||
# Get arrays
|
||||
ifc_file = tool.Ifc.get()
|
||||
array_parents_to_move: list[bpy.types.Object] = []
|
||||
for obj in list(context.selected_objects):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset:
|
||||
continue
|
||||
parent_element = ifc_file.by_guid(pset["Parent"])
|
||||
parent_obj = tool.Ifc.get_object(parent_element)
|
||||
if parent_obj not in array_parents_to_move:
|
||||
array_parents_to_move.append(parent_obj)
|
||||
if element.GlobalId != pset["Parent"]:
|
||||
obj.select_set(False)
|
||||
|
||||
if array_parents_to_move:
|
||||
for parent_obj in array_parents_to_move:
|
||||
parent_element = tool.Ifc.get_entity(parent_obj)
|
||||
for array_obj in tool.Array.get_all_objects(parent_element):
|
||||
array_obj.select_set(True)
|
||||
self.new_active_obj = parent_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
# Get nests
|
||||
props = tool.Nest.get_nest_props()
|
||||
not_editing_objs = [o.obj for o in props.not_editing_objects]
|
||||
|
||||
@@ -148,12 +148,29 @@ class BIM_PT_representations(Panel):
|
||||
self.layout.label(text="No Representations Found")
|
||||
return
|
||||
|
||||
header = self.layout.row(align=True)
|
||||
header.label(text="Context")
|
||||
header.label(text="Subcontext")
|
||||
header.label(text="View")
|
||||
header.label(text="Identifier")
|
||||
header.label(text="Type")
|
||||
# Blank icon cells reserve the same width as the switch/remove buttons below so the
|
||||
# text columns line up with the data rows.
|
||||
header.label(text="", icon="BLANK1")
|
||||
header.label(text="", icon="BLANK1")
|
||||
|
||||
for representation in RepresentationsData.data["representations"]:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=representation["ContextType"])
|
||||
row.label(text=representation["ContextIdentifier"])
|
||||
row.label(text=representation["TargetView"])
|
||||
row.label(text=representation["RepresentationType"])
|
||||
row.label(text=representation["RepresentationIdentifier"])
|
||||
op = row.operator(
|
||||
"bim.select_by_representation_type",
|
||||
text=representation["RepresentationType"],
|
||||
emboss=False,
|
||||
)
|
||||
op.representation_type = representation["RepresentationType"]
|
||||
op = row.operator(
|
||||
"bim.switch_representation",
|
||||
icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -103,7 +103,9 @@ class LibraryReferencesData:
|
||||
results.append(
|
||||
{
|
||||
"id": library.id(),
|
||||
"identification": tool.Document.get_external_reference_id(library),
|
||||
"identification": (
|
||||
library.ItemReference if tool.Ifc.get_schema() == "IFC2X3" else library.Identification
|
||||
),
|
||||
"name": library.Name or "Unnamed",
|
||||
}
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user