mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60063ac1c7 | |||
| f2d3e226b4 | |||
| c0889c7f10 | |||
| e609f10559 | |||
| e70ce17431 |
@@ -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
|
||||
|
||||
@@ -27,7 +27,10 @@ jobs:
|
||||
- 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
|
||||
|
||||
@@ -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*
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -314,12 +314,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 +559,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 +571,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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
+5
-15
@@ -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,
|
||||
@@ -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,
|
||||
@@ -715,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):
|
||||
@@ -723,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)
|
||||
@@ -1172,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",
|
||||
@@ -1187,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
-40
@@ -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,18 +215,6 @@ exclude = [
|
||||
|
||||
[tool.poe.tasks]
|
||||
|
||||
dev-setup.sequence = [
|
||||
{cmd = "uv sync"},
|
||||
{cmd = "uv pip install -e ./src/bsdd/"},
|
||||
{cmd = "uv pip install -e ./src/ifcopenshell-python/[advanced,dev]"},
|
||||
{cmd = "uv pip install -e ./src/ifcedit/"},
|
||||
{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 ."
|
||||
@@ -192,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
|
||||
@@ -209,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.15.22
|
||||
poethepoet
|
||||
ty==0.0.61
|
||||
gersemi==0.26.1
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -120,10 +120,10 @@ class IfcExporter:
|
||||
# updata_representation will run edit_object_placement if object is scaled
|
||||
# and had no openings.
|
||||
return element
|
||||
if not tool.Ifc.is_moved(obj):
|
||||
return
|
||||
if element.is_a("IfcGridAxis"):
|
||||
return self.sync_grid_axis_object_placement(obj, element)
|
||||
if not tool.Ifc.is_moved(obj):
|
||||
return
|
||||
if not hasattr(element, "ObjectPlacement"):
|
||||
return
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
@@ -134,7 +134,8 @@ class IfcExporter:
|
||||
grid_obj = tool.Ifc.get_object(grid)
|
||||
if grid_obj:
|
||||
self.sync_object_placement(grid_obj)
|
||||
if grid_obj.matrix_world != obj.matrix_world:
|
||||
matrices_differ = grid_obj.matrix_world != obj.matrix_world
|
||||
if matrices_differ:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -980,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()
|
||||
@@ -1103,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"):
|
||||
@@ -1400,18 +1309,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 +1452,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 +1553,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 +1706,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(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2460,9 +2343,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()
|
||||
@@ -2484,25 +2365,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
|
||||
@@ -2517,34 +2389,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)
|
||||
@@ -2633,9 +2486,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"
|
||||
)
|
||||
|
||||
|
||||
@@ -3697,7 +3548,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:
|
||||
|
||||
@@ -536,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
|
||||
@@ -979,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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -581,11 +581,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"):
|
||||
@@ -3527,15 +3523,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 +3878,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 +4115,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]
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -156,7 +156,6 @@ class RadianceRender(bpy.types.Operator):
|
||||
print(f"Quality: {quality}, Detail: {detail}, Variability: {variability}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
|
||||
hdr_image_path, hdr_mask_path, sky_map_cal_path = None, None
|
||||
if use_hdr:
|
||||
hdr_image = "noon_grass_2k.hdr"
|
||||
hdr_mask = "noon_grass_2k_mask.hdr"
|
||||
@@ -255,9 +254,6 @@ class RadianceRender(bpy.types.Operator):
|
||||
# 4 0 0 -1 180
|
||||
|
||||
if use_hdr and choose_hdr_image == "Noon":
|
||||
assert hdr_image_path is not None
|
||||
assert hdr_mask_path is not None
|
||||
assert sky_map_cal_path is not None
|
||||
|
||||
with open(sky_file_path, "w") as f:
|
||||
f.write(sky_description_str)
|
||||
|
||||
@@ -418,13 +418,6 @@ class ImportQuickFavorites(bpy.types.Operator):
|
||||
bl_description = "Import operators from Blender's Quick Favorites menu, including their configured properties"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if bpy.app.version[:2] not in tool.Misc.QuickFavorites.OFFSET_USER_MENUS:
|
||||
cls.poll_message_set(f"Blender version {bpy.app.version_string} is not supported.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
|
||||
props = tool.Misc.get_misc_props()
|
||||
props.quick_favorites.clear()
|
||||
|
||||
@@ -564,7 +564,6 @@ class SelectAllArrayObjects(bpy.types.Operator):
|
||||
except RuntimeError:
|
||||
self.report({"ERROR"}, f"Objects that don't have an array parent, were deselected.")
|
||||
object.select_set(False)
|
||||
continue
|
||||
|
||||
array_objects = tool.Array.get_all_objects(parent_element)
|
||||
tool.Blender.set_objects_selection(
|
||||
|
||||
@@ -93,30 +93,6 @@ def _stroke_lines_alpha(
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
def _connected_components(
|
||||
vertex_groups: dict[int, list[bmesh.types.BMVert]],
|
||||
) -> list[list[bmesh.types.BMVert]]:
|
||||
"""Split each vertex group's members into their connected components,
|
||||
since a duplicated arc/circle loop shares its source loop's group index."""
|
||||
components = []
|
||||
for verts in vertex_groups.values():
|
||||
remaining = set(verts)
|
||||
while remaining:
|
||||
seed = remaining.pop()
|
||||
stack = [seed]
|
||||
component = [seed]
|
||||
while stack:
|
||||
v = stack.pop()
|
||||
for edge in v.link_edges:
|
||||
other = edge.other_vert(v)
|
||||
if other in remaining:
|
||||
remaining.discard(other)
|
||||
stack.append(other)
|
||||
component.append(other)
|
||||
components.append(component)
|
||||
return components
|
||||
|
||||
|
||||
class ProfileDecorator:
|
||||
installed = None
|
||||
|
||||
@@ -289,7 +265,7 @@ class ProfileDecorator:
|
||||
# Draw arcs
|
||||
arc_centroids = []
|
||||
arc_segments = []
|
||||
for arc in _connected_components(arcs):
|
||||
for arc in arcs.values():
|
||||
if len(arc) != 3:
|
||||
continue
|
||||
sorted_arc = [None, None, None]
|
||||
@@ -316,7 +292,7 @@ class ProfileDecorator:
|
||||
# Draw circles
|
||||
circle_centroids = []
|
||||
circle_segments = []
|
||||
for circle in _connected_components(circles):
|
||||
for circle in circles.values():
|
||||
if len(circle) != 2:
|
||||
continue
|
||||
p1 = obj.matrix_world @ circle[0].co
|
||||
|
||||
@@ -408,8 +408,6 @@ class MEPGenerator:
|
||||
compare = tool.Cad.is_x(requested_value, fitting_value, compare_precision)
|
||||
elif isinstance(fitting_value, list):
|
||||
compare = tool.Cad.are_vectors_equal(requested_value, Vector(fitting_value), precision)
|
||||
else:
|
||||
assert False, f"{key} {second_key}"
|
||||
return compare
|
||||
|
||||
ignore_keys = []
|
||||
@@ -478,13 +476,11 @@ class MEPGenerator:
|
||||
if predefined_type == "OBSTRUCTION":
|
||||
return packed_data
|
||||
|
||||
start_port = None
|
||||
for port in ports:
|
||||
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
|
||||
if tool.Cad.is_x(port_local_position.length, 0.0):
|
||||
start_port = port
|
||||
break
|
||||
assert start_port is not None
|
||||
|
||||
connected_port = tool.System.get_connected_port(start_port)
|
||||
connected_element = tool.System.get_port_relating_element(connected_port)
|
||||
|
||||
@@ -325,7 +325,7 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
|
||||
props.relating_type_id = str(self.relating_type_id)
|
||||
|
||||
building_obj, building_element = None, None
|
||||
building_obj = None
|
||||
if len(context.selected_objects) == 1 and context.active_object:
|
||||
building_obj = context.active_object
|
||||
building_element = tool.Ifc.get_entity(building_obj)
|
||||
|
||||
@@ -593,8 +593,6 @@ class DumbProfileJoiner:
|
||||
axisl = (profile2.matrix_world.inverted() @ axis1[1]) - (profile2.matrix_world.inverted() @ axis1[0])
|
||||
elif connection1 == "ATSTART":
|
||||
axisl = (profile2.matrix_world.inverted() @ axis1[0]) - (profile2.matrix_world.inverted() @ axis1[1])
|
||||
else:
|
||||
assert False, connection1
|
||||
xy_angle = degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
|
||||
if xy_angle >= -135 and xy_angle <= -45:
|
||||
closest_plane = "bottom"
|
||||
@@ -619,8 +617,6 @@ class DumbProfileJoiner:
|
||||
axisl = (profile1.matrix_world.inverted() @ axis2[1]) - (profile1.matrix_world.inverted() @ axis2[0])
|
||||
elif connection2 == "ATSTART":
|
||||
axisl = (profile1.matrix_world.inverted() @ axis2[0]) - (profile1.matrix_world.inverted() @ axis2[1])
|
||||
else:
|
||||
assert False, connection2
|
||||
xy_angle2 = degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
|
||||
if xy_angle2 >= -135 and xy_angle2 <= -45:
|
||||
closest_plane2 = "bottom"
|
||||
@@ -848,8 +844,6 @@ class DumbProfileJoiner:
|
||||
else:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
|
||||
else:
|
||||
assert False, plane
|
||||
return self.create_matrix(p, x_axis, y_axis, z_axis)
|
||||
|
||||
def create_matrix(self, p: Vector, x: Vector, y: Vector, z: Vector) -> Matrix:
|
||||
|
||||
@@ -508,7 +508,6 @@ class EditSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
converter.run()
|
||||
|
||||
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
|
||||
curve = None
|
||||
for path in converter.paths:
|
||||
points = []
|
||||
lines = path[0]
|
||||
@@ -518,7 +517,6 @@ class EditSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
points.append(tool.Ifc.get().createIfcCartesianPoint(local_point))
|
||||
points.append(points[0])
|
||||
curve = tool.Ifc.get().createIfcPolyline(points)
|
||||
assert curve
|
||||
profile.OuterCurve = curve
|
||||
|
||||
old_profile = extrusion.SweptArea
|
||||
|
||||
@@ -1577,7 +1577,6 @@ class DumbWallJoiner:
|
||||
# Get the ATEND connection from wall1 to use it in wall2
|
||||
relating_element = None
|
||||
connections = element1.ConnectedTo
|
||||
relating_connection, description = ..., ...
|
||||
for conn in connections:
|
||||
if conn.is_a("IfcRelConnectsPathElements") and conn.RelatingConnectionType == "ATEND":
|
||||
relating_element = conn.RelatedElement
|
||||
@@ -1592,7 +1591,6 @@ class DumbWallJoiner:
|
||||
description = conn.Description
|
||||
bonsai.core.geometry.remove_connection(tool.Geometry, connection=conn)
|
||||
if relating_element:
|
||||
assert relating_connection is not ... and description is not ...
|
||||
ifcopenshell.api.geometry.connect_path(
|
||||
tool.Ifc.get(),
|
||||
relating_element=relating_element,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
@@ -122,8 +122,8 @@ class ExecuteIfcPatch(bpy.types.Operator):
|
||||
if props.should_load_from_memory and tool.Ifc.get():
|
||||
args["file"] = tool.Ifc.get()
|
||||
else:
|
||||
args["input"] = props.ifc_patch_input
|
||||
args["file"] = ifcopenshell.open(props.ifc_patch_input)
|
||||
args["input"] = cast(str, props.ifc_patch_input)
|
||||
args["file"] = cast(ifcopenshell.file, ifcopenshell.open(props.ifc_patch_input))
|
||||
|
||||
# Store this in case the patch recipe resets the Blender session, such as by loading a new project.
|
||||
ifc_patch_output = props.ifc_patch_output or props.ifc_patch_input
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
import bpy
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
from . import decorator, gizmo, operator, prop, ui, workspace
|
||||
|
||||
classes = (
|
||||
@@ -60,8 +58,6 @@ classes = (
|
||||
operator.LinkIfc,
|
||||
operator.LoadBlendMetadataAndIFC,
|
||||
operator.LoadLink,
|
||||
operator.AutosavePrompt,
|
||||
operator.LoadAutosavedRecoveryPopup,
|
||||
operator.LoadLinkedProject,
|
||||
operator.LoadProject,
|
||||
operator.LoadProjectElements,
|
||||
@@ -140,7 +136,6 @@ def register():
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
bpy.utils.unregister_tool(workspace.ExploreTool)
|
||||
tool.Autosave.cancel_timer()
|
||||
del bpy.types.Scene.BIMProjectProperties
|
||||
del bpy.types.Scene.MeasureToolSettings
|
||||
bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load)
|
||||
|
||||
@@ -714,8 +714,6 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
representations = element.RepresentationMaps or []
|
||||
elif element.is_a("IfcProduct"):
|
||||
representations = [element.Representation] if element.Representation else []
|
||||
else:
|
||||
assert False, element
|
||||
for representation in representations or []:
|
||||
for element in self.file.traverse(representation):
|
||||
if not element.is_a("IfcRepresentationItem") or not element.StyledByItem:
|
||||
@@ -987,10 +985,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
skip_autosave_recovery: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
|
||||
filename_ext = ".ifc"
|
||||
skip_recent: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filepath: str
|
||||
@@ -999,7 +995,6 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
use_relative_path: bool
|
||||
should_start_fresh_session: bool
|
||||
import_without_ifc_data: bool
|
||||
skip_autosave_recovery: bool
|
||||
use_detailed_tooltip: bool
|
||||
|
||||
@classmethod
|
||||
@@ -1046,33 +1041,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
|
||||
return tooltip
|
||||
|
||||
def check_autosave_recovery(self, context: bpy.types.Context) -> bool:
|
||||
if self.skip_autosave_recovery:
|
||||
return False
|
||||
autosaved_filepath = tool.Autosave.get_newer_autosaved_path(self.get_filepath_abs())
|
||||
if not autosaved_filepath:
|
||||
return False
|
||||
# Fire-and-forget: don't propagate this popup's own RUNNING_MODAL
|
||||
# return value up as if *this* operator were running modally too -
|
||||
# we never call modal_handler_add() on ourselves, so the window
|
||||
# manager would be left tracking a modal operator with no handler,
|
||||
# corrupting its operator bookkeeping until it crashes later when
|
||||
# the (real) popup modal handler is closed.
|
||||
bpy.ops.bim.load_autosaved_recovery_popup(
|
||||
"INVOKE_DEFAULT",
|
||||
original_filepath=str(self.get_filepath_abs()),
|
||||
autosaved_filepath=autosaved_filepath,
|
||||
is_advanced=self.is_advanced,
|
||||
use_relative_path=self.use_relative_path,
|
||||
should_start_fresh_session=self.should_start_fresh_session,
|
||||
import_without_ifc_data=self.import_without_ifc_data,
|
||||
)
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
if self.check_autosave_recovery(context):
|
||||
return {"FINISHED"}
|
||||
|
||||
if (
|
||||
tool.Blender.get_addon_preferences().save_metadata_blend_file
|
||||
and self.should_start_fresh_session
|
||||
@@ -1167,8 +1136,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
props.should_save_metadata_for_this_file = metadata_doc is not None
|
||||
|
||||
tool.Blender.register_toolbar()
|
||||
if not self.skip_recent:
|
||||
tool.Project.add_recent_ifc_project(self.get_filepath_abs())
|
||||
tool.Project.add_recent_ifc_project(self.get_filepath_abs())
|
||||
|
||||
if self.is_advanced:
|
||||
pass
|
||||
@@ -1181,13 +1149,10 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
|
||||
except:
|
||||
bonsai.last_error = traceback.format_exc()
|
||||
raise
|
||||
tool.Autosave.reset_timer()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.filepath:
|
||||
if self.check_autosave_recovery(context):
|
||||
return {"FINISHED"}
|
||||
return self.execute(context)
|
||||
return ImportHelper.invoke(self, context, event)
|
||||
|
||||
@@ -1982,7 +1947,6 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
|
||||
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
|
||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
|
||||
skip_recent: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
filter_glob: str
|
||||
@@ -2031,7 +1995,6 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
project_props = tool.Project.get_project_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
project_props.use_relative_project_path = self.use_relative_path
|
||||
old_history_size, old_undo_steps = None, None
|
||||
if prefs.should_disable_undo_on_save:
|
||||
old_history_size = tool.Ifc.get().history_size
|
||||
old_undo_steps = context.preferences.edit.undo_steps
|
||||
@@ -2039,24 +2002,11 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
context.preferences.edit.undo_steps = 0
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
if prefs.should_disable_undo_on_save:
|
||||
assert old_history_size is not None and old_undo_steps is not None
|
||||
tool.Ifc.get().history_size = old_history_size
|
||||
context.preferences.edit.undo_steps = old_undo_steps
|
||||
return {"FINISHED"}
|
||||
|
||||
def _execute(self, context):
|
||||
project_props = tool.Project.get_project_props()
|
||||
project_props.use_relative_project_path = self.use_relative_path
|
||||
|
||||
# Fallback if filepath is not set
|
||||
if not getattr(self, "filepath", None) or self.filepath.strip() in ("", ".ifc"):
|
||||
props = tool.Blender.get_bim_props()
|
||||
if props.ifc_file:
|
||||
self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(props.ifc_file)))
|
||||
else:
|
||||
self.report({"ERROR"}, "No filepath available for saving.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
committed, failed_commits = tool.Parametric.commit_pending_edits()
|
||||
# Previews are session-transient — discard rather than commit. Sibling
|
||||
# gizmo polls gate on each preview's is_active flag, and a stuck flag
|
||||
@@ -2119,8 +2069,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
settings.logger.info("Export finished in {:.2f} seconds".format(time.time() - start))
|
||||
print("Export finished in {:.2f} seconds".format(time.time() - start))
|
||||
# New project created in Bonsai should be in recent projects too.
|
||||
if not self.skip_recent:
|
||||
tool.Project.add_recent_ifc_project(Path(output_file))
|
||||
tool.Project.add_recent_ifc_project(Path(output_file))
|
||||
props = tool.Project.get_project_props()
|
||||
if props.use_relative_project_path and bpy.data.is_saved:
|
||||
output_file = os.path.relpath(output_file, bpy.path.abspath("//"))
|
||||
@@ -2154,7 +2103,6 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
)
|
||||
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
tool.Autosave.reset_timer()
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
@@ -2163,123 +2111,6 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
return "Save the IFC file. Will save both .IFC/.BLEND files if synced together"
|
||||
|
||||
|
||||
class LoadAutosavedRecoveryPopup(bpy.types.Operator):
|
||||
bl_idname = "bim.load_autosaved_recovery_popup"
|
||||
bl_label = "Recover Autosaved File"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
original_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
autosaved_filepath: bpy.props.StringProperty(options={"SKIP_SAVE"})
|
||||
is_advanced: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
use_relative_path: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
should_start_fresh_session: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"})
|
||||
import_without_ifc_data: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="A newer autosaved copy was found:", icon="INFO")
|
||||
layout.label(text=os.path.basename(self.autosaved_filepath))
|
||||
layout.separator()
|
||||
layout.label(text="Do you want to load the autosaved version instead?")
|
||||
layout.label(text="(Cancel will load the original)")
|
||||
|
||||
def invoke(self, context, event):
|
||||
# invoke_props_dialog is modal - unlike invoke_popup/popup_menu, it
|
||||
# isn't dismissed by the mouse simply leaving its bounds. It always
|
||||
# renders both a fixed "Cancel" button and this confirm_text one, so
|
||||
# the question is framed as Yes/Cancel rather than adding separate
|
||||
# Load buttons on top.
|
||||
return context.window_manager.invoke_props_dialog(
|
||||
self, width=420, title="Recover Autosaved File", confirm_text="Yes"
|
||||
)
|
||||
|
||||
def _load_kwargs(self, filepath: str, skip_recent: bool) -> dict:
|
||||
return dict(
|
||||
filepath=filepath,
|
||||
skip_autosave_recovery=True, # Prevent infinite loop
|
||||
is_advanced=self.is_advanced,
|
||||
use_relative_path=self.use_relative_path,
|
||||
should_start_fresh_session=self.should_start_fresh_session,
|
||||
import_without_ifc_data=self.import_without_ifc_data,
|
||||
skip_recent=skip_recent,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _defer(callback) -> None:
|
||||
def on_timer() -> None:
|
||||
callback()
|
||||
return None
|
||||
|
||||
# bim.load_project (with should_start_fresh_session, our default)
|
||||
# calls wm.read_homefile(), which tears down the window
|
||||
# manager/screens/regions. Calling that synchronously from this
|
||||
# dialog's execute()/cancel() - themselves invoked from deep inside
|
||||
# Blender's modal handling for this popup's button click - frees
|
||||
# data that the still-on-stack caller dereferences once we return,
|
||||
# segfaulting Blender. Deferring by one timer tick runs the reload
|
||||
# after the popup's own modal handling has fully unwound. The
|
||||
# callback only closes over plain values (not `self`), since the
|
||||
# operator instance itself may no longer be valid by the time the
|
||||
# timer fires.
|
||||
bpy.app.timers.register(on_timer, first_interval=0.0)
|
||||
|
||||
def execute(self, context):
|
||||
kwargs = self._load_kwargs(self.autosaved_filepath, skip_recent=True)
|
||||
original_filepath = self.original_filepath
|
||||
|
||||
def load_and_repoint() -> None:
|
||||
bpy.ops.bim.load_project(**kwargs)
|
||||
# Re-point tracking at the original path so future saves write
|
||||
# back to it, not "_autosaved.ifc".
|
||||
tool.Ifc.set_path(original_filepath)
|
||||
|
||||
self._defer(load_and_repoint)
|
||||
return {"FINISHED"}
|
||||
|
||||
def cancel(self, context):
|
||||
# Also reached via Escape or a click outside the dialog, not just Cancel.
|
||||
kwargs = self._load_kwargs(self.original_filepath, skip_recent=False)
|
||||
self._defer(lambda: bpy.ops.bim.load_project(**kwargs))
|
||||
|
||||
|
||||
class AutosavePrompt(bpy.types.Operator):
|
||||
bl_idname = "bim.autosave_prompt"
|
||||
bl_label = "Autosave Reminder"
|
||||
bl_options = set()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(
|
||||
self, width=400, confirm_text="Save", title="Autosave Reminder"
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="The autosave timer has expired.", icon="INFO")
|
||||
layout.label(text="Would you like to save your IFC project now?")
|
||||
|
||||
def execute(self, context):
|
||||
# Get current IFC path
|
||||
props = tool.Blender.get_bim_props()
|
||||
current_ifc_path = props.ifc_file
|
||||
|
||||
if not current_ifc_path:
|
||||
self.report({"WARNING"}, "No IFC file path set. Please save manually.")
|
||||
tool.Autosave.reset_timer()
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Call save_project with explicit filepath using EXEC_DEFAULT
|
||||
result = bpy.ops.bim.save_project(
|
||||
"EXEC_DEFAULT", filepath=current_ifc_path, should_save_as=False, skip_recent=True
|
||||
)
|
||||
|
||||
tool.Autosave.reset_timer()
|
||||
return result
|
||||
|
||||
def cancel(self, context):
|
||||
tool.Autosave.reset_timer()
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "bim.load_linked_project"
|
||||
bl_label = "Load Project For Viewing Only"
|
||||
|
||||
@@ -113,8 +113,6 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif props.active_pset_type == "QTO":
|
||||
pset = ifcopenshell.api.pset.add_qto(self.file, product=element, name=props.active_pset_name)
|
||||
props.active_pset_id = pset.id()
|
||||
else:
|
||||
assert False
|
||||
|
||||
if self.properties:
|
||||
properties = json.loads(self.properties)
|
||||
|
||||
@@ -228,8 +228,6 @@ def get_qto_name(self: "PsetProperties", context: bpy.types.Context) -> tool.Ble
|
||||
if "bpy.data.objects" in pset_type:
|
||||
if prop_type == "PsetProperties":
|
||||
results = get_object_qto_name(self, context)
|
||||
else:
|
||||
assert False
|
||||
elif prop_type == "TaskPsetProperties":
|
||||
results = get_task_qto_names(self, context)
|
||||
elif prop_type == "ResourcePsetProperties":
|
||||
|
||||
@@ -480,7 +480,6 @@ class BIM_PT_material_psets(Panel):
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
props = tool.Material.get_material_props()
|
||||
ifc_definition_id = None
|
||||
if material := props.active_material:
|
||||
ifc_definition_id = material.ifc_definition_id
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import ifcopenshell.api.material
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.type
|
||||
@@ -130,25 +129,13 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
same_ifc_product = element.is_a(ifc_product)
|
||||
|
||||
if not same_ifc_product:
|
||||
# A spatial element (e.g. IfcSite) anchors the containment
|
||||
# hierarchy, so only allow reassigning it to another family when
|
||||
# it actually carries geometry - i.e. it's a real modelled thing
|
||||
# (a bench dropped onto IfcSite -> IfcFurniture) rather than an
|
||||
# empty spatial container we'd be turning into a loose element.
|
||||
# IfcSpatialStructureElement covers IFC2X3, which has no
|
||||
# IfcSpatialElement supertype.
|
||||
is_spatial = element.is_a("IfcSpatialElement") or element.is_a("IfcSpatialStructureElement")
|
||||
if is_spatial:
|
||||
has_geometry = (
|
||||
next(ifcopenshell.util.representation.get_representations_iter(element), None) is not None
|
||||
if not (element.is_a("IfcElement") and ifc_product == "IfcElementType") and not (
|
||||
element.is_a("IfcElementType") and ifc_product == "IfcElement"
|
||||
):
|
||||
self.report(
|
||||
{"ERROR"}, f"Not supported class reassignment for object '{obj.name}' -> {ifc_product}."
|
||||
)
|
||||
if not has_geometry:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Cannot reassign '{obj.name}' ({element.is_a()}) to {ifc_product}: "
|
||||
"a spatial element can only be reassigned to another class when it has geometry.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.is_reassigning_class = False
|
||||
@@ -413,13 +400,12 @@ class UnlinkObject(bpy.types.Operator, tool.Ifc.Operator):
|
||||
skip_invoke: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def _execute(self, context):
|
||||
objects: list[bpy.types.Object]
|
||||
if self.obj:
|
||||
requested_obj = bpy.data.objects.get(self.obj)
|
||||
objects = [requested_obj] if requested_obj is not None else []
|
||||
objects = [bpy.data.objects.get(self.obj)]
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
|
||||
objects: list[bpy.types.Object]
|
||||
for obj in objects:
|
||||
was_active_object = obj == context.active_object
|
||||
|
||||
@@ -631,13 +617,10 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
local_z = wall_matrix.to_3x3() @ Vector((0, 0, 1))
|
||||
direction_sense = getattr(usage, "DirectionSense", "POSITIVE")
|
||||
|
||||
layer_set_direction = usage.LayerSetDirection
|
||||
if layer_set_direction == "AXIS2":
|
||||
if usage.LayerSetDirection == "AXIS2":
|
||||
z_axis = tuple(local_y) if direction_sense == "POSITIVE" else tuple(-local_y)
|
||||
elif layer_set_direction == "AXIS3":
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
z_axis = tuple(local_z) if direction_sense == "POSITIVE" else tuple(-local_z)
|
||||
else:
|
||||
assert False, layer_set_direction
|
||||
|
||||
item = builder.extrude(
|
||||
profile,
|
||||
@@ -767,8 +750,6 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
WebThickness=default_web_thickness / unit_scale,
|
||||
FlangeThickness=default_flange_thickness / unit_scale,
|
||||
)
|
||||
else:
|
||||
assert False, representation_template
|
||||
|
||||
rel = ifcopenshell.api.material.assign_material(
|
||||
tool.Ifc.get(), products=[element], type="IfcMaterialProfileSet"
|
||||
|
||||
@@ -1009,7 +1009,6 @@ class ColourByProperty(Operator):
|
||||
palette = props.palette
|
||||
is_qualitative = palette in ("tab10", "paired")
|
||||
|
||||
colours = None
|
||||
if is_qualitative:
|
||||
colours = tool.Search.get_qualitative_palette(palette)
|
||||
|
||||
@@ -1036,7 +1035,6 @@ class ColourByProperty(Operator):
|
||||
if value in colourscheme:
|
||||
colourscheme[value]["total"] += 1
|
||||
else:
|
||||
assert colours is not None
|
||||
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
|
||||
obj.color = (*colourscheme[value]["colour"], 1)
|
||||
else:
|
||||
@@ -1141,7 +1139,6 @@ class SelectByProperty(Operator):
|
||||
|
||||
is_qualitative = palette in ("tab10", "paired")
|
||||
|
||||
values = None
|
||||
if not is_qualitative:
|
||||
values = []
|
||||
for colour in props.colourscheme:
|
||||
|
||||
@@ -12,7 +12,7 @@ function create_gantt_chart(json_data) {
|
||||
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
|
||||
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
|
||||
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
|
||||
vFormatArr: ['Hour', 'Day', 'Week', 'Month', 'Quarter'], // vUseSingleCell keeps Hour usable on large charts.
|
||||
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
|
||||
vShowRes: true, // Disable the resource column.
|
||||
vShowComp: false, // Disable the completion column.
|
||||
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
|
||||
|
||||
@@ -281,11 +281,11 @@ class BIM_PT_work_schedules(Panel):
|
||||
def draw_task_operators(self) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
task, ifc_definition_id = None, None
|
||||
ifc_definition_id = None
|
||||
if self.tprops.tasks and self.props.active_task_index < len(self.tprops.tasks):
|
||||
task = self.tprops.tasks[self.props.active_task_index]
|
||||
ifc_definition_id = task.ifc_definition_id
|
||||
if task and ifc_definition_id:
|
||||
if ifc_definition_id:
|
||||
if self.props.active_task_id:
|
||||
if self.props.editing_task_type == "TASKTIME":
|
||||
row.operator("bim.edit_task_time", text="", icon="CHECKMARK")
|
||||
@@ -341,8 +341,6 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.prop(self.props, "other_columns", text="")
|
||||
column_type, name = self.props.other_columns.split(".")
|
||||
data_type = "string"
|
||||
else:
|
||||
assert False, column_type
|
||||
row.operator("bim.set_task_sort_column", text="", icon="SORTALPHA").column = f"{column_type}.{name}"
|
||||
row.prop(
|
||||
self.props, "is_sort_reversed", text="", icon="SORT_DESC" if self.props.is_sort_reversed else "SORT_ASC"
|
||||
|
||||
@@ -516,7 +516,7 @@ class SetContainerVisibility(bpy.types.Operator):
|
||||
if self.mode == "ISOLATE":
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
containers = tool.Ifc.get().by_type("IfcSpatialStructureElement")
|
||||
else:
|
||||
elif tool.Ifc.get_schema() != "IFC2X3":
|
||||
containers = set(tool.Ifc.get().by_type("IfcSpatialElement"))
|
||||
containers -= set(tool.Ifc.get().by_type("IfcSpatialZone"))
|
||||
for container in containers:
|
||||
|
||||
@@ -128,7 +128,7 @@ def update_grid_is_locked(self: "BIMGridProperties", context: bpy.types.Context)
|
||||
if tool.Ifc.get().schema in ("IFC2X3", "IFC4"):
|
||||
elements = tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis")
|
||||
else:
|
||||
elements = tool.Ifc.get().by_type("IfcPositioningElement")
|
||||
elements = tool.Ifc.get().by_type("IfcPositioningElement") + tool.Ifc.get().by_type("IfcGridAxis")
|
||||
for element in elements:
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
if self.is_locked:
|
||||
|
||||
@@ -125,7 +125,6 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
row.label(text="Warning: No Default Container", icon="ERROR")
|
||||
row.operator("bim.import_spatial_decomposition", icon="FILE_REFRESH", text="")
|
||||
|
||||
ifc_definition_id = None
|
||||
if self.props.active_container:
|
||||
ifc_definition_id = self.props.active_container.ifc_definition_id
|
||||
row = self.layout.row(align=True)
|
||||
@@ -171,7 +170,6 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
|
||||
if not self.props.active_container:
|
||||
return
|
||||
assert ifc_definition_id is not None
|
||||
|
||||
container_has_elements = bool(self.props.total_elements)
|
||||
if container_has_elements:
|
||||
|
||||
@@ -71,11 +71,7 @@ class LoadByDirection(TypedDict):
|
||||
|
||||
ProcessedLoad = TypedDict(
|
||||
"ProcessedLoad",
|
||||
{
|
||||
"linear loads": dict[str, LoadByDirection] | None,
|
||||
"max linear load": float,
|
||||
"discrete loads": list[list[DiscreteConfigItem]],
|
||||
},
|
||||
{"linear loads": LoadByDirection, "max linear load": float, "discrete loads": list[list[DiscreteConfigItem]]},
|
||||
)
|
||||
|
||||
|
||||
@@ -849,16 +845,13 @@ class ShaderInfo:
|
||||
v = l1[1] + fac * (pos - l1[0])
|
||||
return v
|
||||
|
||||
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int) -> np.ndarray:
|
||||
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int, key: str) -> np.ndarray:
|
||||
"""interpolate the result vectors between load poits"""
|
||||
result = np.zeros(6)
|
||||
for i in range(6):
|
||||
# [position, force_component]
|
||||
value1 = [loadinfo[start]["pos"], loadinfo[start]["load values"][i]]
|
||||
# [position, force_component]
|
||||
value2 = [loadinfo[end]["pos"], loadinfo[end]["load values"][i]]
|
||||
# interpolated [position, force_component]
|
||||
result[i] = self.interp1d(value1, value2, pos)
|
||||
value1 = [loadinfo[start]["pos"], loadinfo[start][key][i]] # [position, force_component]
|
||||
value2 = [loadinfo[end]["pos"], loadinfo[end][key][i]] # [position, force_component]
|
||||
result[i] = self.interp1d(value1, value2, pos) # interpolated [position, force_component]
|
||||
return result
|
||||
|
||||
def get_before_and_after(self, pos: float, load_config_list: list[list[LoadConfigItem]]) -> dict[str, list[float]]:
|
||||
@@ -902,8 +895,8 @@ class ShaderInfo:
|
||||
load_before += config[end]["load values"]
|
||||
|
||||
elif end - start == 1:
|
||||
load_before += self.interpolate(pos, config, start, end)
|
||||
load_after += self.interpolate(pos, config, start, end)
|
||||
load_before += self.interpolate(pos, config, start, end, "load values")
|
||||
load_after += self.interpolate(pos, config, start, end, "load values")
|
||||
start += 1
|
||||
end -= 1
|
||||
return_value = {"before": load_before.tolist(), "after": load_after.tolist()}
|
||||
|
||||
@@ -102,7 +102,6 @@ class BIM_PT_styles(Panel):
|
||||
|
||||
# style ui tools
|
||||
if active_style:
|
||||
style = active_style
|
||||
row = self.layout.row(align=True)
|
||||
if material := style.blender_material:
|
||||
msprops = tool.Style.get_material_style_props(material)
|
||||
|
||||
@@ -20,7 +20,6 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api.attribute
|
||||
import ifcopenshell.api.material
|
||||
import ifcopenshell.api.type
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
@@ -116,94 +115,51 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if TYPE_CHECKING:
|
||||
related_object: str
|
||||
|
||||
@staticmethod
|
||||
def _reattach_styles(file: ifcopenshell.file, copied_entities: dict[int, ifcopenshell.entity_instance]) -> None:
|
||||
"""copy_deep only follows forward references, so IfcStyledItem (an inverse,
|
||||
``StyledByItem``) is not carried onto the copied geometry. Re-create a
|
||||
styled item on each copy that points at the same presentation styles as
|
||||
the original, so the unmapped occurrence keeps its appearance."""
|
||||
for original_id, copied in copied_entities.items():
|
||||
original = file.by_id(original_id)
|
||||
for styled_item in getattr(original, "StyledByItem", None) or []:
|
||||
file.create_entity(
|
||||
"IfcStyledItem",
|
||||
Item=copied,
|
||||
Styles=styled_item.Styles,
|
||||
Name=styled_item.Name,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def unassign_and_unmap(obj: bpy.types.Object) -> None:
|
||||
"""Unassign the type from ``obj`` and bake a private copy of any mapped
|
||||
representation onto it, so the occurrence keeps its geometry, styles, and
|
||||
material once the type (the source of all three) is gone."""
|
||||
|
||||
def _execute(self, context):
|
||||
def exclude_callback(attribute):
|
||||
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
||||
|
||||
file = tool.Ifc.get()
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcObject"):
|
||||
return
|
||||
|
||||
# Capture the material inherited from the type before we sever the link,
|
||||
# but only if the occurrence has no material of its own to override it.
|
||||
own_material = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
||||
inherited_material = ifcopenshell.util.element.get_material(element, should_inherit=True)
|
||||
|
||||
ifcopenshell.api.type.unassign_type(file, related_objects=[element])
|
||||
|
||||
if element.Representation:
|
||||
new_active_representation = None
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
active_context = active_representation.ContextOfItems
|
||||
representations = []
|
||||
for representation in element.Representation.Representations:
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
if representation == resolved_representation:
|
||||
representations.append(representation)
|
||||
else:
|
||||
# We must unmap representations, carrying over their styles.
|
||||
copied_entities: dict[int, ifcopenshell.entity_instance] = {}
|
||||
copied_representation = ifcopenshell.util.element.copy_deep(
|
||||
file,
|
||||
resolved_representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
copied_entities=copied_entities,
|
||||
)
|
||||
UnassignType._reattach_styles(file, copied_entities)
|
||||
representations.append(copied_representation)
|
||||
if representation.ContextOfItems == active_context:
|
||||
new_active_representation = copied_representation
|
||||
element.Representation.Representations = representations
|
||||
|
||||
if new_active_representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_active_representation,
|
||||
)
|
||||
|
||||
# Bake the inherited material down onto the occurrence now that its type
|
||||
# link (and, in the delete-type case, the type itself) is gone. Usages are
|
||||
# occurrence-specific and never inherited, so they need no handling here.
|
||||
if inherited_material is not None and own_material is None:
|
||||
material_type = inherited_material.is_a()
|
||||
if material_type not in ("IfcMaterialLayerSetUsage", "IfcMaterialProfileSetUsage"):
|
||||
ifcopenshell.api.material.assign_material(
|
||||
file, products=[element], type=material_type, material=inherited_material
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
if self.related_object:
|
||||
related_objects = [bpy.data.objects[self.related_object]]
|
||||
else:
|
||||
related_objects = tool.Blender.get_selected_objects()
|
||||
|
||||
for obj in related_objects:
|
||||
self.unassign_and_unmap(obj)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcObject"):
|
||||
continue
|
||||
ifcopenshell.api.type.unassign_type(self.file, related_objects=[element])
|
||||
|
||||
if element.Representation:
|
||||
new_active_representation = None
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
active_context = active_representation.ContextOfItems
|
||||
representations = []
|
||||
for representation in element.Representation.Representations:
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
if representation == resolved_representation:
|
||||
representations.append(representation)
|
||||
else:
|
||||
# We must unmap representations.
|
||||
copied_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
resolved_representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
)
|
||||
representations.append(copied_representation)
|
||||
if representation.ContextOfItems == active_context:
|
||||
new_active_representation = copied_representation
|
||||
element.Representation.Representations = representations
|
||||
|
||||
if new_active_representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_active_representation,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -349,82 +305,14 @@ class SelectTypeObjects(bpy.types.Operator):
|
||||
|
||||
class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_type"
|
||||
bl_label = "Delete Type"
|
||||
bl_description = (
|
||||
"Delete this type. Its occurrences are kept but become untyped.\n\n"
|
||||
"SHIFT+Click to also delete every occurrence of this type in the project"
|
||||
)
|
||||
bl_label = "Remove Type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
element: bpy.props.IntProperty()
|
||||
also_delete_instances: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
element: int
|
||||
also_delete_instances: bool
|
||||
|
||||
@staticmethod
|
||||
def _detach_type_material_set(element: ifcopenshell.entity_instance) -> None:
|
||||
"""Cascade-free removal of the type's IfcMaterialLayerSet / IfcMaterialProfileSet
|
||||
association, called just before the type is deleted.
|
||||
|
||||
``remove_product`` would otherwise route the type's material association
|
||||
through ``unassign_material``, which deletes *every* usage of that set
|
||||
across the model (documented behaviour, with an upstream TODO calling it
|
||||
too aggressive) — stripping the material off the very occurrences we are
|
||||
trying to keep. By unhooking the type<->set link by hand here, the type
|
||||
has no material at delete time, so that cascade never fires and the set
|
||||
plus the occurrences' usages survive intact."""
|
||||
file = tool.Ifc.get()
|
||||
material = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
||||
if not material or material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialProfileSet"):
|
||||
return
|
||||
for rel in list(getattr(element, "HasAssociations", None) or []):
|
||||
if not (rel.is_a("IfcRelAssociatesMaterial") and rel.RelatingMaterial == material):
|
||||
continue
|
||||
remaining = [o for o in rel.RelatedObjects if o != element]
|
||||
if remaining:
|
||||
rel.RelatedObjects = remaining
|
||||
else:
|
||||
history = rel.OwnerHistory
|
||||
file.remove(rel)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.also_delete_instances = event.shift
|
||||
if self.also_delete_instances:
|
||||
element = tool.Ifc.get().by_id(self.element)
|
||||
count = len(ifcopenshell.util.element.get_types(element))
|
||||
return context.window_manager.invoke_confirm(
|
||||
self,
|
||||
event,
|
||||
title="Delete Type and Occurrences",
|
||||
message=f"This will delete the type and all {count} of its occurrences.",
|
||||
confirm_text="Delete",
|
||||
)
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
element = tool.Ifc.get().by_id(self.element)
|
||||
occurrences = ifcopenshell.util.element.get_types(element)
|
||||
if self.also_delete_instances:
|
||||
for occurrence in occurrences:
|
||||
occurrence_obj = tool.Ifc.get_object(occurrence)
|
||||
if occurrence_obj:
|
||||
tool.Geometry.delete_ifc_object(occurrence_obj)
|
||||
else:
|
||||
# Keep the occurrences: bake their (previously type-mapped) geometry,
|
||||
# styles, and inherited material onto each one so nothing is lost when
|
||||
# the type is deleted...
|
||||
for occurrence in occurrences:
|
||||
occ_obj = tool.Ifc.get_object(occurrence)
|
||||
if occ_obj:
|
||||
UnassignType.unassign_and_unmap(occ_obj)
|
||||
# ...and keep any layer/profile-set material usages alive across the deletion.
|
||||
self._detach_type_material_set(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
|
||||
|
||||
class RenameType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@@ -144,10 +144,8 @@ class BIM_PT_type_attributes(Panel):
|
||||
|
||||
bonsai.bim.helper.draw_attributes(props.type_attributes, layout)
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row = layout.row()
|
||||
row.operator("bim.enable_editing_type_attributes", icon="GREASEPENCIL", text="Edit")
|
||||
op = row.operator("bim.remove_type", icon="TRASH", text="")
|
||||
op.element = TypeData.data["relating_type"]["id"]
|
||||
|
||||
for attribute in TypeData.data["relating_type_attributes"]:
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -72,7 +72,6 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
opening_objects = [obj for obj in selected_objects if obj != target_object]
|
||||
|
||||
obj1 = ...
|
||||
for opening_obj in opening_objects:
|
||||
element1 = tool.Ifc.get_entity(target_object)
|
||||
obj1 = target_object
|
||||
@@ -197,7 +196,6 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.data.objects.remove(obj2)
|
||||
|
||||
tool.Model.purge_scene_openings()
|
||||
assert obj1 is not ...
|
||||
context.view_layer.objects.active = obj1
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -284,13 +284,11 @@ class GizmoPreferences(bpy.types.PropertyGroup):
|
||||
draw_gizmos_in_3d_viewport: bool
|
||||
|
||||
|
||||
_gizmo_pref_entry = None
|
||||
for _gizmo_pref_entry in tool.Parametric.EDIT_TYPES:
|
||||
GizmoPreferences.__annotations__[_gizmo_pref_entry.name] = BoolProperty(
|
||||
name=_gizmo_pref_entry.name.replace("_", " ").title(),
|
||||
default=True,
|
||||
)
|
||||
assert _gizmo_pref_entry is not None
|
||||
del _gizmo_pref_entry
|
||||
|
||||
|
||||
@@ -396,14 +394,12 @@ class DefaultParameters(bpy.types.PropertyGroup):
|
||||
and gives the create operator a preset to copy from."""
|
||||
|
||||
|
||||
_default_params_entry = None
|
||||
for _default_params_entry in tool.Parametric.EDIT_TYPES:
|
||||
if not _default_params_entry.has_default_parameters:
|
||||
continue
|
||||
DefaultParameters.__annotations__[_default_params_entry.name] = bpy.props.PointerProperty(
|
||||
type=getattr(_model_prop, _default_params_entry.props_attr),
|
||||
)
|
||||
assert _default_params_entry is not None
|
||||
del _default_params_entry
|
||||
|
||||
|
||||
@@ -581,43 +577,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
should_disable_undo_on_save: BoolProperty(
|
||||
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
|
||||
)
|
||||
|
||||
def update_autosave_settings(self, context: bpy.types.Context) -> None:
|
||||
if self.autosave_enabled:
|
||||
tool.Autosave.reset_timer()
|
||||
else:
|
||||
tool.Autosave.cancel_timer()
|
||||
|
||||
autosave_enabled: BoolProperty(
|
||||
name="Enable IFC Autosave Timer",
|
||||
description="Periodically remind you to save or automatically create a backup copy of the IFC file",
|
||||
default=False,
|
||||
update=update_autosave_settings,
|
||||
)
|
||||
autosave_interval_minutes: bpy.props.IntProperty(
|
||||
name="Autosave Interval (Minutes)",
|
||||
description="Time between autosave reminders or backups. The timer resets whenever you open or save a project",
|
||||
default=10,
|
||||
min=1,
|
||||
max=1440,
|
||||
update=update_autosave_settings,
|
||||
)
|
||||
autosave_mode: bpy.props.EnumProperty(
|
||||
name="Autosave Mode",
|
||||
items=[
|
||||
(
|
||||
"PROMPT",
|
||||
"Prompt to Save",
|
||||
"Show a dialog offering to save the IFC project when the timer expires",
|
||||
),
|
||||
(
|
||||
"BACKUP",
|
||||
"Automatic Backup",
|
||||
"Save a backup copy as filename_autosaved.ifc when the timer expires",
|
||||
),
|
||||
],
|
||||
default="PROMPT",
|
||||
)
|
||||
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
|
||||
should_always_cache: BoolProperty(
|
||||
name="Always Cache Geometry",
|
||||
@@ -730,9 +689,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
bsdd_load_test_dictionaries: bool
|
||||
bsdd_baseurl: str
|
||||
should_disable_undo_on_save: bool
|
||||
autosave_enabled: bool
|
||||
autosave_interval_minutes: int
|
||||
autosave_mode: Literal["PROMPT", "BACKUP"]
|
||||
should_stream: bool
|
||||
should_always_cache: bool
|
||||
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
|
||||
@@ -881,12 +837,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
def draw_other_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "opening_focus_opacity")
|
||||
layout.prop(self, "should_disable_undo_on_save")
|
||||
layout.separator()
|
||||
layout.label(text="Autosave:")
|
||||
layout.prop(self, "autosave_enabled")
|
||||
if self.autosave_enabled:
|
||||
layout.prop(self, "autosave_interval_minutes")
|
||||
layout.prop(self, "autosave_mode")
|
||||
layout.prop(self, "should_stream")
|
||||
layout.prop(self, "should_always_cache")
|
||||
layout.label(text="bSDD:")
|
||||
|
||||
@@ -74,11 +74,10 @@ def add_instance_ceiling_covering_from_cursor(
|
||||
if not relating_type.is_a("IfcCoveringType"):
|
||||
relating_type = None
|
||||
|
||||
ceiling_height = None
|
||||
if selected_objects and active_obj:
|
||||
x, y, z, _, _ = spatial.get_x_y_z_h_mat_from_obj(active_obj)
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
|
||||
else:
|
||||
x, y, z, _, _ = spatial.get_x_y_z_h_mat_from_cursor()
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
|
||||
ceiling_height = covering.get_z_from_ceiling_height()
|
||||
|
||||
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
@@ -88,7 +87,6 @@ def add_instance_ceiling_covering_from_cursor(
|
||||
|
||||
obj = spatial.create_object("Covering")
|
||||
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
|
||||
assert ceiling_height is not None
|
||||
spatial.translate_obj_to_z_location(obj, z + ceiling_height)
|
||||
spatial.assign_type_to_obj(obj)
|
||||
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
|
||||
@@ -102,9 +100,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
|
||||
selected_objects = spatial.get_selected_objects()
|
||||
|
||||
if selected_objects and active_obj:
|
||||
x, y, _, _, _ = spatial.get_x_y_z_h_mat_from_obj(active_obj)
|
||||
else:
|
||||
assert False, "Object has to be active and selected."
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
|
||||
|
||||
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
|
||||
|
||||
@@ -303,7 +303,17 @@ def add_drawing(
|
||||
ifc_representation_class=None,
|
||||
)
|
||||
|
||||
drawings_parent_group = drawing.ensure_drawings_parent_group()
|
||||
drawings_parent_group = None
|
||||
for group in ifc.get().by_type("IfcGroup"):
|
||||
if group.Name == "DRAWINGS" and group.ObjectType == "DRAWINGS":
|
||||
drawings_parent_group = group
|
||||
break
|
||||
|
||||
if not drawings_parent_group:
|
||||
drawings_parent_group = ifc.run("group.add_group")
|
||||
ifc.run(
|
||||
"group.edit_group", group=drawings_parent_group, attributes={"Name": "DRAWINGS", "ObjectType": "DRAWINGS"}
|
||||
)
|
||||
|
||||
group = ifc.run("group.add_group")
|
||||
ifc.run("group.edit_group", group=group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
|
||||
@@ -342,7 +352,19 @@ def add_drawing(
|
||||
)
|
||||
drawing.setup_shading_styles_path(shading_styles_path)
|
||||
|
||||
drawings_parent_document = drawing.ensure_drawings_parent_document()
|
||||
drawings_parent_document = None
|
||||
for document in ifc.get().by_type("IfcDocumentInformation"):
|
||||
if document.Name == "DRAWINGS" and document.Scope == "DRAWINGS":
|
||||
drawings_parent_document = document
|
||||
break
|
||||
|
||||
if not drawings_parent_document:
|
||||
drawings_parent_document = ifc.run("document.add_information")
|
||||
if ifc.get_schema() == "IFC2X3":
|
||||
attributes = {"DocumentId": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
|
||||
else:
|
||||
attributes = {"Identification": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
|
||||
ifc.run("document.edit_information", information=drawings_parent_document, attributes=attributes)
|
||||
|
||||
information = ifc.run("document.add_information", parent=drawings_parent_document)
|
||||
uri = drawing.get_default_drawing_path(drawing_name)
|
||||
@@ -373,7 +395,17 @@ def duplicate_drawing(
|
||||
group = drawing_tool.get_drawing_group(new_drawing)
|
||||
ifc.run("group.unassign_group", group=group, products=[new_drawing])
|
||||
|
||||
drawings_parent_group = drawing_tool.ensure_drawings_parent_group()
|
||||
drawings_parent_group = None
|
||||
for parent_group in ifc.get().by_type("IfcGroup"):
|
||||
if parent_group.Name == "DRAWINGS" and parent_group.ObjectType == "DRAWINGS":
|
||||
drawings_parent_group = parent_group
|
||||
break
|
||||
|
||||
if not drawings_parent_group:
|
||||
drawings_parent_group = ifc.run("group.add_group")
|
||||
ifc.run(
|
||||
"group.edit_group", group=drawings_parent_group, attributes={"Name": "DRAWINGS", "ObjectType": "DRAWINGS"}
|
||||
)
|
||||
|
||||
new_group = ifc.run("group.add_group")
|
||||
ifc.run("group.edit_group", group=new_group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
|
||||
@@ -394,7 +426,19 @@ def duplicate_drawing(
|
||||
old_reference = drawing_tool.get_drawing_document(new_drawing)
|
||||
ifc.run("document.unassign_document", products=[new_drawing], document=old_reference)
|
||||
|
||||
drawings_parent_document = drawing_tool.ensure_drawings_parent_document()
|
||||
drawings_parent_document = None
|
||||
for document in ifc.get().by_type("IfcDocumentInformation"):
|
||||
if document.Name == "DRAWINGS" and document.Scope == "DRAWINGS":
|
||||
drawings_parent_document = document
|
||||
break
|
||||
|
||||
if not drawings_parent_document:
|
||||
drawings_parent_document = ifc.run("document.add_information")
|
||||
if ifc.get_schema() == "IFC2X3":
|
||||
attributes = {"DocumentId": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
|
||||
else:
|
||||
attributes = {"Identification": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
|
||||
ifc.run("document.edit_information", information=drawings_parent_document, attributes=attributes)
|
||||
|
||||
information = ifc.run("document.add_information", parent=drawings_parent_document)
|
||||
uri = drawing_tool.get_default_drawing_path(drawing_name)
|
||||
@@ -411,37 +455,6 @@ def duplicate_drawing(
|
||||
return new_drawing
|
||||
|
||||
|
||||
def copy_annotations_to_drawing(
|
||||
ifc: type[tool.Ifc],
|
||||
collector: type[tool.Collector],
|
||||
drawing_tool: type[tool.Drawing],
|
||||
geometry: type[tool.Geometry],
|
||||
annotations: list[ifcopenshell.entity_instance],
|
||||
target_drawing: ifcopenshell.entity_instance,
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
"""Duplicate annotations into another drawing, leaving the originals untouched."""
|
||||
target_group = drawing_tool.get_drawing_group(target_drawing)
|
||||
if not target_group:
|
||||
return []
|
||||
annotations = [a for a in annotations if drawing_tool.get_annotation_drawing(a) != target_drawing]
|
||||
annotation_objs = [obj for a in annotations if (obj := ifc.get_object(a))]
|
||||
if not annotation_objs:
|
||||
return []
|
||||
camera = ifc.get_object(target_drawing) or drawing_tool.import_drawing(target_drawing)
|
||||
old_to_new, _ = geometry.duplicate_ifc_objects(annotation_objs)
|
||||
copied: list[ifcopenshell.entity_instance] = []
|
||||
for new_elements in old_to_new.values():
|
||||
for new_element in new_elements:
|
||||
if old_group := drawing_tool.get_drawing_group(new_element):
|
||||
ifc.run("group.unassign_group", group=old_group, products=[new_element])
|
||||
ifc.run("group.assign_group", group=target_group, products=[new_element])
|
||||
new_obj = ifc.get_object(new_element)
|
||||
drawing_tool.ensure_annotation_in_drawing_plane(new_obj, camera)
|
||||
collector.assign(new_obj, should_clean_users_collection=True)
|
||||
copied.append(new_element)
|
||||
return copied
|
||||
|
||||
|
||||
def remove_drawing(
|
||||
ifc: type[tool.Ifc], drawing_tool: type[tool.Drawing], drawing: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
@@ -528,7 +541,6 @@ def add_annotation(
|
||||
drawing_tool.show_decorations()
|
||||
obj = drawing_tool.create_annotation_object(drawing, object_type)
|
||||
element = ifc.get_entity(obj)
|
||||
relating_type_rep = None
|
||||
if not element: # Brand new annotation
|
||||
relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None
|
||||
element = drawing_tool.run_root_assign_class(
|
||||
@@ -614,7 +626,8 @@ def sync_references(
|
||||
|
||||
for reference_element in potential_reference_elements:
|
||||
if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
|
||||
if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
|
||||
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
|
||||
if annotation:
|
||||
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
|
||||
ifc.run("group.assign_group", group=group, products=[annotation])
|
||||
collector.assign(ifc.get_object(annotation))
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import bonsai.core.geometry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
|
||||
Z_ROTATION_ALIGNMENT_TOLERANCE = 1e-9
|
||||
|
||||
|
||||
def _z_rotation_diff(target_z: float, source_z: float) -> float:
|
||||
"""Signed Z-Euler difference wrapped to [-π, π]."""
|
||||
return (target_z - source_z + math.pi) % (2 * math.pi) - math.pi
|
||||
|
||||
|
||||
def copy_z_rotation_to_selected(
|
||||
ifc: type[tool.Ifc],
|
||||
geometry: type[tool.Geometry],
|
||||
surveyor: type[tool.Surveyor],
|
||||
*,
|
||||
active: bpy.types.Object,
|
||||
targets: Iterable[bpy.types.Object],
|
||||
flip: bool = False,
|
||||
) -> int:
|
||||
"""Apply ``active``'s Z-Euler rotation to each target."""
|
||||
source_z = surveyor.get_z_rotation(active) # ty: ignore[missing-argument]
|
||||
if flip:
|
||||
source_z += math.pi
|
||||
rotated = 0
|
||||
for obj in targets:
|
||||
target_z = surveyor.get_z_rotation(obj) # ty: ignore[missing-argument]
|
||||
if abs(_z_rotation_diff(target_z, source_z)) < Z_ROTATION_ALIGNMENT_TOLERANCE:
|
||||
continue
|
||||
surveyor.set_z_rotation(obj, source_z) # ty: ignore[missing-argument]
|
||||
rotated += 1
|
||||
if ifc.get_entity(obj) is not None:
|
||||
bonsai.core.geometry.edit_object_placement(ifc, geometry, surveyor, obj=obj)
|
||||
return rotated
|
||||
@@ -254,6 +254,7 @@ class Cost:
|
||||
def get_cost_schedule(cls, cost_schedule): pass
|
||||
def get_cost_value_attributes(cls): pass
|
||||
def get_cost_value_unit_component(cls): pass
|
||||
def get_direct_cost_item_products(cls): pass
|
||||
def get_highlighted_cost_item(cls): pass
|
||||
def get_products(cls, related_object_type): pass
|
||||
def get_schedule_cost_items(cls, cost_schedule): pass
|
||||
@@ -354,9 +355,6 @@ class Drawing:
|
||||
def enable_editing_schedules(cls): pass
|
||||
def enable_editing_sheets(cls): pass
|
||||
def enable_editing_text(cls, obj): pass
|
||||
def ensure_annotation_in_drawing_plane(cls, obj, camera=None): pass
|
||||
def ensure_drawings_parent_document(cls): pass
|
||||
def ensure_drawings_parent_group(cls): pass
|
||||
def ensure_unique_drawing_name(cls, name): pass
|
||||
def ensure_unique_identification(cls, identification): pass
|
||||
def export_font_size(cls, obj): pass
|
||||
@@ -368,7 +366,6 @@ class Drawing:
|
||||
def generate_reference_attributes(cls, reference, **attributes): pass
|
||||
def generate_sheet_identification(cls): pass
|
||||
def get_annotation_context(cls, target_view, object_type=None): pass
|
||||
def get_annotation_drawing(cls, element): pass
|
||||
def get_annotation_representation(cls, element_type): pass
|
||||
def get_assigned_product(cls, element): pass
|
||||
def get_assigned_product_workaround(cls, element): pass
|
||||
@@ -386,7 +383,6 @@ class Drawing:
|
||||
def get_drawing_group(cls, drawing): pass
|
||||
def get_drawing_references(cls, drawing): pass
|
||||
def get_drawing_target_view(cls, drawing): pass
|
||||
def get_group_drawing(cls, group): pass
|
||||
def get_group_elements(cls, group): pass
|
||||
def get_ifc_representation_class(cls, object_type): pass
|
||||
def get_name(cls, element): pass
|
||||
@@ -400,7 +396,6 @@ class Drawing:
|
||||
def get_unit_system(cls): pass
|
||||
def import_assigned_product(cls, obj): pass
|
||||
def import_documents(cls, document_type): pass
|
||||
def import_drawing(cls, drawing): pass
|
||||
def import_drawings(cls): pass
|
||||
def import_sheets(cls): pass
|
||||
def import_text_attributes(cls, obj): pass
|
||||
@@ -1175,6 +1170,8 @@ class Style:
|
||||
@interface
|
||||
class Surveyor:
|
||||
def get_absolute_matrix(cls, obj): pass
|
||||
def get_z_rotation(cls, obj): pass
|
||||
def set_z_rotation(cls, obj, z): pass
|
||||
|
||||
|
||||
@interface
|
||||
|
||||
@@ -80,6 +80,3 @@ from bonsai.tool.type import Type
|
||||
from bonsai.tool.unit import Unit
|
||||
from bonsai.tool.wall import Wall
|
||||
from bonsai.tool.web import Web
|
||||
|
||||
# Have to move after import of tool.drawing
|
||||
from bonsai.tool.autosave import Autosave # isort: skip
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import bpy
|
||||
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim import export_ifc
|
||||
from bonsai.bim.module.model import preview_base
|
||||
|
||||
AUTOSAVING_SUFFIX = "_autosaving.ifc"
|
||||
AUTOSAVED_SUFFIX = "_autosaved.ifc"
|
||||
|
||||
_timer_callback: Union[Callable[[], None], None] = None
|
||||
# See cleanup_stale_autosave() for why this is a cached plain string rather
|
||||
# than looked up live.
|
||||
_active_ifc_path_cache: Union[str, None] = None
|
||||
|
||||
|
||||
class Autosave:
|
||||
@classmethod
|
||||
def get_paths(cls, ifc_path: Union[str, Path]) -> tuple[Path, Path, Path]:
|
||||
path = Path(ifc_path)
|
||||
stem = path.stem if path.suffix.lower() == ".ifc" else path.name
|
||||
parent = path.parent
|
||||
main_path = path if path.suffix.lower() == ".ifc" else parent / f"{stem}.ifc"
|
||||
autosaving_path = parent / f"{stem}{AUTOSAVING_SUFFIX}"
|
||||
autosaved_path = parent / f"{stem}{AUTOSAVED_SUFFIX}"
|
||||
return main_path, autosaving_path, autosaved_path
|
||||
|
||||
@classmethod
|
||||
def get_active_ifc_path(cls) -> Union[Path, None]:
|
||||
props = tool.Blender.get_bim_props()
|
||||
if not props.ifc_file:
|
||||
return None
|
||||
path = tool.Blender.ensure_blender_path_is_abs(Path(props.ifc_file))
|
||||
if path.suffix.lower() != ".ifc":
|
||||
return None
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def _update_active_ifc_path_cache(cls) -> None:
|
||||
global _active_ifc_path_cache
|
||||
ifc_path = cls.get_active_ifc_path()
|
||||
_active_ifc_path_cache = ifc_path.as_posix() if ifc_path is not None else None
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls) -> bool:
|
||||
return bool(tool.Blender.get_addon_preferences().autosave_enabled)
|
||||
|
||||
@classmethod
|
||||
def get_interval_seconds(cls) -> float:
|
||||
minutes = tool.Blender.get_addon_preferences().autosave_interval_minutes
|
||||
return max(1.0, float(minutes) * 60.0)
|
||||
|
||||
@classmethod
|
||||
def is_eligible(cls) -> bool:
|
||||
return cls.is_enabled() and tool.Ifc.get() is not None and cls.get_active_ifc_path() is not None
|
||||
|
||||
@classmethod
|
||||
def cancel_timer(cls) -> None:
|
||||
global _timer_callback
|
||||
if _timer_callback is not None and bpy.app.timers.is_registered(_timer_callback):
|
||||
bpy.app.timers.unregister(_timer_callback)
|
||||
_timer_callback = None
|
||||
|
||||
@classmethod
|
||||
def reset_timer(cls) -> None:
|
||||
cls.cancel_timer()
|
||||
cls._update_active_ifc_path_cache()
|
||||
if not cls.is_eligible():
|
||||
return
|
||||
|
||||
def on_timer() -> Union[float, None]:
|
||||
cls._on_timer_expired()
|
||||
# Reschedule by returning the next interval rather than calling
|
||||
# reset_timer(), which would unregister this timer from within
|
||||
# its own callback. Blender frees the timer's internal registry
|
||||
# entry on that manual unregister, then frees it again when the
|
||||
# callback returns - a double free that corrupts the heap and
|
||||
# crashes Blender shortly after (e.g. when the prompt dialog
|
||||
# spawned below is next interacted with).
|
||||
return cls.get_interval_seconds() if cls.is_eligible() else None
|
||||
|
||||
global _timer_callback
|
||||
_timer_callback = on_timer
|
||||
bpy.app.timers.register(on_timer, first_interval=cls.get_interval_seconds())
|
||||
|
||||
@classmethod
|
||||
def _on_timer_expired(cls) -> None:
|
||||
if not cls.is_eligible():
|
||||
return
|
||||
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
|
||||
if bim_props.is_dirty:
|
||||
if prefs.autosave_mode == "PROMPT":
|
||||
bpy.ops.bim.autosave_prompt("INVOKE_DEFAULT")
|
||||
elif prefs.autosave_mode == "BACKUP":
|
||||
try:
|
||||
cls.perform_backup(bpy.context)
|
||||
except Exception as error:
|
||||
print(f"Bonsai: autosave backup failed: {error}")
|
||||
|
||||
@classmethod
|
||||
def perform_backup(cls, context: bpy.types.Context) -> None:
|
||||
ifc_path = cls.get_active_ifc_path()
|
||||
if ifc_path is None:
|
||||
return
|
||||
|
||||
_, autosaving_path, autosaved_path = cls.get_paths(ifc_path)
|
||||
autosaving_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tool.Parametric.commit_pending_edits()
|
||||
preview_base.discard_pending_previews(context.scene)
|
||||
|
||||
logger = logging.getLogger("ExportIFC")
|
||||
output_file = autosaving_path.as_posix().replace("\\", "/")
|
||||
settings = export_ifc.IfcExportSettings.factory(context, output_file, logger)
|
||||
export_ifc.IfcExporter(settings).export()
|
||||
|
||||
try:
|
||||
os.replace(autosaving_path, autosaved_path)
|
||||
except OSError:
|
||||
if autosaving_path.is_file():
|
||||
autosaving_path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def get_newer_autosaved_path(cls, ifc_path: Union[str, Path]) -> Union[str, None]:
|
||||
path = Path(ifc_path)
|
||||
if path.suffix.lower() != ".ifc" or not path.is_file():
|
||||
return None
|
||||
|
||||
_, _, autosaved_path = cls.get_paths(path)
|
||||
if not autosaved_path.is_file():
|
||||
return None
|
||||
if autosaved_path.stat().st_mtime > path.stat().st_mtime:
|
||||
return autosaved_path.as_posix().replace("\\", "/")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def cleanup_stale_autosave(cls) -> None:
|
||||
"""Remove the active IFC's autosave file(s) on a graceful shutdown.
|
||||
|
||||
Registered via `atexit`, which only runs on a normal interpreter
|
||||
shutdown - never on an actual crash. So a deliberate quit (whether
|
||||
the user saved or chose "don't save") clears the recovery file and
|
||||
won't prompt on next startup, while a genuine crash leaves it in
|
||||
place for recovery, since no atexit callbacks fire then.
|
||||
|
||||
Deliberately reads only `_active_ifc_path_cache` - a plain string
|
||||
kept up to date by `reset_timer()` - rather than touching `bpy` here.
|
||||
By the time `atexit` fires, Blender's own C++ side is torn down far
|
||||
enough that even reading `bpy.context.scene` aborts the process
|
||||
(std::bad_optional_access) instead of raising a catchable exception.
|
||||
"""
|
||||
if _active_ifc_path_cache is None:
|
||||
return
|
||||
try:
|
||||
_, autosaving_path, autosaved_path = cls.get_paths(_active_ifc_path_cache)
|
||||
autosaving_path.unlink(missing_ok=True)
|
||||
autosaved_path.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
atexit.register(Autosave.cleanup_stale_autosave)
|
||||
@@ -192,9 +192,10 @@ class Brick(bonsai.core.tool.Brick):
|
||||
def get_brick(cls, element: ifcopenshell.entity_instance) -> Union[str, None]:
|
||||
for rel in element.HasAssociations:
|
||||
if rel.is_a("IfcRelAssociatesLibrary"):
|
||||
identification = tool.Document.get_external_reference_id(rel.RelatingLibrary)
|
||||
if identification and "#" in identification:
|
||||
return identification
|
||||
if tool.Ifc.get_schema() == "IFC2X3" and "#" in rel.RelatingLibrary.ItemReference:
|
||||
return rel.RelatingLibrary.ItemReference
|
||||
if tool.Ifc.get_schema() != "IFC2X3" and "#" in rel.RelatingLibrary.Identification:
|
||||
return rel.RelatingLibrary.Identification
|
||||
|
||||
@classmethod
|
||||
def get_brick_class(cls, element: ifcopenshell.entity_instance) -> Union[str, None]:
|
||||
|
||||
@@ -981,7 +981,6 @@ class Cad:
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
new_verts = None
|
||||
for loop in loops:
|
||||
all_verts = {v.index for e in loop for v in e.verts}
|
||||
possible_v1s = []
|
||||
@@ -1085,7 +1084,6 @@ class Cad:
|
||||
break
|
||||
|
||||
v1 = v2
|
||||
assert new_verts is not None
|
||||
|
||||
return new_verts
|
||||
|
||||
|
||||
@@ -120,7 +120,8 @@ class Collector(bonsai.core.tool.Collector):
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
||||
elif element.is_a("IfcAnnotation") and (drawing_obj := cls.get_annotation_drawing_obj(element)):
|
||||
cls.link_collection_object_safe(tool.Blender.get_object_bim_props(drawing_obj).collection, obj)
|
||||
target_collection = tool.Blender.get_object_bim_props(drawing_obj).collection
|
||||
cls.link_collection_object_safe(target_collection, obj)
|
||||
elif container := ifcopenshell.util.element.get_container(element):
|
||||
while container.is_a("IfcSpace"):
|
||||
container = ifcopenshell.util.element.get_aggregate(container)
|
||||
|
||||
@@ -280,8 +280,6 @@ class Cost(bonsai.core.tool.Cost):
|
||||
new = props.cost_item_processes.add()
|
||||
elif related_object.is_a("IfcResource"):
|
||||
new = props.cost_item_resources.add()
|
||||
else:
|
||||
assert False, related_object
|
||||
new.ifc_definition_id = related_object.id()
|
||||
new.name = related_object.Name or "Unnamed"
|
||||
|
||||
|
||||
@@ -261,8 +261,7 @@ class Document(bonsai.core.tool.Document):
|
||||
def get_reference_document(cls, reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None:
|
||||
# TODO: migrate to util.document and replace all instances
|
||||
if reference.file.schema == "IFC2X3":
|
||||
reference_to_document = reference.ReferenceToDocument
|
||||
return reference_to_document[0] if reference_to_document else None
|
||||
return (reference.ReferenceToDocument or (None))[0]
|
||||
return reference.ReferencedDocument
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -38,7 +38,6 @@ import ifcopenshell.api.context
|
||||
import ifcopenshell.api.document
|
||||
import ifcopenshell.api.drawing
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.api.group
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.geom
|
||||
@@ -756,17 +755,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
|
||||
return rel.RelatingGroup
|
||||
|
||||
@classmethod
|
||||
def get_group_drawing(cls, group: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
"""Get the drawing that owns this group, if the group represents a drawing."""
|
||||
if group.ObjectType != "DRAWING":
|
||||
return None
|
||||
for rel in group.IsGroupedBy or []:
|
||||
for related_object in rel.RelatedObjects:
|
||||
if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING":
|
||||
return related_object
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_drawing_document(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
for rel in drawing.HasAssociations:
|
||||
@@ -785,32 +773,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
def get_drawing_target_view(cls, drawing: ifcopenshell.entity_instance) -> str:
|
||||
return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("TargetView", "MODEL_VIEW")
|
||||
|
||||
@classmethod
|
||||
def ensure_drawings_parent_document(cls) -> ifcopenshell.entity_instance:
|
||||
ifc_file = tool.Ifc.get()
|
||||
for document in ifc_file.by_type("IfcDocumentInformation"):
|
||||
if document.Name == "DRAWINGS" and document.Scope == "DRAWINGS":
|
||||
return document
|
||||
document = ifcopenshell.api.document.add_information(ifc_file)
|
||||
if ifc_file.schema == "IFC2X3":
|
||||
attributes = {"DocumentId": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
|
||||
else:
|
||||
attributes = {"Identification": "DRAWINGS", "Name": "DRAWINGS", "Scope": "DRAWINGS"}
|
||||
ifcopenshell.api.document.edit_information(ifc_file, information=document, attributes=attributes)
|
||||
return document
|
||||
|
||||
@classmethod
|
||||
def ensure_drawings_parent_group(cls) -> ifcopenshell.entity_instance:
|
||||
ifc_file = tool.Ifc.get()
|
||||
for group in ifc_file.by_type("IfcGroup"):
|
||||
if group.Name == "DRAWINGS" and group.ObjectType == "DRAWINGS":
|
||||
return group
|
||||
group = ifcopenshell.api.group.add_group(ifc_file)
|
||||
ifcopenshell.api.group.edit_group(
|
||||
ifc_file, group=group, attributes={"Name": "DRAWINGS", "ObjectType": "DRAWINGS"}
|
||||
)
|
||||
return group
|
||||
|
||||
@classmethod
|
||||
def get_group_elements(cls, group: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||
for rel in group.IsGroupedBy or []:
|
||||
@@ -1083,12 +1045,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
camera_props.has_annotation = True
|
||||
camera_props.target_view = "PLAN_VIEW"
|
||||
camera_props.is_nts = False
|
||||
camera_props.use_edge_classification = False
|
||||
camera_props.render_creases = True
|
||||
camera_props.valley_angle_min_degrees = 12.0
|
||||
camera_props.render_sharp = True
|
||||
camera_props.ridge_angle_min_degrees = 45.0
|
||||
camera_props.render_flush = False
|
||||
camera.shift_x = 0.0
|
||||
camera.shift_y = 0.0
|
||||
|
||||
@@ -1118,18 +1074,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
camera_props.has_annotation = bool(pset["HasAnnotation"])
|
||||
if "IsNTS" in pset:
|
||||
camera_props.is_nts = bool(pset["IsNTS"])
|
||||
if "UseEdgeClassification" in pset:
|
||||
camera_props.use_edge_classification = bool(pset["UseEdgeClassification"])
|
||||
if "RenderCreases" in pset:
|
||||
camera_props.render_creases = bool(pset["RenderCreases"])
|
||||
if "ValleyAngleMinDegrees" in pset:
|
||||
camera_props.valley_angle_min_degrees = float(pset["ValleyAngleMinDegrees"])
|
||||
if "RenderSharp" in pset:
|
||||
camera_props.render_sharp = bool(pset["RenderSharp"])
|
||||
if "RidgeAngleMinDegrees" in pset:
|
||||
camera_props.ridge_angle_min_degrees = float(pset["RidgeAngleMinDegrees"])
|
||||
if "RenderFlush" in pset:
|
||||
camera_props.render_flush = bool(pset["RenderFlush"])
|
||||
if "DPI" in pset:
|
||||
camera_props.dpi = int(pset["DPI"])
|
||||
if "LineworkMode" in pset:
|
||||
@@ -1196,7 +1140,10 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
new = documents_collection.add()
|
||||
new.ifc_definition_id = schedule.id()
|
||||
new.name = schedule.Name or "Unnamed"
|
||||
new.identification = tool.Document.get_document_information_id(schedule) or ""
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
new.identification = schedule.DocumentId
|
||||
else:
|
||||
new.identification = schedule.Identification
|
||||
|
||||
@classmethod
|
||||
def get_sheet_identification(cls, sheet: ifcopenshell.entity_instance) -> str:
|
||||
@@ -1237,7 +1184,10 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.is_sheet = False
|
||||
|
||||
new.identification = tool.Document.get_external_reference_id(reference) or ""
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
new.identification = reference.ItemReference or ""
|
||||
else:
|
||||
new.identification = reference.Identification or ""
|
||||
|
||||
new.name = os.path.basename(reference.Location)
|
||||
new.reference_type = reference_description
|
||||
@@ -2003,19 +1953,29 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
if camera.data.type != "ORTHO":
|
||||
return
|
||||
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
geometry = ifcopenshell.geom.create_shape(settings, axis.AxisCurve)
|
||||
verts = ifcopenshell.util.shape.get_vertices(geometry)
|
||||
grid = (axis.PartOfU or axis.PartOfV or axis.PartOfW)[0]
|
||||
m = ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)
|
||||
axis_obj = tool.Ifc.get_object(axis)
|
||||
if axis_obj and axis_obj.data and len(axis_obj.data.vertices) >= 2:
|
||||
m = np.array(axis_obj.matrix_world)
|
||||
verts = [np.array(v.co) for v in axis_obj.data.vertices[:2]]
|
||||
else:
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
geometry = ifcopenshell.geom.create_shape(settings, axis.AxisCurve)
|
||||
verts = list(ifcopenshell.util.shape.get_vertices(geometry)[:2])
|
||||
grid_obj = tool.Ifc.get_object(grid)
|
||||
if grid_obj:
|
||||
m = np.array(grid_obj.matrix_world)
|
||||
else:
|
||||
m = ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)
|
||||
im = camera.matrix_world.inverted()
|
||||
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in verts[:2]]
|
||||
v1, v2 = [im @ Vector((m @ np.append(v[:3], 1.0))[:3]) for v in verts]
|
||||
|
||||
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
||||
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"):
|
||||
bounds = helper.ortho_view_frame(camera.data)
|
||||
if not (points := helper.clip_segment(bounds, [v1, v2])):
|
||||
points = helper.clip_segment(bounds, [v1, v2])
|
||||
if not points:
|
||||
return
|
||||
elif target_view in ("ELEVATION_VIEW", "SECTION_VIEW"):
|
||||
bounds = helper.ortho_view_frame(camera.data)
|
||||
@@ -2233,6 +2193,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
def sync_object_placement(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
blender_matrix = np.array(obj.matrix_world)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
is_moved = tool.Ifc.is_moved(obj)
|
||||
if tool.Geometry.is_scaled(obj):
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
return element
|
||||
@@ -2249,7 +2210,8 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
grid_obj = tool.Ifc.get_object(grid)
|
||||
if grid_obj:
|
||||
cls.sync_object_placement(grid_obj)
|
||||
if grid_obj.matrix_world != obj.matrix_world:
|
||||
matrices_differ = grid_obj.matrix_world != obj.matrix_world
|
||||
if matrices_differ:
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
tool.Geometry.record_object_position(obj)
|
||||
|
||||
@@ -2473,8 +2435,9 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
def get_reference_document(
|
||||
cls, reference: ifcopenshell.entity_instance
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
# TODO: migrate to document.get_reference_document.
|
||||
return tool.Document.get_reference_document(reference)
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
return reference.ReferenceToDocument[0]
|
||||
return reference.ReferencedDocument
|
||||
|
||||
@classmethod
|
||||
def select_assigned_product(cls, context: bpy.types.Context) -> None:
|
||||
@@ -2604,15 +2567,16 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
if not obj:
|
||||
continue
|
||||
current_representation = tool.Geometry.get_active_representation(obj)
|
||||
current_representation_subcontext = None
|
||||
if current_representation:
|
||||
subcontext = current_representation.ContextOfItems
|
||||
current_representation_subcontext = tool.Geometry.get_subcontext_parameters(subcontext)
|
||||
|
||||
has_context = False
|
||||
for subcontext in subcontexts:
|
||||
# prioritize already active representation if it matches the subcontext
|
||||
# (element could have multiple representations in the same subcontext)
|
||||
if current_representation_subcontext and subcontext == current_representation_subcontext:
|
||||
if current_representation and subcontext == current_representation_subcontext:
|
||||
has_context = True
|
||||
break
|
||||
priority_representation = ifcopenshell.util.representation.get_representation(element, *subcontext)
|
||||
if priority_representation:
|
||||
@@ -2622,6 +2586,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
obj=obj,
|
||||
representation=priority_representation,
|
||||
)
|
||||
has_context = True
|
||||
break
|
||||
|
||||
linked_handles: set[bpy.types.Object] = set()
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api.feature
|
||||
import ifcopenshell.util.representation
|
||||
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.tool
|
||||
@@ -49,7 +50,6 @@ class Feature(bonsai.core.tool.Feature):
|
||||
has_visible_openings = True
|
||||
break
|
||||
|
||||
element_had_openings = None
|
||||
for feature_obj in feature_objs:
|
||||
feature_element = tool.Ifc.get_entity(feature_obj)
|
||||
|
||||
@@ -58,6 +58,7 @@ class Feature(bonsai.core.tool.Feature):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=featured_obj)
|
||||
|
||||
element_had_openings = tool.Geometry.has_openings(featured_element)
|
||||
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body")
|
||||
ifcopenshell.api.feature.add_feature(tool.Ifc.get(), feature=feature_element, element=featured_element)
|
||||
|
||||
if tool.Ifc.is_moved(feature_obj):
|
||||
@@ -72,7 +73,6 @@ class Feature(bonsai.core.tool.Feature):
|
||||
if voided_obj.data:
|
||||
if tool.Ifc.is_edited(voided_obj):
|
||||
voided_element_ = tool.Ifc.get_entity(voided_obj)
|
||||
assert element_had_openings is not None
|
||||
if element_had_openings or (voided_element_ != featured_element and voided_element_.HasOpenings):
|
||||
voided_obj.scale = (1.0, 1.0, 1.0)
|
||||
tool.Ifc.finish_edit(voided_obj)
|
||||
|
||||
@@ -33,6 +33,7 @@ from typing import (
|
||||
Optional,
|
||||
TypeGuard,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
)
|
||||
|
||||
@@ -757,7 +758,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
# its centroid not obscured (tested via raycasting) by any other
|
||||
# face.
|
||||
distance = max(obj.dimensions.xyz)
|
||||
min_y, max_z = None, None
|
||||
if axis == "+Z":
|
||||
max_z = max([co[2] for co in obj.bound_box]) + 0.002
|
||||
direction = Vector((0, 0, -1))
|
||||
@@ -772,10 +772,8 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
if direction.dot(face.normal) > 0:
|
||||
continue
|
||||
if axis == "+Z":
|
||||
assert max_z is not None
|
||||
face_centroid_at_max = Vector((*face.calc_center_median().xy, max_z))
|
||||
elif axis == "-Y":
|
||||
assert min_y is not None
|
||||
centroid = face.calc_center_median()
|
||||
face_centroid_at_max = Vector((centroid.x, min_y, centroid.z))
|
||||
face_centroid_at_max = obj.matrix_world @ face_centroid_at_max
|
||||
@@ -1151,9 +1149,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
settings.set("layerset-first", True)
|
||||
settings.set("keep-bounding-boxes", True)
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
settings.set("mesher-linear-deflection", ifc_import_settings.deflection_tolerance)
|
||||
settings.set("mesher-angular-deflection", ifc_import_settings.angular_tolerance)
|
||||
geometry_library = ifc_import_settings.geometry_library
|
||||
|
||||
ifc_importer = bonsai.bim.import_ifc.IfcImporter(ifc_import_settings)
|
||||
ifc_importer.file = tool.Ifc.get()
|
||||
@@ -1165,11 +1160,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
shape = None
|
||||
if elements:
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
settings,
|
||||
tool.Ifc.get(),
|
||||
multiprocessing.cpu_count(),
|
||||
include=elements,
|
||||
geometry_library=geometry_library,
|
||||
settings, tool.Ifc.get(), multiprocessing.cpu_count(), include=elements
|
||||
)
|
||||
else:
|
||||
iterator = None # For example, when switching representation of a type with no occurrences
|
||||
@@ -1224,9 +1215,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
for element in element_types:
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
||||
geometry = ifcopenshell.geom.create_shape(
|
||||
settings, representation, geometry_library=geometry_library
|
||||
)
|
||||
geometry = ifcopenshell.geom.create_shape(settings, representation)
|
||||
mesh_name = tool.Loader.get_mesh_name_from_shape(geometry)
|
||||
mesh = meshes.get(mesh_name)
|
||||
if mesh is None:
|
||||
@@ -1897,7 +1886,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
"""NOTE: we assume that all items belonged to the same representation and to the same shape aspect"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
previous_shape_aspect = None
|
||||
base_representation = None
|
||||
for inverse in ifc_file.get_inverse(representation_items[0]):
|
||||
if inverse.is_a("IfcShapeRepresentation"):
|
||||
if inverse.OfShapeAspect:
|
||||
@@ -1907,7 +1895,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
previous_shape_aspect = inverse.OfShapeAspect[0]
|
||||
else:
|
||||
base_representation = inverse
|
||||
assert base_representation
|
||||
|
||||
# remove item from previous shape aspect
|
||||
if previous_shape_aspect:
|
||||
@@ -2200,7 +2187,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
setattr(item, attribute.name, attribute.get_value())
|
||||
|
||||
if item.is_a("IfcSweptAreaSolid"):
|
||||
item_profile = props.item_profile
|
||||
item_profile = cast(str, props.item_profile)
|
||||
profile = item.SweptArea
|
||||
profile_name: Union[str, None] = profile.ProfileName
|
||||
if item_profile == "-":
|
||||
@@ -2225,7 +2212,6 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
assert item
|
||||
obj.data.clear_geometry()
|
||||
|
||||
cartesian_point_offset = None
|
||||
if item.is_a("IfcHalfSpaceSolid"):
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_grid(bm, size=0.5)
|
||||
@@ -2684,6 +2670,11 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
# copy the actual class
|
||||
new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj)
|
||||
|
||||
# Give each duplicated IfcGridAxis its own AxisCurve so it doesn't
|
||||
# share geometry with the source axis.
|
||||
if new and new.is_a("IfcGridAxis"):
|
||||
tool.Model.create_axis_curve(new_obj, new)
|
||||
|
||||
# clean up the orphaned mesh with ifc id of the original object to avoid confusion
|
||||
# IfcGridAxis keeps the same mesh data (it's pointing to ifc id 0, so it's not a problem)
|
||||
if new and temp_data and not new.is_a("IfcGridAxis"):
|
||||
|
||||
@@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
import bpy
|
||||
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim import import_ifc
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
@@ -51,7 +50,7 @@ if TYPE_CHECKING:
|
||||
from bonsai.bim.module.ifcgit.prop import IfcGitProperties
|
||||
|
||||
|
||||
class IfcGit(bonsai.core.tool.IfcGit):
|
||||
class IfcGit:
|
||||
STEP_IDS = dict[str, set[int]]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -197,6 +197,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
cls, blender_material: bpy.types.Material, surface_style: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
surface_style = cls.surface_style_to_dict(surface_style)
|
||||
surface_style: dict[str, Any]
|
||||
|
||||
cls.create_surface_style_shading(blender_material, surface_style)
|
||||
|
||||
@@ -1087,21 +1088,18 @@ class Loader(bonsai.core.tool.Loader):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
prev_co = None
|
||||
layer_set_direction = usage.LayerSetDirection
|
||||
if layer_set_direction == "AXIS2":
|
||||
if usage.LayerSetDirection == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif layer_set_direction == "AXIS3":
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
elif layer_set_direction == "AXIS1":
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
else:
|
||||
assert False, layer_set_direction
|
||||
no *= sense_factor
|
||||
# Cache this
|
||||
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
@@ -1111,7 +1109,6 @@ class Loader(bonsai.core.tool.Loader):
|
||||
if style := tool.Ifc.get_entity(material):
|
||||
styles[style] = i
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
bisect_geom = None
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
if i != last_i:
|
||||
prev_co = co.copy()
|
||||
@@ -1125,7 +1122,6 @@ class Loader(bonsai.core.tool.Loader):
|
||||
if (material_index := styles.get(style, None)) is None:
|
||||
material_index = len(mesh.materials)
|
||||
mesh.materials.append(tool.Ifc.get_object(style))
|
||||
assert bisect_geom is not None
|
||||
if i == last_i:
|
||||
for face in bisect_geom["geom"]:
|
||||
if isinstance(face, bmesh.types.BMFace):
|
||||
@@ -1291,7 +1287,6 @@ class Loader(bonsai.core.tool.Loader):
|
||||
polyline.material_index = material_index
|
||||
return polyline
|
||||
|
||||
item = None
|
||||
for item_data, item_style in zip(rep_items, item_styles):
|
||||
item = item_data["item"]
|
||||
|
||||
@@ -1319,7 +1314,6 @@ class Loader(bonsai.core.tool.Loader):
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = native_data["matrix"] @ Vector(v2)
|
||||
|
||||
assert item is not None
|
||||
curve.bevel_depth = unit_scale * item.Radius
|
||||
thickness = None
|
||||
if (inner_radius := item.InnerRadius) and (thickness := max(item.Radius - inner_radius, 0)):
|
||||
|
||||
@@ -112,14 +112,10 @@ class Misc(bonsai.core.tool.Misc):
|
||||
reading data and never writing, to avoid the possibility of corrupting user preferences.
|
||||
"""
|
||||
|
||||
# Byte offset of UserDef.user_menus within the UserDef C struct, per (major, minor)
|
||||
# Blender version. Shifts whenever UserDef's fields change, so must be re-derived
|
||||
# per version (e.g. from that Blender build's SDNA).
|
||||
OFFSET_USER_MENUS: dict[tuple[int, int], int] = {
|
||||
(4, 5): 10032,
|
||||
(5, 0): 10032,
|
||||
(5, 1): 10032,
|
||||
(5, 2): 10800,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -220,12 +216,10 @@ class Misc(bonsai.core.tool.Misc):
|
||||
related_objects.append((element, ifcopenshell.util.placement.get_storey_elevation(element)))
|
||||
related_objects = sorted(related_objects, key=lambda e: e[1])
|
||||
storey_elevation = None
|
||||
i = None
|
||||
for i, related_object in enumerate(related_objects):
|
||||
if related_object[0] == storey:
|
||||
storey_elevation = related_object[1]
|
||||
break
|
||||
assert i is not None
|
||||
if i + total_storeys < len(related_objects):
|
||||
next_storey_elevation = related_objects[i + total_storeys][1]
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
@@ -2292,18 +2292,32 @@ class Model(bonsai.core.tool.Model):
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
|
||||
# Sanity check
|
||||
group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
if deform_layer:
|
||||
for vert in bm.verts:
|
||||
vert_group_indices = tool.Blender.bmesh_get_vertex_groups(vert, deform_layer)
|
||||
is_circle = any(gi in groups["IFCCIRCLE"] for gi in vert_group_indices)
|
||||
is_arc = any(gi in groups["IFCARCINDEX"] for gi in vert_group_indices)
|
||||
if (is_circle or is_arc) and not vert.link_edges:
|
||||
return (False, "CIRCLE" if is_circle else "3POINT_ARC")
|
||||
is_circle = False
|
||||
for group_index in vert_group_indices:
|
||||
group_type = "IFCARCINDEX" if group_index in groups["IFCARCINDEX"] else "IFCCIRCLE"
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
if group_type == "IFCCIRCLE":
|
||||
is_circle = True
|
||||
if is_circle:
|
||||
pass # Circles are allowed to be unclosed
|
||||
elif len(vert.link_edges) != 2: # Unclosed loop or forked loop
|
||||
return (False, "UNCLOSED_LOOP")
|
||||
|
||||
for group_type, group_counts in group_verts.items():
|
||||
if group_type == "IFCARCINDEX":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
elif group_type == "IFCCIRCLE":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
loop_edges = list(bm.edges)
|
||||
|
||||
# Create loops from edges
|
||||
@@ -2326,28 +2340,6 @@ class Model(bonsai.core.tool.Model):
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
# Sanity check, per loop rather than across the whole mesh
|
||||
if deform_layer:
|
||||
for loop in loops:
|
||||
loop_group_counts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
loop_verts = {v for edge in loop for v in edge.verts}
|
||||
for vert in loop_verts:
|
||||
for group_index in tool.Blender.bmesh_get_vertex_groups(vert, deform_layer):
|
||||
if group_index in groups["IFCARCINDEX"]:
|
||||
group_type = "IFCARCINDEX"
|
||||
elif group_index in groups["IFCCIRCLE"]:
|
||||
group_type = "IFCCIRCLE"
|
||||
else:
|
||||
continue
|
||||
loop_group_counts[group_type].setdefault(group_index, 0)
|
||||
loop_group_counts[group_type][group_index] += 1
|
||||
for group_count in loop_group_counts["IFCARCINDEX"].values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
for group_count in loop_group_counts["IFCCIRCLE"].values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
|
||||
|
||||
def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool:
|
||||
@@ -2528,11 +2520,27 @@ class Model(bonsai.core.tool.Model):
|
||||
deform_layer = bm.verts.layers.deform.active
|
||||
|
||||
# Sanity check
|
||||
group_verts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
if deform_layer:
|
||||
for vert in bm.verts:
|
||||
vert_group_indices = tool.Blender.bmesh_get_vertex_groups(vert, deform_layer)
|
||||
for group_index in vert_group_indices:
|
||||
group_type = "IFCARCINDEX" if group_index in groups["IFCARCINDEX"] else "IFCCIRCLE"
|
||||
group_verts[group_type].setdefault(group_index, 0)
|
||||
group_verts[group_type][group_index] += 1
|
||||
if len(vert.link_edges) > 2: # Forked loop
|
||||
return (False, "FORKED_LOOP")
|
||||
|
||||
for group_type, group_counts in group_verts.items():
|
||||
if group_type == "IFCARCINDEX":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
elif group_type == "IFCCIRCLE":
|
||||
for group_count in group_counts.values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
loop_edges = list(bm.edges)
|
||||
|
||||
# Create loops from edges
|
||||
@@ -2555,28 +2563,6 @@ class Model(bonsai.core.tool.Model):
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
# Sanity check, per loop rather than across the whole mesh
|
||||
if deform_layer:
|
||||
for loop in loops:
|
||||
loop_group_counts = {"IFCARCINDEX": {}, "IFCCIRCLE": {}}
|
||||
loop_verts = {v for edge in loop for v in edge.verts}
|
||||
for vert in loop_verts:
|
||||
for group_index in tool.Blender.bmesh_get_vertex_groups(vert, deform_layer):
|
||||
if group_index in groups["IFCARCINDEX"]:
|
||||
group_type = "IFCARCINDEX"
|
||||
elif group_index in groups["IFCCIRCLE"]:
|
||||
group_type = "IFCCIRCLE"
|
||||
else:
|
||||
continue
|
||||
loop_group_counts[group_type].setdefault(group_index, 0)
|
||||
loop_group_counts[group_type][group_index] += 1
|
||||
for group_count in loop_group_counts["IFCARCINDEX"].values():
|
||||
if group_count != 3: # Each arc needs 3 verts
|
||||
return (False, "3POINT_ARC")
|
||||
for group_count in loop_group_counts["IFCCIRCLE"].values():
|
||||
if group_count != 2: # Each circle needs 2 verts
|
||||
return (False, "CIRCLE")
|
||||
|
||||
tmp = ifcopenshell.file(schema=tool.Ifc.get().schema)
|
||||
|
||||
def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool:
|
||||
|
||||
@@ -641,8 +641,6 @@ del _edit_type_names
|
||||
# call sites can reference ``tool.Parametric.ROOF`` directly. Renaming a
|
||||
# registry entry renames the constant; a typo at the call site surfaces as
|
||||
# AttributeError at module load.
|
||||
_entry = None
|
||||
for _entry in Parametric.EDIT_TYPES:
|
||||
setattr(Parametric, _entry.name.upper(), _entry)
|
||||
assert _entry is not None
|
||||
del _entry
|
||||
|
||||
@@ -168,7 +168,6 @@ class Polyline(bonsai.core.tool.Polyline):
|
||||
distance = (mouse_vector - last_point).length
|
||||
if distance < 0:
|
||||
return
|
||||
angle, orientation_angle, angle_round_threshold = None, None, None
|
||||
if distance > 0:
|
||||
angle = tool.Cad.angle_3_vectors(
|
||||
second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
|
||||
@@ -189,7 +188,6 @@ class Polyline(bonsai.core.tool.Polyline):
|
||||
angle = 0
|
||||
orientation_angle = 0
|
||||
if input_ui:
|
||||
assert angle is not None and orientation_angle is not None and angle_round_threshold is not None
|
||||
if should_round:
|
||||
angle_snap = tool.Snap.get_angle_snap_value(context)
|
||||
angle = angle_snap * round(angle / angle_snap) if distance < angle_round_threshold else angle
|
||||
|
||||
@@ -370,21 +370,18 @@ class Project(bonsai.core.tool.Project):
|
||||
props = cls.get_project_props()
|
||||
active_library_breadcrumb = props.get_active_library_breadcrumb()
|
||||
change_back = False
|
||||
breadcrumb = None
|
||||
if active_library_breadcrumb:
|
||||
name = active_library_breadcrumb.name
|
||||
breadcrumb_type = active_library_breadcrumb.breadcrumb_type
|
||||
library_id = active_library_breadcrumb.library_id
|
||||
breadcrumb = (name, breadcrumb_type, library_id)
|
||||
change_back = True
|
||||
|
||||
bpy.ops.bim.rewind_library()
|
||||
if change_back:
|
||||
assert breadcrumb
|
||||
bpy.ops.bim.change_library_element(
|
||||
element_name=breadcrumb[0],
|
||||
breadcrumb_type=breadcrumb[1],
|
||||
library_id=breadcrumb[2],
|
||||
element_name=name,
|
||||
breadcrumb_type=breadcrumb_type,
|
||||
library_id=library_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -124,9 +124,6 @@ class Pset(bonsai.core.tool.Pset):
|
||||
return bpy.context.scene.GroupPsetProperties
|
||||
elif obj_type == "Zone":
|
||||
return bpy.context.scene.ZonePsetProperties
|
||||
elif obj_type == "Cost":
|
||||
# No psets for cost items currently.
|
||||
assert False, obj_type
|
||||
assert_never(obj_type)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -232,7 +232,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
return final_2d, v2
|
||||
|
||||
@classmethod
|
||||
def intersect_mouse_2d_bounding_box(cls, mouse_pos: tuple[int, int], bbox: list[float]):
|
||||
def intersect_mouse_2d_bounding_box(cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float]):
|
||||
x, y = mouse_pos
|
||||
xmin, xmax, ymin, ymax = bbox
|
||||
|
||||
|
||||
@@ -505,8 +505,6 @@ class Search(bonsai.core.tool.Search):
|
||||
(0.773, 0.922, 0.816),
|
||||
(0.871, 0.957, 0.894),
|
||||
]
|
||||
else:
|
||||
assert False, theme
|
||||
|
||||
if value < min_val:
|
||||
value = min_val
|
||||
@@ -576,10 +574,8 @@ class ImportFilterQueryTransformer(lark.Transformer):
|
||||
new = self.filter_groups.add()
|
||||
global_ids = []
|
||||
is_first_group = len(self.filter_groups) == 1
|
||||
new2 = None
|
||||
for filter_index, arg in enumerate(args):
|
||||
if arg["type"] == "instance" and global_ids:
|
||||
assert new2
|
||||
if "bpy.data.texts" in new2.value:
|
||||
data_name = new2.value.split("bpy.data.texts")[1][2:-2]
|
||||
bpy.data.texts[data_name].write("," + arg["value"])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user