Compare commits

..

1 Commits

Author SHA1 Message Date
Andrej730 54df93c001 build_pyodide: debug 2026-08-07 12:12:36 +05:00
716 changed files with 62740 additions and 63484 deletions
+1
View File
@@ -1,4 +1,5 @@
Checks: 'bugprone-*,cert-*,clang-analyzer-*,readability-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''
AnalyzeTemporaryDtors: false
FormatStyle: none
-382
View File
@@ -1,382 +0,0 @@
# /// script
# dependencies = [
# "pytest",
# ]
# ///
"""Check (and by default fix) whitespace issues in tracked source files:
- stray CR, e.g. 'hello\\rworld' -> 'helloworld'
- line ending mismatch, e.g. 'hello\\r\\n' -> 'hello\\n' (or vice versa)
- missing newline at end of file
- extra newline(s) at end of file
- trailing whitespace at end of line
"""
import argparse
import io
import os
import re
import subprocess
import sys
from collections.abc import Callable
from pathlib import Path
from typing import BinaryIO, Literal, cast
import pytest
class C:
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
RESET = "\033[0m"
CR = b"\r"
CRLF = b"\r\n"
LF = b"\n"
LineSeparator = Literal[b"\r\n", b"\n"]
SYSTEM_LINE_SEPARATOR = cast(LineSeparator, os.linesep.encode())
class Checker:
def __init__(self, newline: LineSeparator = SYSTEM_LINE_SEPARATOR) -> None:
self.newline = newline
self.issues = 0
def report(self, label: str, issue: str) -> None:
self.issues += 1
print(f"{label}: {C.RED}{issue}{C.RESET}")
def check_stray_cr(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_stray_cr(f, str(filepath), check)
def _check_stray_cr(self, f: BinaryIO, label: str, check: bool) -> None:
# a CR is "stray" if it isn't immediately followed by a LF, i.e. not part of a CRLF pair
# CRLF/CR mismatch will be reported separately.
stray_cr = re.compile(rb"\r(?!\n)")
content = f.read()
matches = list(stray_cr.finditer(content))
if not matches:
return
line_numbers = dict.fromkeys(content.count(b"\n", 0, m.start()) + 1 for m in matches)
for line_number in line_numbers:
self.report(f"{label}:{line_number}", "stray carriage return")
if check:
return
f.seek(0)
f.write(stray_cr.sub(b"", content))
f.truncate()
def check_line_endings_mismatch(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_line_endings_mismatch(f, str(filepath), check)
def _check_line_endings_mismatch(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
def get_line_ending(line: bytes) -> LineSeparator | None:
if line.endswith(CRLF):
return CRLF
if line.endswith(LF):
return LF
# last line with no trailing newline at all; check_eof_newline handles that
return None
changed = False
fixed_lines = []
for line_number, line in enumerate(f, start=1):
found = get_line_ending(line)
if found in (NEWLINE, None):
fixed_lines.append(line)
continue
self.report(f"{label}:{line_number}", f"line ending mismatch (expected {NEWLINE!r}, found {found!r})")
changed = True
content = line[: -len(found)]
fixed_lines.append(content + NEWLINE)
if changed and not check:
f.seek(0)
f.write(b"".join(fixed_lines))
f.truncate()
def check_eof_newline(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_eof_newline(f, str(filepath), check)
def _check_eof_newline(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
NEWLINE_SIZE = len(NEWLINE)
size = f.seek(0, os.SEEK_END)
if size == 0:
return
trailing_newlines = 0
while True:
pos = f.seek((-trailing_newlines - 1) * NEWLINE_SIZE, os.SEEK_END)
if f.read(NEWLINE_SIZE) != NEWLINE:
break
trailing_newlines += 1
if pos == 0:
break
if trailing_newlines == 0:
self.report(label, "missing newline at end of file")
if check:
return
f.seek(0, os.SEEK_END)
f.write(NEWLINE)
elif trailing_newlines > 1:
self.report(label, f"{trailing_newlines} trailing newlines at end of file")
if check:
return
f.truncate(size - (trailing_newlines - 1) * NEWLINE_SIZE)
def check_trailing_whitespaces(self, filepath: Path, check: bool) -> None:
with filepath.open("r+b") as f:
self._check_trailing_whitespaces(f, str(filepath), check)
def _check_trailing_whitespaces(self, f: BinaryIO, label: str, check: bool) -> None:
NEWLINE = self.newline
NEWLINE_SIZE = len(NEWLINE)
changed = False
fixed_lines = []
for line_number, line in enumerate(f, start=1):
has_newline = line.endswith(NEWLINE)
content = line[:-NEWLINE_SIZE] if has_newline else line
stripped = content.rstrip()
if stripped != content:
self.report(f"{label}:{line_number}", "trailing whitespace")
changed = True
fixed_lines.append(stripped + (NEWLINE if has_newline else b""))
if changed and not check:
f.seek(0)
f.write(b"".join(fixed_lines))
f.truncate()
CheckMethod = Callable[[Checker, BinaryIO, str, bool], None]
class TestChecker:
def _assert_check(
self,
method: CheckMethod,
content: bytes,
expected_issues: int,
fixed: bytes,
check: bool,
line_ending: LineSeparator,
*,
transform: bool = True,
) -> None:
checker = Checker(line_ending)
if line_ending == CRLF and transform:
content = content.replace(LF, CRLF)
fixed = fixed.replace(LF, CRLF)
buffer = io.BytesIO(content)
method(checker, buffer, "test", check)
assert buffer.getvalue() == (content if check else fixed)
assert checker.issues == expected_issues
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
(b"line1\r\nline2\n", 0, b"line1\r\nline2\n"),
# ERR
(b"hello\rworld\n", 1, b"helloworld\n"),
(b"a\rb\rc\n", 1, b"abc\n"),
(b"hello\r", 1, b"hello"),
),
)
@pytest.mark.parametrize("check", [False, True])
def test_check_stray_cr(self, content: bytes, expected_issues: int, fixed: bytes, check: bool) -> None:
# Don't parametrize by line endings, since in this case it doesn't matter.
self._assert_check(Checker._check_stray_cr, content, expected_issues, fixed, check, LF)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed", "line_ending"),
(
# OK
(b"", 0, b"", LF),
(b"hello\n", 0, b"hello\n", LF),
(b"hello\r\n", 0, b"hello\r\n", CRLF),
# ERR
(b"hello\r\n", 1, b"hello\n", LF),
(b"a\nb\r\nc\n", 1, b"a\nb\nc\n", LF),
(b"a\r\nb\r\n", 2, b"a\nb\n", LF),
(b"hello\n", 1, b"hello\r\n", CRLF),
(b"a\r\nb\nc\r\n", 1, b"a\r\nb\r\nc\r\n", CRLF),
),
)
@pytest.mark.parametrize("check", [False, True])
def test_check_line_endings_mismatch(
self, content: bytes, expected_issues: int, fixed: bytes, line_ending: LineSeparator, check: bool
) -> None:
self._assert_check(
Checker._check_line_endings_mismatch, content, expected_issues, fixed, check, line_ending, transform=False
)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
# ERR
(b"hello", 1, b"hello\n"),
(b"hello\n\n\n", 1, b"hello\n"),
(b"\n\n\n", 1, b"\n"),
),
)
@pytest.mark.parametrize("check", [False, True])
@pytest.mark.parametrize("line_ending", [LF, CRLF])
def test_check_eof_newline(
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
) -> None:
self._assert_check(Checker._check_eof_newline, content, expected_issues, fixed, check, line_ending)
@pytest.mark.parametrize(
("content", "expected_issues", "fixed"),
(
# OK
(b"", 0, b""),
(b"hello\n", 0, b"hello\n"),
(b"hello", 0, b"hello"),
# ERR
(b" ", 1, b""),
(b"hello ", 1, b"hello"),
),
)
@pytest.mark.parametrize("check", [False, True])
@pytest.mark.parametrize("line_ending", [LF, CRLF])
def test_check_trailing_whitespaces(
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
) -> None:
self._assert_check(Checker._check_trailing_whitespaces, content, expected_issues, fixed, check, line_ending)
@staticmethod
def run_tests(extra_args: list[str] | None = None) -> None:
pytest.main([__file__, *(extra_args or [])])
def existing_path(value: str) -> Path:
path = Path(value)
if not path.exists():
raise argparse.ArgumentTypeError(f"path not found: {value}")
return path
# Python files are covered by `black`.
PATTERNS = (
"*.cpp",
"*.h",
"*.i",
"*.cmake",
"*/CMakeLists.txt",
)
REPO_ROOT = Path(subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip())
# Generated files; formatted by the express codegen, not by this script.
IGNORED_DIRS = (
REPO_ROOT / "src/ifcparse/schemas",
REPO_ROOT / "win/patches",
)
def get_tracked_files(root: Path | None = None) -> list[Path]:
output = subprocess.check_output(
["git", "ls-files", "--others", "--cached", "--exclude-standard", *PATTERNS],
cwd=root,
text=True,
)
base = root if root is not None else Path()
filepaths = []
for line in output.splitlines():
filepath = base / line
if not any(filepath.resolve().is_relative_to(d) for d in IGNORED_DIRS):
filepaths.append(filepath)
return filepaths
def main() -> int:
# anything after "--" is forwarded to pytest, e.g. `--test -- --capture=no`
argv = sys.argv[1:]
if "--" in argv:
split = argv.index("--")
argv, extra_args = argv[:split], argv[split + 1 :]
else:
extra_args = []
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=__doc__,
)
parser.add_argument("paths", type=existing_path, nargs="*", help="files or directories to check")
parser.add_argument(
"--check",
action="store_true",
help="only check for whitespace issues without applying fixes",
)
parser.add_argument(
"--test",
action="store_true",
help="run self-tests",
)
parser.add_argument(
"--verbose",
action="store_true",
help="print each checked path",
)
args = parser.parse_args(argv)
if args.test:
TestChecker.run_tests(extra_args)
return 0
if args.paths:
filepaths: list[Path] = []
for path in args.paths:
filepaths.extend(get_tracked_files(path) if path.is_dir() else [path])
else:
filepaths = get_tracked_files()
# dict.fromkeys() dedupes while preserving order, unlike set().
filepaths = list(dict.fromkeys(filepaths))
checker = Checker()
for filepath in filepaths:
if args.verbose:
print(f"checking {filepath}")
checker.check_stray_cr(filepath, args.check)
checker.check_line_endings_mismatch(filepath, args.check)
checker.check_eof_newline(filepath, args.check)
checker.check_trailing_whitespaces(filepath, args.check)
print(f"{len(filepaths)} file(s) checked.")
if not checker.issues:
color = C.GREEN
elif args.check:
color = C.RED
else:
color = C.YELLOW
outcome = "found" if args.check else "found and fixed"
print(f"{color}{checker.issues} issue(s) {outcome}.{C.RESET}")
return 1 if args.check and checker.issues else 0
if __name__ == "__main__":
sys.exit(main())
+76 -7
View File
@@ -35,9 +35,6 @@ jobs:
lfs: true
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Install Dependencies
run: |
brew update
@@ -64,7 +61,7 @@ jobs:
- name: Unpack Dependencies
run: |
cd build
uv run ../nix/cache_dependencies.py unpack
python ../nix/cache_dependencies.py unpack
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.23
@@ -105,7 +102,7 @@ jobs:
# INSTALL_RPATH to "@loader_path" on Apple.
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
BUILD_BONSAIVIEWER=ON QT_DIR="${QT_DIR}" \
uv run ./nix/build-all.py -v --diskcleanup --ifcopenshell-shared ${MAC_INTEL} \
python3 ./nix/build-all.py -v --diskcleanup --ifcopenshell-shared ${MAC_INTEL} \
| tee build.log
- name: Upload Build Logs
@@ -122,7 +119,7 @@ jobs:
- name: Pack Dependencies
run: |
cd build
uv run ../nix/cache_dependencies.py pack
python ../nix/cache_dependencies.py pack
- name: Commit and Push Changes to Build Repository
run: |
@@ -135,7 +132,79 @@ jobs:
- name: Package .zip archives
run: |
uv run nix/package-zip-archives.py "macos${{ matrix.oldarch }}64"
VERSION=v`cat VERSION`
# packaging/build.py stages the connector binary + connector.json
# into dist/autodesk/; the .app loop below copies that folder into
# the bundle. Same on-disk shape as the Linux and Windows builds.
python3 src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/10.15/install/ifcopenshell
mkdir -p ~/output
install_root="$PWD"
stage_runtime_payload() {
dest="$1"
while IFS= read -r runtime_file; do
cp -L "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
}
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
stage_runtime_payload ifcopenshell
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
stage_runtime_payload "$package_dir"
pushd "$package_dir" > /dev/null
zip -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
# .app bundles (e.g. BonsaiViewer.app) live at the install-prefix
# root because their install rule uses `BUNDLE DESTINATION "."` —
# that's the layout Qt's macdeployqt expects. macdeployqt has
# already embedded the Qt frameworks inside each bundle during
# install/strip, so the only thing left to stage is the connector.
find "$install_root" -maxdepth 1 -type d -name "*.app" | while read app_path; do
app=`basename "$app_path" .app`
if [ "$app" = "BonsaiViewer" ]; then
# ConnectorDiscovery looks in applicationDirPath()/connectors,
# which for a bundle is Contents/MacOS.
mkdir -p "$app_path/Contents/MacOS/connectors"
cp -a "$autodesk_connector_dir" "$app_path/Contents/MacOS/connectors/"
fi
pushd "$install_root" > /dev/null
zip -qq -r "$HOME/output/${app}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" "$(basename "$app_path")"
popd > /dev/null
done
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+4
View File
@@ -92,6 +92,10 @@ jobs:
./run_pytest.py setup
./run_pytest.py run
- name: Setup tmate session
if: failure()
uses: mxschmitt/action-tmate@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
+164 -1
View File
@@ -110,7 +110,170 @@ jobs:
- name: Package .zip archives
shell: bash
run: |
uv run nix/package-zip-archives.py linux64
VERSION=v`cat VERSION`
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
# invokes `cargo build --release` and stages the binary +
# connector.json into dist/autodesk/. Same on-disk shape as the
# old PyInstaller flow so the symlink + zip steps below
# continue to work unchanged.
uv run src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/install/ifcopenshell
mkdir -p ~/output
install_root="$PWD"
QT6_VERSION="${QT6_VERSION:-6.8.3}"
if [ -z "${QT_DIR:-}" ]; then
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
if [ -d "$qt_candidate/lib" ]; then
QT_DIR="$qt_candidate"
break
fi
done
fi
# Ensure that all shared libraries in provided dest `$1`
# are present using their SONAMEs (at least as symlinks).
ensure_soname_links() {
dest="$1"
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
# TODO: actual pattern is "Library soname" instead of "Shared library"?
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
[ -n "$soname" ] || continue
[ -e "$dest/$soname" ] && continue
ln -s "$(basename "$shared_object")" "$dest/$soname"
done
}
# Copy all libs from `install/ifcopenshell` to the provided `$1`.
# Set `$2` to `0` to skip including geometry writers.
stage_runtime_payload() {
dest="$1"
include_geometry_writers="${2:-1}"
while IFS= read -r runtime_file; do
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
continue
fi
cp -P "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
ensure_soname_links "$dest"
}
# Copy all libs from `QT_DIR` to the provided `$2`.
stage_qt_runtime_payload() {
exe_path="$1"
dest="$2"
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
# Skip executables that don't depend on QT (don't have `libQt6` referenced).
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
return 0
fi
# Copy all QT libs to `dest`.
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
ensure_soname_links "$dest"
# Copy QT plugins.
if [ -d "$QT_DIR/plugins" ]; then
pushd "$QT_DIR/plugins" > /dev/null
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
done
popd > /dev/null
# Point plugins rpath to `$dest`.
if [ -d "$dest/plugins" ]; then
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
fi
fi
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
}
# Check all binaries in the dest `$1`
# and report if they're still missing dependencies or are static.
check_runtime_dependencies() {
package_dir="$1"
missing=0
# Iterate over all .so files.
while IFS= read -r binary_file; do
# Skip non-binaries.
readelf -h "$binary_file" >/dev/null 2>&1 || continue
# Report non-dynamic binaries.
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
echo "ldd failed for $binary_file"
cat "$package_dir/.ldd.out"
missing=1
continue
fi
# Report missing dependencies.
if grep -q "not found" "$package_dir/.ldd.out"; then
echo "Missing runtime dependencies for $binary_file"
grep "not found" "$package_dir/.ldd.out"
missing=1
fi
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
rm -f "$package_dir/.ldd.out"
# TODO: should error?
if [ "$missing" -ne 0 ]; then
echo "Runtime dependency check found issues; continuing packaging."
fi
return 0
}
# Iterate over all built Python wrappers in `install/ifcopenshell/python-x.y.z`.
# and zip them, bundling all dynamic libs from `lib`.
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
# TODO: packs qt libs also?
stage_runtime_payload ifcopenshell
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
# Iterate over all executables in `install/ifcopenshell/bin` and zip them.
# Each zip bundles dynamic libs from `lib` and also qt libs.
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
stage_runtime_payload "$package_dir" 0
stage_qt_runtime_payload "$exe_path" "$package_dir"
if [ "$exe" = "BonsaiViewer" ]; then
mkdir -p "$package_dir/connectors"
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
fi
check_runtime_dependencies "$package_dir"
pushd "$package_dir" > /dev/null
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+142 -1
View File
@@ -122,7 +122,148 @@ jobs:
- name: Package .zip archives
shell: bash
run: |
uv run nix/package-zip-archives.py linuxarm64
VERSION=v`cat VERSION`
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
# invokes `cargo build --release` and stages the binary +
# connector.json into dist/autodesk/. Same on-disk shape as the
# old PyInstaller flow so the symlink + zip steps below
# continue to work unchanged.
uv run src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/install/ifcopenshell
mkdir -p ~/output
install_root="$PWD"
QT6_VERSION="${QT6_VERSION:-6.8.3}"
if [ -z "${QT_DIR:-}" ]; then
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
if [ -d "$qt_candidate/lib" ]; then
QT_DIR="$qt_candidate"
break
fi
done
fi
ensure_soname_links() {
dest="$1"
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
[ -n "$soname" ] || continue
[ -e "$dest/$soname" ] && continue
ln -s "$(basename "$shared_object")" "$dest/$soname"
done
}
stage_runtime_payload() {
dest="$1"
include_geometry_writers="${2:-1}"
while IFS= read -r runtime_file; do
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
continue
fi
cp -P "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
ensure_soname_links "$dest"
}
stage_qt_runtime_payload() {
exe_path="$1"
dest="$2"
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
return 0
fi
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
ensure_soname_links "$dest"
if [ -d "$QT_DIR/plugins" ]; then
pushd "$QT_DIR/plugins" > /dev/null
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
done
popd > /dev/null
if [ -d "$dest/plugins" ]; then
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
fi
fi
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
}
check_runtime_dependencies() {
package_dir="$1"
missing=0
while IFS= read -r binary_file; do
readelf -h "$binary_file" >/dev/null 2>&1 || continue
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
echo "ldd failed for $binary_file"
cat "$package_dir/.ldd.out"
missing=1
continue
fi
if grep -q "not found" "$package_dir/.ldd.out"; then
echo "Missing runtime dependencies for $binary_file"
grep "not found" "$package_dir/.ldd.out"
missing=1
fi
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
rm -f "$package_dir/.ldd.out"
if [ "$missing" -ne 0 ]; then
echo "Runtime dependency check found issues; continuing packaging."
fi
return 0
}
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
stage_runtime_payload ifcopenshell
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
stage_runtime_payload "$package_dir" 0
stage_qt_runtime_payload "$exe_path" "$package_dir"
if [ "$exe" = "BonsaiViewer" ]; then
mkdir -p "$package_dir/connectors"
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
fi
check_runtime_dependencies "$package_dir"
pushd "$package_dir" > /dev/null
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+12 -3
View File
@@ -16,7 +16,7 @@ on:
- 'src/ifc5d/ifc5d/**'
- 'src/ifccityjson/**'
branches:
- v0.9.0
- v0.8.0
workflow_dispatch:
jobs:
@@ -51,10 +51,19 @@ jobs:
name: "Linux Build",
short_name: linux,
}
- {
name: "MacOS Build",
short_name: macos,
}
- {
name: "MacOS ARM Build",
short_name: macosm1,
}
exclude:
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
- pyver: py313
config:
short_name: macos
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
@@ -63,7 +72,7 @@ jobs:
python-version: '3.11'
- name: Get current version
id: version
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Compile
run: |
cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
@@ -119,7 +128,7 @@ jobs:
blender --command extension install-file -r user_default -e $bonsai_zip
blender --command extension list
git clone --branch ${{ github.ref_name }} --single-branch https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
# Reregister Bonsai.
# Note that running it in background might miss some errors
+9
View File
@@ -34,10 +34,19 @@ jobs:
name: "Linux Build",
short_name: linux,
}
- {
name: "MacOS Build",
short_name: macos,
}
- {
name: "MacOS ARM Build",
short_name: macosm1,
}
exclude:
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
- pyver: py313
config:
short_name: macos
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
@@ -27,9 +27,7 @@ jobs:
- name: Get current version
id: version
# Strip any trailing prerelease label and number; the dated alpha
# suffix is added below.
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Get current date
id: date
+2 -2
View File
@@ -7,7 +7,7 @@ on:
- '.github/workflows/ci-ifcsverchok-build.yml'
- 'src/ifcsverchok/*'
branches:
- v0.9.0
- v0.8.0
jobs:
activate:
@@ -32,7 +32,7 @@ jobs:
python-version: '3.11'
- name: Get current version
id: version
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Get current date
id: date
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
+1 -3
View File
@@ -3,13 +3,11 @@ name: ci-ifctester-org
on:
workflow_dispatch:
push:
branches:
- v0.9.0
paths:
- src/ifctester/**
jobs:
publish_ifctester_org:
publish_website:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
+2 -13
View File
@@ -43,7 +43,6 @@ jobs:
sudo apt update
sudo apt-get install --no-install-recommends -y \
cmake \
bison \
gcc \
g++ \
libboost-date-time-dev \
@@ -62,23 +61,13 @@ jobs:
libocct-ocaf-dev \
libocct-visualization-dev \
libpcre3-dev \
libpcre2-dev \
libtbb-dev \
libxml2-dev \
libxi-dev \
occt-misc \
tcl-dev \
tk-dev
- name: Build SWIG
# IfcOpenShell requires SWIG 4.1+, ubuntu-22.04 ships 4.0.2.
run: |
sudo apt-get remove --purge -y swig swig4.0
git clone https://github.com/swig/swig --branch v4.2.1 --depth 1
cmake -S swig -B swig/build -DCMAKE_BUILD_TYPE=Release
cmake --build swig/build -j "$(nproc)"
sudo cmake --install swig/build
swig -version
tk-dev \
swig
- name: Configure minimal IfcOpenShell
run: |
+1 -1
View File
@@ -121,7 +121,7 @@ jobs:
cd OpenCOLLADA
git checkout v1.6.68
patch -p1 --batch --forward -i ../nix/patches/opencollada/pr622_and_disable_subdirs.patch
patch -p1 --batch --forward -i ../nix/patches/opencollada/config_select_libs_by_use_shared.patch
patch -p1 --batch --forward -i ../nix/patches/opencollada/allow_static_libraries_config_on_unix.patch
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
@@ -1,87 +0,0 @@
# This file was generated with the assistance of an AI coding tool.
name: Publish C++ API documentation
on:
push:
branches:
- v0.9.0
paths:
- '.github/workflows/publish-cpp-api-docs.yml'
- 'docs/cpp-api/**'
- 'src/ifcgeom/**'
- 'src/ifcparse/**'
- 'src/serializers/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: publish-cpp-api-docs
cancel-in-progress: false
jobs:
publish:
if: github.repository == 'IfcOpenShell/IfcOpenShell'
runs-on: ubuntu-24.04
steps:
- name: Checkout IfcOpenShell
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.10'
- name: Install documentation dependencies
run: |
sudo apt-get update
sudo apt-get install --yes doxygen graphviz
python -m pip install --requirement docs/cpp-api/requirements.txt
- name: Build C++ API documentation
working-directory: docs/cpp-api
run: |
export PROJECT_NUMBER="$(git rev-parse --short HEAD)"
python -m sphinx -M html . output -W --keep-going
- name: Checkout documentation repository
uses: actions/checkout@v7
with:
repository: IfcOpenShell/cpp_docs
ref: master
path: published-docs
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Replace published documentation
run: |
publish_tree="${RUNNER_TEMP}/published-docs-tree"
mkdir -p "${publish_tree}/v0.9.0-latest"
rsync --archive docs/cpp-api/output/html/ "${publish_tree}/v0.9.0-latest/"
touch "${publish_tree}/.nojekyll"
if [[ -f published-docs/CNAME ]]; then
cp published-docs/CNAME "${publish_tree}/CNAME"
fi
rsync --archive --delete --exclude='.git/' "${publish_tree}/" published-docs/
- name: Commit and push if changed
working-directory: published-docs
env:
SOURCE_SHA: ${{ github.sha }}
run: |
git config user.name 'IfcOpenBot'
git config user.email 'IfcOpenBot@users.noreply.github.com'
git add --all
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update C++ API docs from ${SOURCE_SHA:0:7}"
git push origin master
+3 -7
View File
@@ -111,11 +111,10 @@ src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
# plugins
src/ifcopenshell-python/ifcopenshell/ifcopenshell_document_*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_geometry_*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell_parse_schema*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell.document.*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell.geometry.*.so
src/ifcopenshell-python/ifcopenshell/ifcopenshell.parse.schema*.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.geometry.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.geometry.writer.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.parse.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.plugin.so
@@ -126,9 +125,6 @@ src/ifcopenshell-python/ifcopenshell/libifcopenshell.plugin.so
.clangd
# clangd cache
.cache
# Useful for symlinking json compilation database from cmake,
# allowing clang commands without `-p path/to/build`.
/compile_commands.json
# Brickschema
src/bonsai/bonsai/bim/schema/Brick.ttl
+3
View File
@@ -8,6 +8,9 @@
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
path = src/ifcopenshell-python/test/Sample-BIM-Files
url = https://github.com/IfcOpenShell/ids-test-files
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
path = docs/cpp-api/assets/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
path = src/ifcopenshell-python/ifcopenshell/simple_spf
url = https://github.com/IfcOpenShell/step-file-parser
+1 -1
View File
@@ -1 +1 @@
0.9.0alpha0
0.8.6
+34 -57
View File
@@ -36,14 +36,6 @@ file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
# CMake's project(VERSION) only accepts numeric components. Keep the complete
# release identifier for build information, but use its numeric release part
# for PROJECT_VERSION, SOVERSION, and generated CMake package metadata.
string(REGEX MATCH "^[0-9]+\\.[0-9]+\\.[0-9]+" PROJECT_VERSION_NUMERIC "${RELEASE_VERSION}")
if(NOT PROJECT_VERSION_NUMERIC)
message(FATAL_ERROR "VERSION must start with a numeric major.minor.patch version: '${RELEASE_VERSION}'")
endif()
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
if(POLICY CMP0141) # 3.25+
@@ -63,15 +55,9 @@ endif()
# Include utility macros and functions
include(utilities.cmake)
# Use a SemVer-compatible spelling for CPack artifact names. A trailing
# alphabetic label and number is separated from the numeric version by a
# hyphen: for example, 0.9.0alpha0 becomes 0.9.0-alpha0.
# use extra version to make pre-release using eg semver
if(NOT DEFINED EXTRA_VERSION)
if(RELEASE_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+([A-Za-z]+)([0-9]+)$")
set(EXTRA_VERSION "-${CMAKE_MATCH_1}${CMAKE_MATCH_2}")
else()
set(EXTRA_VERSION "")
endif()
set(EXTRA_VERSION "-alpha.3")
endif()
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
@@ -133,7 +119,7 @@ option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
option(USE_DEBUG_PYTHON "Use debug binaries when building Debug IfcPython on Windows." OFF)
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
option(VERSION_OVERRIDE "Use VERSION as the branch label when commit information is embedded" OFF)
option(VERSION_OVERRIDE "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" OFF)
set(
PYTHON_MODULE_INSTALL_DIR
@@ -141,7 +127,15 @@ set(
"Directory to install IfcPython package to. By default package is installed in found Python's site-packages."
)
project(IfcOpenShell VERSION ${PROJECT_VERSION_NUMERIC})
if (VERSION_OVERRIDE)
file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
else()
set(RELEASE_VERSION "0.8.0")
endif()
project(IfcOpenShell VERSION ${RELEASE_VERSION})
# Make sure CMake modules in this project are found first
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
@@ -178,31 +172,26 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
set(BUILD_IFCGEOM ON)
endif()
option(USE_CCACHE "Use ccache as a compiler launcher if it is found" ON)
if(USE_CCACHE)
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
# By default Visual Studio generators will use /Zi which is not compatible
# with ccache, so tell Visual Studio to use /Z7 instead.
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
# Not needed for Ninja.
if(CMAKE_GENERATOR MATCHES "Visual Studio")
file(COPY_FILE
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS
"CLToolExe=cl.exe"
"CLToolPath=${CMAKE_BINARY_DIR}"
"UseMultiToolTask=true"
)
endif()
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
# By default Visual Studio generators will use /Zi which is not compatible
# with ccache, so tell Visual Studio to use /Z7 instead.
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
# Not needed for Ninja.
if(CMAKE_GENERATOR MATCHES "Visual Studio")
file(COPY_FILE
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS
"CLToolExe=cl.exe"
"CLToolPath=${CMAKE_BINARY_DIR}"
"UseMultiToolTask=true"
)
endif()
endif()
else()
message(STATUS "ccache usage disabled via USE_CCACHE=OFF")
endif()
if(MSVC AND MSVC_PARALLEL_BUILD)
@@ -316,25 +305,12 @@ if (WITH_ROCKSDB)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB)
# See https://github.com/facebook/rocksdb/issues/981.
if(TARGET RocksDB::rocksdb)
set(IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET RocksDB::rocksdb)
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb)
elseif(TARGET RocksDB::rocksdb-shared)
set(IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET RocksDB::rocksdb-shared)
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb-shared)
else()
message(FATAL_ERROR "RocksDB found but neither RocksDB::rocksdb nor RocksDB::rocksdb-shared target exists")
endif()
# Our win/build-deps.cmd builds RocksDB separately per Debug/Release config into the
# same install prefix, so the imported target only ever has DEBUG and RELEASE listed in
# IMPORTED_CONFIGURATIONS. On a multi-config generator (Visual Studio), CMake maps any
# unmatched build config to the *first* entry of that list, which happens to be DEBUG
# (RocksDBTargets-debug.cmake sorts before RocksDBTargets-release.cmake). Without an
# explicit mapping, RelWithDebInfo and MinSizeRel builds would end up linking the
# /MDd-flavored rocksdb_d.lib into an /MD (NDEBUG) binary, causing a CRT/runtime-library
# mismatch that depends on nothing but that alphabetical ordering.
set_target_properties(${IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET} PROPERTIES
MAP_IMPORTED_CONFIG_RELWITHDEBINFO RELEASE
MAP_IMPORTED_CONFIG_MINSIZEREL RELEASE
)
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE ${IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET})
if (WITH_ZSTD)
# @todo do we actually need the zstd include dir or rather just pass
@@ -717,7 +693,8 @@ endif()
# Documentation
if(BUILD_DOCUMENTATION)
add_subdirectory(../docs/cpp-api docs/cpp-api)
set(CMAKE_MODULE_PATH "../docs/cmake")
add_subdirectory(../docs docs)
endif()
if(BUILD_EXAMPLES)
-1
View File
@@ -52,7 +52,6 @@ macro(SET_INSTALL_SELF_RPATH _target)
endmacro()
function(ifcopenshell_plugin_target TARGET)
# Plug-ins are loaded by exact filename and should not receive a platform library prefix.
set_target_properties(${TARGET} PROPERTIES PREFIX "")
if((NOT WIN32) AND BUILD_SHARED_LIBS AND NOT WASM_BUILD AND NOT CREATE_BUNDLE AND NOT CMAKE_INSTALL_RPATH AND COMMAND SET_INSTALL_SELF_RPATH)
SET_INSTALL_SELF_RPATH(${TARGET})
+34 -14
View File
@@ -1,15 +1,35 @@
find_package(Doxygen REQUIRED)
find_program(
SPHINX_EXECUTABLE
NAMES sphinx-build
REQUIRED
DOC "Path to the sphinx-build executable"
)
#Look for an executable called sphinx-build
find_program(SPHINX_EXECUTABLE NAMES sphinx-build DOC "Path to sphinx-build executable")
add_custom_target(
cpp_api_docs
COMMAND ${SPHINX_EXECUTABLE} -M html . output -W --keep-going
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating the IfcOpenShell C++ API documentation"
VERBATIM
)
include(FindPackageHandleStandardArgs)
#Handle standard arguments to find_package like REQUIRED and QUIET
find_package_handle_standard_args(Sphinx "Failed to find sphinx-build executable" SPHINX_EXECUTABLE)
find_package(Doxygen REQUIRED)
#find_package(Sphinx REQUIRED)
set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/docs/sphinx)
message(STATUS "SPHINX BUILD ${CMAKE_CURRENT_BINARY_DIR}")
file(MAKE_DIRECTORY ./output/doxygen)
if(DOXYGEN_FOUND)
add_custom_target(
Sphinx
ALL
COMMAND ${SPHINX_EXECUTABLE} -v -T -b html ${SPHINX_SOURCE} ${CMAKE_CURRENT_SOURCE_DIR}/output
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/output
COMMENT "Generating documentation with Sphinx"
)
# add_custom_target(ifcopenshell_python_docs ALL
# COMMAND make html
# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../src/ifcblenderexport/docs
# OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/../src/ifcblenderexport/docs
# COMMENT "Generating documentation with Sphinx")
else(DOXYGEN_FOUND)
message("Doxygen need to be installed to generate the doxygen documentation")
endif(DOXYGEN_FOUND)
+22 -63
View File
@@ -68,7 +68,7 @@ PROJECT_LOGO =
# entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used.
OUTPUT_DIRECTORY = ./output/doxygen
OUTPUT_DIRECTORY = ./output
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
# sub-directories (in 2 levels) under the output directory of each output format
@@ -852,7 +852,7 @@ WARNINGS = YES
# will automatically be disabled.
# The default value is: YES.
WARN_IF_UNDOCUMENTED = NO
WARN_IF_UNDOCUMENTED = YES
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
# potential errors in the documentation, such as documenting some parameters in
@@ -901,7 +901,7 @@ WARN_IF_UNDOC_ENUM_VAL = NO
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
# The default value is: NO.
WARN_AS_ERROR = FAIL_ON_WARNINGS
WARN_AS_ERROR = NO
# The WARN_FORMAT tag determines the format of the warning messages that doxygen
# can produce. The string should contain the $file, $line, and $text tags, which
@@ -944,6 +944,7 @@ WARN_LOGFILE =
# Note: If this tag is empty the current directory is searched.
INPUT = ../../src/ifcgeom \
../../src/ifcgeom_schema_agnostic \
../../src/ifcparse \
../../src/serializers \
@@ -1000,7 +1001,7 @@ RECURSIVE = YES
# Note that relative paths are relative to the directory from which doxygen is
# run.
EXCLUDE = ../../src/ifcparse/schemas
EXCLUDE =
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded
@@ -1024,33 +1025,7 @@ EXCLUDE_PATTERNS =
# wildcard * is used, a substring. Examples: ANamespace, AClass,
# ANamespace::AClass, ANamespace::*Test
EXCLUDE_SYMBOLS = "ifcopenshell::geom::opaque_number::*" \
ifcopenshell::entity::attribute_by_name_cmp \
ifcopenshell::impl::rocks_db_file_storage::rocksdb_types_iterator \
ifcopenshell::impl::in_memory_file_storage::type_iterator \
"util::string_buffer::*_item" \
util::string_buffer::item \
ifcopenshell::geom::layer_filter::wildcards_match \
ifcopenshell::paged_file_impl::entry \
ifcopenshell::token \
attribute_value::pointer_type \
INCLUDE_PARENT_PARENT_DIR \
POSTFIX_SCHEMA_ \
POSTFIX_SCHEMA__ \
STRINGIFY_ \
MAKE_INIT_FN_ \
MAKE_INIT_FN__ \
key_from_string \
add_ \
subtract_ \
multiply_ \
divide_ \
equals_ \
less_than_ \
negate_ \
ifcopenshell::geom::utils::create_cube \
ifcopenshell::geom::utils::create_polyhedron \
ifcopenshell::geom::utils::create_nef_polyhedron
EXCLUDE_SYMBOLS =
# The EXAMPLE_PATH tag can be used to specify one or more files or directories
# that contain example code fragments that are included (see the \include
@@ -1261,7 +1236,7 @@ IGNORE_PREFIX =
# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
# The default value is: YES.
GENERATE_HTML = NO
GENERATE_HTML = YES
# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
@@ -1336,7 +1311,7 @@ HTML_STYLESHEET =
# documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_STYLESHEET =
HTML_EXTRA_STYLESHEET = assets/doxygen-awesome-css/doxygen-awesome.css
# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the HTML output directory. Note
@@ -2191,7 +2166,7 @@ MAN_LINKS = NO
# captures the structure of the code including all documentation.
# The default value is: NO.
GENERATE_XML = YES
GENERATE_XML = NO
# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
@@ -2328,7 +2303,7 @@ ENABLE_PREPROCESSING = YES
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
MACRO_EXPANSION = YES
MACRO_EXPANSION = NO
# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
# the macro expansion is limited to the macros specified with the PREDEFINED and
@@ -2336,7 +2311,7 @@ MACRO_EXPANSION = YES
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
EXPAND_ONLY_PREDEF = YES
EXPAND_ONLY_PREDEF = NO
# If the SEARCH_INCLUDES tag is set to YES, the include files in the
# INCLUDE_PATH will be searched if a #include is found.
@@ -2369,17 +2344,7 @@ INCLUDE_FILE_PATTERNS =
# recursively expanded use the := operator instead of the = operator.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
PREDEFINED = IFC_PARSE_API= \
IFC_SCHEMA_API= \
IFC_GEOM_API= \
IFC_GEOMLIBRARY_API= \
IFC_GEOMSERIALIZATION_API= \
SERIALIZERS_API= \
"POSTFIX_SCHEMA(name)=name##_Schema" \
"Handle(name):=opencascade::handle<name>" \
kernel_=kernel \
Simplekernel_=Simplekernel \
inline=
PREDEFINED =
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The
@@ -2388,22 +2353,7 @@ PREDEFINED = IFC_PARSE_API= \
# definition found in the source code.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
EXPAND_AS_DEFINED = kernel_ \
cgal_shape \
cgal_kernel \
cgal_placement \
cgal_point \
cgal_direction \
cgal_vector \
cgal_plane \
cgal_curve \
cgal_wire \
cgal_face \
cgal_polyhedron \
cgal_vertex_descriptor \
cgal_face_descriptor \
create_cube \
create_polyhedron
EXPAND_AS_DEFINED =
# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
# remove all references to function-like macros that are alone on a line, have
@@ -2781,6 +2731,15 @@ DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
# files in one run (i.e. multiple -o and -T options on the command line). This
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
# this, this feature is disabled by default.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
DOT_MULTI_TARGETS = NO
# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
# explaining the meaning of the various boxes and arrows in the dot generated
# graphs.
+18 -41
View File
@@ -1,56 +1,33 @@
# IfcOpenShell C++ API documentation
This directory contains the Sphinx, Doxygen, Breathe, and Exhale configuration
for the IfcOpenShell C++ API reference. During a Sphinx build, Exhale runs
Doxygen, Breathe consumes the generated XML, and Exhale creates the API pages.
## Prerequisites
- Python 3.10 or newer
- [Doxygen](https://www.doxygen.nl/)
- [Graphviz](https://graphviz.org/)
Install the Python dependencies from this directory:
```shell
python -m pip install -r requirements.txt
```
Both `doxygen` and `dot` must be available on `PATH`. For the standard Windows
install locations, this can be done for the current PowerShell session with:
```powershell
$env:Path = "C:\Program Files\doxygen\bin;C:\Program Files\Graphviz\bin;$env:Path"
```
This folder contains the setup to build the IfcOpenShell C++ API documentation from the source code.
## Generating the documentation
From this directory, run:
> Prerequisites:
>
> Make sure to have [Doxygen](https://www.doxygen.nl) and [Graphviz](https://graphviz.org) installed into your `$PATH` variable.
>
> The documentation also use the [doxygen-awesome](https://jothepro.github.io/doxygen-awesome-css) theme as a git submodule.
Build with the command (from within the `/docs/cpp-api` folder):
```shell
python -m sphinx -M html . output -W --keep-going
$ doxygen
```
To include the current Git commit in Doxygen's project metadata, set
`PROJECT_NUMBER` before building. For example, in PowerShell:
```powershell
$env:PROJECT_NUMBER = git rev-parse --short HEAD
python -m sphinx -M html . output -W --keep-going
```
Or in a POSIX shell:
To include the current git commit hash into the build documentation, use the following command:
```shell
PROJECT_NUMBER=$(git rev-parse --short HEAD) python -m sphinx -M html . output -W --keep-going
$ PROJECT_NUMBER=$(git rev-parse --short HEAD) doxygen
```
Alternatively, configure the main CMake project with
`-DBUILD_DOCUMENTATION=ON` and build the `cpp_api_docs` target.
This will extract the current commit hash in short version and sets the propper ENV variable used by doxygen.
The generated documentation is written to `output/html/index.html`. The
generated Doxygen XML and Exhale sources are kept under `output/` as build
artifacts.
The generation of the documentation might take a while depending on your systems hardware, as it is configured to generate the Class graphs using .
The generated headers under `src/ifcparse/schemas` are intentionally excluded
from this documentation build.
The resulting documentation is located unter `/cpp-api/output/html` and can be directly accessed with your browser:
```shell
$ open ./output/html/index.html
```
-59
View File
@@ -1,59 +0,0 @@
# This file was generated with the assistance of an AI coding tool.
import warnings
from pathlib import Path
from shutil import rmtree
from sphinx.deprecation import RemovedInSphinx90Warning
warnings.filterwarnings("ignore", category=RemovedInSphinx90Warning, module=r"exhale\.configs")
generated_directories = (
Path(__file__).parent / "output" / "api",
Path(__file__).parent / "output" / "doxygen",
)
for generated_directory in generated_directories:
if generated_directory.is_dir():
rmtree(generated_directory)
project = "IfcOpenShell"
copyright = "2020, IfcOpenShell"
extensions = [
"breathe",
"exhale",
]
primary_domain = "cpp"
highlight_language = "cpp"
html_theme = "alabaster"
breathe_projects = {
"IfcOpenShell": "./output/doxygen/xml",
}
breathe_default_project = "IfcOpenShell"
exhale_args = {
"containmentFolder": "./output/api",
"rootFileName": "library_root.rst",
"rootFileTitle": "IfcOpenShell C++ API",
"doxygenStripFromPath": "../..",
"createTreeView": False,
"exhaleExecutesDoxygen": True,
"exhaleUseDoxyfile": True,
}
cpp_id_attributes = [
"IFC_PARSE_API",
"IFC_SCHEMA_API",
"IFC_GEOM_API",
"IFC_GEOMLIBRARY_API",
"IFC_GEOMSERIALIZATION_API",
"SERIALIZERS_API",
]
exclude_patterns = [
"output/doctrees",
"output/doxygen",
"output/html",
]
-9
View File
@@ -1,9 +0,0 @@
.. This file was generated with the assistance of an AI coding tool.
IfcOpenShell C++ API
====================
.. toctree::
:maxdepth: 2
output/api/library_root
-5
View File
@@ -1,5 +0,0 @@
# This file was generated with the assistance of an AI coding tool.
Sphinx==8.1.3
breathe==4.36.0
exhale==0.3.7
+81 -283
View File
@@ -1,8 +1,5 @@
#!/usr/bin/python
# /// script
# dependencies = [
# "typing_extensions",
# ]
# ///
###############################################################################
# #
@@ -32,7 +29,17 @@ Example usage:
python build-all.py IfcParse IfcOpenShell-Python
Run with --help to see available arguments.
Available arguments:
``-py-313`` - build for specific Python version
(building for all supported Python version by default).
``-wasm`` - compile for wasm
``-without-xxx`` - do not build dependency ``xxx`` (e.g. ``--without-swig``)
``-mac-cross-compile-intel`` - cross compile for Intel Mac on Apple Silicon host
``-shared`` - build shared libraries. By default will build static.
``-ifcopenshell-shared`` - build only IfcOpenShell's own libraries as shared
(dependencies stay static). Redundant if ``-shared`` is also passed.
``-diskcleanup`` - clean up build directories after finishing building dependencies
``-v`` - enable verbose logs
Used environment variables:
@@ -61,8 +68,6 @@ Used environment variables:
`ADD_COMMIT_SHA` and `VERSION_OVERRIDE` will be set to `ON` while configuring IfcOpenShell
- ``BUILD_BONSAIVIEWER`` - enable building BonsaiViewer, `off` by default.
- ``IFCOS_BUILD_PYTHON_WRAPPER`` - enable building the Python wrapper, `on` by default.
- ``PYTHON_USER_SITE`` - install the Python wrapper into the user's site-packages directory
instead of the interpreter's prefix, `off` by default.
# This script builds IfcOpenShell and its dependencies #
# #
@@ -107,9 +112,6 @@ Used environment variables:
"""
from __future__ import annotations
import argparse
import glob
import logging
import multiprocessing
@@ -121,17 +123,14 @@ import subprocess as sp
import sys
import sysconfig
import tarfile
import textwrap
import threading
import time
from collections.abc import Generator, Sequence
from datetime import datetime
from pathlib import Path
from typing import Literal, NamedTuple
from typing import Literal
from urllib.request import urlretrieve
from typing_extensions import assert_never
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
@@ -155,9 +154,8 @@ ADD_COMMIT_SHA = is_on_off(os.getenv("ADD_COMMIT_SHA"), default=False)
IFCOS_BUILD_PYTHON_WRAPPER = is_on_off(os.getenv("IFCOS_BUILD_PYTHON_WRAPPER"), default=True)
BUILD_BONSAIVIEWER = is_on_off(os.getenv("BUILD_BONSAIVIEWER"), default=False)
USE_OCCT = is_on_off(os.getenv("USE_OCCT"), default=True)
PYTHON_USER_SITE = is_on_off(os.getenv("PYTHON_USER_SITE"), default=False)
PYTHON_VERSIONS = ["3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0", "3.15.0"]
PYTHON_VERSIONS = ["3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
JSON_VERSION = "3.11.3"
OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.8.1"
@@ -196,142 +194,10 @@ strip = "strip"
xz = "xz" # Used implicitly for `tar -xf *.tar.xz`.
brew = "brew"
class Args(NamedTuple):
explicit_targets: list[str]
build_examples: bool
diskcleanup: bool
lto: bool
verbose: bool
shared: bool
ifcopenshell_shared: bool
occt_shared: bool
mac_cross_compile_intel: bool
wasm: bool
class DynamicArgs(NamedTuple):
without: set[str]
py_versions: set[str]
occt_version: str | None
@classmethod
def from_unknown_flags(cls, unknown_flags: list[str], arg_parser: argparse.ArgumentParser) -> DynamicArgs:
flags = set(s.lstrip("-") for s in unknown_flags if s.startswith("-"))
without: set[str] = set()
py_versions: set[str] = set()
occt_versions: set[str] = set()
leftover: set[str] = set()
for f in flags:
if f.startswith("without-"):
without.add(f.removeprefix("without-").lower())
elif f.startswith("py-"):
py_versions.add(f.removeprefix("py-"))
elif f.startswith("occt-"):
occt_versions.add(f.removeprefix("occt-"))
else:
leftover.add(f)
if leftover:
arg_parser.error(f"unrecognized arguments: {', '.join('-' + f for f in sorted(leftover))}")
if len(occt_versions) > 1:
arg_parser.error(f"more than one OCCT version provided: {', '.join(sorted(occt_versions))}")
occt_version = next(iter(occt_versions), None)
return cls(without=without, py_versions=py_versions, occt_version=occt_version)
def parse_args() -> tuple[Args, DynamicArgs]:
arg_parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent("""\
Additional dynamic -flags (not declared above):
-py-313 build for specific Python version
(building for all supported Python versions by default)
-occt-xxx use a specific OCCT version (e.g. -occt-7.8.1) instead of the default
-without-xxx do not build dependency `xxx` (e.g. --without-swig)"""),
)
arg_parser.add_argument("explicit_targets", nargs="*", help="Targets provided by CLI.")
arg_parser.add_argument(
"--build-examples",
action="store_true",
default=False,
help="Build IfcOpenShell examples.",
)
arg_parser.add_argument(
"-diskcleanup",
"--diskcleanup",
action="store_true",
default=False,
help="Clean up build directories after finishing building dependencies.",
)
arg_parser.add_argument(
"-lto",
"--lto",
action="store_true",
default=False,
help="Enable link-time optimization (adds -flto to compiler flags).",
)
arg_parser.add_argument(
"-v",
"--verbose",
action="store_true",
default=False,
help="Enable verbose logs.",
)
arg_parser.add_argument(
"-shared",
"--shared",
action="store_true",
default=False,
help="Build shared libraries. By default will build static.",
)
arg_parser.add_argument(
"-ifcopenshell-shared",
"--ifcopenshell-shared",
action="store_true",
default=False,
help="Build only IfcOpenShell's own libraries as shared (dependencies stay static). "
"Redundant if -shared is also passed.",
)
arg_parser.add_argument(
"--occt-shared",
action="store_true",
default=False,
help="Build OCCT as shared. Redundant if -shared is also passed.",
)
arg_parser.add_argument(
"-mac-cross-compile-intel",
"--mac-cross-compile-intel",
action="store_true",
default=False,
help="Cross compile for Intel Mac on Apple Silicon host.",
)
arg_parser.add_argument("-wasm", "--wasm", action="store_true", default=False, help="Compile for wasm.")
namespace, unknown_flags = arg_parser.parse_known_args()
args = Args(
explicit_targets=namespace.explicit_targets,
build_examples=namespace.build_examples,
diskcleanup=namespace.diskcleanup,
lto=namespace.lto,
verbose=namespace.verbose,
shared=namespace.shared,
ifcopenshell_shared=namespace.ifcopenshell_shared or namespace.shared,
occt_shared=namespace.occt_shared or namespace.shared,
mac_cross_compile_intel=namespace.mac_cross_compile_intel,
wasm=namespace.wasm,
)
dynamic_args = DynamicArgs.from_unknown_flags(unknown_flags, arg_parser)
return args, dynamic_args
ARGS, DYNAMIC_ARGS = parse_args()
explicit_targets: set[str] = set(ARGS.explicit_targets)
explicit_targets = [s for s in sys.argv[1:] if not s.startswith("-")]
"""Targets provided by CLI."""
flags = set(s.lstrip("-") for s in sys.argv[1:] if s.startswith("-"))
"""CLI flags."""
# Helper function for coloured printing
@@ -350,11 +216,12 @@ def cecho(message, color=NO_COLOR):
logger.info(f"{color}{message}\033[0m")
# Flags.
APPLE = platform.system() == "Darwin"
MAC_CROSS_COMPILE_INTEL = ARGS.mac_cross_compile_intel
MAC_CROSS_COMPILE_INTEL = "mac-cross-compile-intel" in flags
assert platform.system() == "Darwin" or not MAC_CROSS_COMPILE_INTEL
WASM = ARGS.wasm
WASM = "wasm" in flags
"""Build WASM outside pyodide build environment."""
WASM_CMAKE_IS_USING_INIT_VARS = False
if WASM:
@@ -500,12 +367,12 @@ dependency_tree: dict[str, tuple[str, ...]] = {
def gather_dependencies(dep: str) -> Generator[str]:
yield dep
for d in dependency_tree[dep]:
if d.lower() not in DYNAMIC_ARGS.without:
if f"without-{d.lower()}" not in flags:
for x in gather_dependencies(d):
yield x
if ARGS.verbose:
if "v" in flags:
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter)
@@ -526,26 +393,29 @@ else:
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
OFF_ON = ["OFF", "ON"]
BUILD_STATIC = not ARGS.shared
BUILD_STATIC = "shared" not in flags
"""Whether dependencies are built static."""
IFCOPENSHELL_STATIC = BUILD_STATIC and "ifcopenshell-shared" not in flags
"""Whether IfcOpenShell's own libraries are built static."""
ENABLE_FLAG = "--enable-static" if BUILD_STATIC else "--enable-shared"
DISABLE_FLAG = "--disable-shared" if BUILD_STATIC else "--disable-static"
LINK_TYPE = "static" if BUILD_STATIC else "shared"
LINK_TYPE_UCFIRST = LINK_TYPE.capitalize()
LIBRARY_EXT = "a" if BUILD_STATIC else "so"
PIC = "-fPIC" if BUILD_STATIC else ""
if DYNAMIC_ARGS.py_versions:
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if "".join(pyv.split(".")[:2]) in DYNAMIC_ARGS.py_versions]
if any(f.startswith("py-") for f in flags):
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if f"py-{''.join(pyv.split('.')[:2])}" in flags]
if DYNAMIC_ARGS.occt_version is not None:
OCCT_VERSION = DYNAMIC_ARGS.occt_version
if any(f.startswith("occt-") for f in flags):
OCCT_VERSION = next(f.split("-", 1)[1] for f in flags if f.startswith("occt-"))
if explicit_targets:
targets = {dep for target in explicit_targets for dep in gather_dependencies(target)}
else:
targets = set(dependency_tree.keys())
targets = set(t for t in targets if t.lower() not in DYNAMIC_ARGS.without)
targets = set(t for t in targets if "without-%s" % t.lower() not in flags)
if not explicit_targets and not BUILD_BONSAIVIEWER:
targets.difference_update({"BonsaiViewer", "qt6"})
if BUILD_BONSAIVIEWER:
@@ -562,6 +432,7 @@ if WASM:
SKIP_TARGETS_FOR_WASM = {
"rocksdb",
"opencollada",
"swig",
"pcre",
"IfcGeom",
"IfcConvert",
@@ -583,8 +454,12 @@ bison = "bison"
missing_commands: list[str] = []
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz, bison]
if WASM:
if "wasm" in flags:
# Skip swig build for WASM.
required_commands.append("swig")
required_commands.append("pyodide")
required_commands.remove(yacc)
required_commands.remove(bison)
if platform.system() == "Linux" and "BonsaiViewer" in targets:
required_commands.append("patchelf")
@@ -628,7 +503,7 @@ def restore_env(var_name: str, old_value: str | None) -> None:
os.environ[var_name] = old_value
def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False, env: dict[str, str] | None = None) -> str:
def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False) -> str:
"""
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
sets up logging `stderr` to `LOG_FILE` (in append mode) and returns stdout
@@ -652,7 +527,7 @@ def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False, env
# Ensure both live logs available in the log file
# and the putput.
with open(LOG_FILE, "a", encoding="utf-8") as log_file_handle:
proc = sp.Popen(cmds, cwd=cwd, stdout=sp.PIPE, stderr=sp.PIPE, encoding="utf-8", env=env)
proc = sp.Popen(cmds, cwd=cwd, stdout=sp.PIPE, stderr=sp.PIPE, encoding="utf-8")
assert proc.stdout and proc.stderr
t_out = threading.Thread(target=stream_reader, args=(proc.stdout, stdout, log_file_handle))
@@ -701,7 +576,7 @@ def run_autoconf(dependency_name: str, configure_args: list[str], cwd: str) -> N
prefix = os.path.realpath(f"{DEPS_DIR}/install/{dependency_name}")
wasm = []
if WASM:
if "wasm" in flags:
wasm.append("emconfigure")
run(
@@ -709,7 +584,7 @@ def run_autoconf(dependency_name: str, configure_args: list[str], cwd: str) -> N
*wasm,
"/bin/sh",
"../configure",
*(["--host=wasm32"] if WASM and not any(s.startswith("--host") for s in configure_args) else []),
*(["--host=wasm32"] if "wasm" in flags and not any(s.startswith("--host") for s in configure_args) else []),
*configure_args,
f"--prefix={prefix}",
],
@@ -717,20 +592,18 @@ def run_autoconf(dependency_name: str, configure_args: list[str], cwd: str) -> N
)
def run_cmake(
name, cmake_args: list[str], cmake_dir: str | None = None, cwd: str | None = None, native: bool = False
) -> None:
def run_cmake(arg1, cmake_args: list[str], cmake_dir: str | None = None, cwd: str | None = None):
if cmake_dir is None:
P = ".."
else:
P = cmake_dir
wasm = []
if WASM and not native:
if "wasm" in flags:
wasm.append("emcmake")
cmake_flags: list[str] = []
if not native and (not WASM or not WASM_CMAKE_IS_USING_INIT_VARS):
if not WASM or not WASM_CMAKE_IS_USING_INIT_VARS:
# For WASM we provide flags using just environment variables.
# If we provide them using cmake vars, it will override emscripten toolchain flags.
# Unsure if we need this in general even for non-WASM builds.
@@ -746,10 +619,6 @@ def run_cmake(
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
)
if WASM and native:
# Override emscripten cmake toolchain coming from environment variable.
cmake_flags.append("-DCMAKE_TOOLCHAIN_FILE=")
run(
[
*wasm,
@@ -758,7 +627,7 @@ def run_cmake(
*cmake_flags,
*cmake_args,
f"-DCMAKE_BUILD_TYPE={BUILD_CFG}",
*([] if native else [f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}"]),
f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}",
],
cwd=cwd,
)
@@ -806,7 +675,7 @@ def build_dependency(
additional_files: dict[str, str] | None = None,
no_append_name=False,
cmake_dir=None,
cmake_native: bool = False,
**kwargs,
) -> None:
"""Handles building of dependencies with different tools (which are
distinguished with the `mode` argument. `build_tool_args` is expected to be
@@ -815,8 +684,7 @@ def build_dependency(
:param pre_compile_subs: A sequence of ``(fn, before, after)``
:param additional_files: Mapping path->url.
:param cmake_native: For ``mode="cmake"``, force a native (host) build
even when building for WASM. Needed for build-time tools like swig.
:param kwargs: Additional ``mode`` related kwargs.
"""
check_dir = os.path.join(DEPS_DIR, "install", name)
if os.path.exists(check_dir):
@@ -852,7 +720,7 @@ def build_dependency(
logger.info(f"\rChecking {name}... ")
git_clone_or_pull_repository(download_url, target_dir=os.path.join(build_dir, download_name), revision=revision)
else:
assert_never(download_tool)
raise ValueError(f"download tool '{download_tool}' is not supported")
download_dir = os.path.join(build_dir, download_name)
if os.path.isdir(download_dir):
@@ -913,9 +781,9 @@ def build_dependency(
if mode == "autoconf":
run_autoconf(name, build_tool_args, cwd=extract_build_dir)
elif mode == "cmake":
run_cmake(name, build_tool_args, cwd=extract_build_dir, native=cmake_native)
run_cmake(name, build_tool_args, cwd=extract_build_dir)
else:
assert_never(mode)
raise ValueError()
for fn, before, after in pre_compile_subs:
with open(os.path.join(extract_dir, fn), "r") as f:
s = f.read()
@@ -931,14 +799,14 @@ def build_dependency(
logger.info(f"\rConfiguring {name}...")
run([bash, "./bootstrap.sh"], cwd=extract_dir)
logger.info(f"\rBuilding {name}... ")
run(["./b2", f"-j{IFCOS_NUM_BUILD_PROCS}"] + build_tool_args, cwd=extract_dir, can_fail=WASM)
run(["./b2", f"-j{IFCOS_NUM_BUILD_PROCS}"] + build_tool_args, cwd=extract_dir, can_fail="wasm" in flags)
logger.info(f"\rInstalling {name}... ")
shutil.copytree(
os.path.join(extract_dir, "boost"), os.path.join(DEPS_DIR, "install", f"boost-{BOOST_VERSION}", "boost")
)
logger.info(f"\rInstalled {name} \n")
if ARGS.diskcleanup:
if "diskcleanup" in flags:
shutil.rmtree(build_dir, ignore_errors=True)
@@ -1044,9 +912,7 @@ ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
CXXFLAGS_SHARED = CXXFLAGS_MINIMAL
CFLAGS_SHARED = CFLAGS_MINIMAL
if WASM:
if "wasm" in flags:
# WASM `SIDE_MODULE_` are absorbed by `emcmake` automatically.
CXXFLAGS = CXXFLAGS_MINIMAL
CFLAGS = CFLAGS_MINIMAL
@@ -1055,19 +921,19 @@ elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev
CXXFLAGS = f"{CXXFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
CFLAGS = f"{CFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden {ADDITIONAL_ARGS_STR}"
else:
CXXFLAGS = CXXFLAGS_SHARED
CFLAGS = CFLAGS_SHARED
CXXFLAGS = CXXFLAGS_MINIMAL
CFLAGS = CFLAGS_MINIMAL
LDFLAGS = f"{LDFLAGS} -Wl,--gc-sections {ADDITIONAL_ARGS_STR}"
else:
if BUILD_STATIC:
CXXFLAGS = f"{CXXFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
CFLAGS = f"{CFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
else:
CXXFLAGS = CXXFLAGS_SHARED
CFLAGS = CFLAGS_SHARED
CXXFLAGS = CXXFLAGS_MINIMAL
CFLAGS = CFLAGS_MINIMAL
LDFLAGS = f"{LDFLAGS} {ADDITIONAL_ARGS_STR}"
if ARGS.lto:
if "lto" in flags:
for f in compiler_flags:
locals()[f] += f" -flto={IFCOS_NUM_BUILD_PROCS}"
@@ -1144,46 +1010,31 @@ if "swig" in targets:
download_name="swig",
download_tool=download_tool_git,
revision=f"v{SWIG_VERSION}",
cmake_native=WASM,
)
if USE_OCCT and "occ" in targets:
occt_args: list[str] = []
patches: list[str] = []
occt_link_type = "Shared" if ARGS.occt_shared else "Static"
occt_name = f"occt-shared-{OCCT_VERSION}" if ARGS.occt_shared else f"occt-{OCCT_VERSION}"
OCCT_INSTALL_PATH = f"{DEPS_DIR}/install/{occt_name}"
if OCCT_VERSION < "7.4":
patches.append("./patches/occt/enable-exception-handling.patch")
# Skip ExpToCasExe as we don't need it and it requires additional dependencies.
# Before 7.7.2 ExpToCasExe is part of DataExchange, DETools doesn't exist yet.
# Since we do need DataExchange (used for iges_serializer), we use a patch to skip only ExpToCasExe.
# Since we do need DataExchange (used for IgesSerializer), we use a patch to skip only ExpToCasExe.
if "7.7.2" > OCCT_VERSION >= "7.7":
patches.append("./patches/occt/no_ExpToCasExe.patch")
elif OCCT_VERSION >= "7.7.2":
occt_args.append("-DBUILD_MODULE_DETools=OFF")
if WASM:
if "wasm" in flags:
patches.append("./patches/occt/no_em_js.patch")
if ARGS.occt_shared:
# Using static flags for shared builds break it
# (e.g. `-fvisibility=hidden` hides many symbols).
# So we temporarily override flags.
OLD_CPP_FLAGS = os.environ["CPPFLAGS"]
OLD_CXX_FLAGS = os.environ["CXXFLAGS"]
OLD_C_FLAGS = os.environ["CFLAGS"]
os.environ["CXXFLAGS"] = CXXFLAGS_SHARED
os.environ["CPPFLAGS"] = CXXFLAGS_SHARED
os.environ["CFLAGS"] = CFLAGS_SHARED
build_dependency(
name=occt_name,
name=f"occt-{OCCT_VERSION}",
mode="cmake",
build_tool_args=[
f"-DINSTALL_DIR={OCCT_INSTALL_PATH}",
f"-DBUILD_LIBRARY_TYPE={occt_link_type}",
f"-DINSTALL_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}",
f"-DBUILD_LIBRARY_TYPE={LINK_TYPE_UCFIRST}",
f"-DBUILD_MODULE_Draw=0",
f"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off",
# Disable xlib explicitly, as it tries to use it on Desktop Ubuntu, adding unnecessary dependency.
@@ -1202,11 +1053,6 @@ if USE_OCCT and "occ" in targets:
patch=patches,
revision="V" + OCCT_VERSION.replace(".", "_"),
)
if ARGS.occt_shared:
restore_env("CPPFLAGS", OLD_CPP_FLAGS)
restore_env("CXXFLAGS", OLD_CXX_FLAGS)
restore_env("CFLAGS", OLD_C_FLAGS)
elif "occ" in targets:
build_dependency(
name=f"oce-{OCE_VERSION}",
@@ -1264,7 +1110,7 @@ if "libxml2" in targets:
"--without-iconv",
"--without-lzma",
]
if WASM:
if "wasm" in flags:
build_tool_args.append("--without-threads")
build_dependency(
f"libxml2-{LIBXML2_VERSION}",
@@ -1286,7 +1132,7 @@ if "OpenCOLLADA" in targets:
# whether shared libs were actually built. We make it follow `USE_SHARED` instead.
patches.append("./patches/opencollada/config_select_libs_by_use_shared.patch")
if WASM:
if "wasm" in flags:
# This is necessary for the WASM build, because recent versions of
# clang don't have the tr1:: namespace anymore. However, it breaks
# some versions of gcc (9.4.0 at least) due to specializing std::hash
@@ -1316,15 +1162,7 @@ if "OpenCOLLADA" in targets:
revision=OPENCOLLADA_VERSION,
)
def python_consider_rc(python_version: str) -> str:
# TODO: remove after Python 3.15 release.
if python_version == "3.15.0":
python_version += "rc1"
return python_version
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and not WASM:
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flags:
# Python should not be built with -fvisibility=hidden, from experience that introduces segfaults
OLD_CPP_FLAGS = os.environ["CPPFLAGS"]
OLD_CXX_FLAGS = os.environ["CXXFLAGS"]
@@ -1352,16 +1190,13 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and not WASM:
PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"])
for PYTHON_VERSION in PYTHON_VERSIONS:
python_version_url = PYTHON_VERSION
PYTHON_VERSION = python_consider_rc(PYTHON_VERSION)
# Don't fail silently on missing Python dependencies (e.g. openssl or zlib),
# because later ifcopenshell-python build will fail too but in a more confusing way.
build_dependency(
f"python-{PYTHON_VERSION}",
"autoconf",
PYTHON_CONFIGURE_ARGS,
f"http://www.python.org/ftp/python/{python_version_url}/",
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
f"Python-{PYTHON_VERSION}.tgz",
)
python_install = INSTALL_DIR / f"python-{PYTHON_VERSION}"
@@ -1388,7 +1223,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and not WASM:
if "boost" in targets:
str_concat = lambda prefix: lambda postfix: "" if postfix.strip() == "" else "=".join((prefix, postfix.strip()))
toolset = []
if WASM:
if "wasm" in flags:
toolset.append("toolset=emscripten")
build_dependency(
f"boost-{BOOST_VERSION}",
@@ -1416,7 +1251,7 @@ if "boost" in targets:
# patch="./patches/boost/boostorg_regex_62.patch",
download_name=f"boost-{BOOST_VERSION}-b2-nodocs.tar.gz",
)
if WASM:
if "wasm" in flags:
# only supported on nix for now
run(
("find", ".", "-name", "*.bc", "-exec", "bash", "-c", "emar q ${1%.bc}.a $1", "bash", "{}", ";"),
@@ -1458,7 +1293,9 @@ if "cgal" in targets:
name=f"gmp-{GMP_VERSION}",
mode="autoconf",
build_tool_args=[ENABLE_FLAG, DISABLE_FLAG, "--with-pic", *gmp_args],
pre_compile_subs=([("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if WASM else []),
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/",
@@ -1580,7 +1417,7 @@ if "qt6" in targets:
cecho("Building IfcOpenShell:", GREEN)
IFCOS_DIR = os.path.join(DEPS_DIR, "build", "ifcopenshell")
if not is_on_off(os.getenv("NO_CLEAN"), default=False):
if os.environ.get("NO_CLEAN", "").lower() not in {"1", "on", "true"}:
if os.path.exists(IFCOS_DIR):
shutil.rmtree(IFCOS_DIR)
os.makedirs(IFCOS_DIR, exist_ok=True)
@@ -1590,8 +1427,8 @@ os.makedirs(ifcos_build_dir, exist_ok=True)
cmake_args = [
"-DUSE_MMAP=OFF",
f"-DBUILD_EXAMPLES={OFF_ON[ARGS.build_examples]}",
"-DBUILD_SHARED_LIBS=" + OFF_ON[ARGS.ifcopenshell_shared],
"-DBUILD_EXAMPLES=OFF",
"-DBUILD_SHARED_LIBS=" + OFF_ON[not IFCOPENSHELL_STATIC],
"-DGLTF_SUPPORT=ON",
"-DBoost_NO_BOOST_CMAKE=On",
"-DCREATE_BUNDLE=On",
@@ -1620,7 +1457,7 @@ def get_cmake_args_prefix_path(additional_paths: Sequence[str] = ()) -> list[str
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
if WASM:
if "wasm" in flags:
# Boost is built by the build script so should not be found
# inside of the sysroot set by the emscriptem toolchain
cmake_args.append("-DWASM_BUILD=On")
@@ -1636,7 +1473,7 @@ if "cgal" in targets:
cmake_args.append(f"-DCGAL_WITH_GMPXX=Off")
if "occ" in targets and USE_OCCT:
cmake_args_prefix_path.append(OCCT_INSTALL_PATH)
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/occt-{OCCT_VERSION}")
elif "occ" in targets:
# We don't support find_package for OCE.
@@ -1686,10 +1523,7 @@ if "rocksdb" in targets:
)
if "swig" in targets:
# `cmake_args_prefix_path` won't work on wasm
# because `find_program` in emscripten toolchain don't use `find_root_path`.
# As a workaround we provide executable path directly on all platforms.
cmake_args.append(f"-DSWIG_EXECUTABLE={DEPS_DIR}/install/swig-{SWIG_VERSION}/bin/swig")
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/swig-{SWIG_VERSION}")
if os.environ.get("QT_DIR"):
cmake_args_prefix_path.append(os.environ["QT_DIR"])
@@ -1722,42 +1556,6 @@ if not WASM and (
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=ifcos_build_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=ifcos_build_dir)
def test_examples() -> None:
cecho("Running examples...", GREEN)
examples_bin_dir = Path(DEPS_DIR) / "install" / "ifcopenshell" / "bin"
examples_env = os.environ.copy()
ld_library_paths = ["../lib"]
if ARGS.occt_shared:
ld_library_paths.append(f"{OCCT_INSTALL_PATH}/lib")
examples_env["LD_LIBRARY_PATH"] = os.pathsep.join(ld_library_paths)
examples: dict[tuple[str, ...], str | None] = {
("./IfcOpenHouse",): "IfcOpenHouse.ifc",
("./IfcParseExamples", "IfcOpenHouse.ifc"): None,
("./IfcAdvancedHouse",): "IfcAdvancedHouse.ifc",
}
# Only for ifc4x3 schema.
if (examples_bin_dir / "IfcAlignment").is_file():
examples[("./IfcAlignment",)] = "FHWA_Bridge_Geometry_Alignment_Example.ifc"
examples[("./IfcSimplifiedAlignment",)] = "FHWA_Bridge_Geometry_Alignment_Example_Simplified.ifc"
produced_files: set[str] = set()
try:
for cmd, expected_file in examples.items():
run(cmd, cwd=str(examples_bin_dir), env=examples_env)
if expected_file is None:
continue
if not (examples_bin_dir / expected_file).is_file():
raise RuntimeError(f"Example `{' '.join(cmd)}` did not produce expected file '{expected_file}'.")
produced_files.add(expected_file)
finally:
for produced_file in produced_files:
(examples_bin_dir / produced_file).unlink(missing_ok=True)
if ARGS.build_examples:
test_examples()
if "IfcOpenShell-Python" in targets:
wrapper_ldflags = ""
if platform.system() == "Darwin":
@@ -1809,7 +1607,8 @@ if "IfcOpenShell-Python" in targets:
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX=" + OFF_ON[PYTHON_USER_SITE],
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
],
cmake_dir=CMAKE_DIR,
cwd=ifcos_build_dir,
@@ -1836,7 +1635,7 @@ if "IfcOpenShell-Python" in targets:
if platform.system() != "Darwin":
if BUILD_CFG == "Release":
for so in glob.glob(os.path.join(module_dir, "*.so")):
if WASM:
if "wasm" in flags:
run(["wasm-strip", so, "-k", "dylink.0"])
elif os.path.basename(so).startswith("_ifcopenshell_wrapper"):
# TODO: This symbol name depends on the Python version?
@@ -1846,7 +1645,7 @@ if "IfcOpenShell-Python" in targets:
return module_dir
if WASM:
if "wasm" in flags:
compile_python_wrapper(
run(["pyodide", "config", "get", "python_version"]),
run(["pyodide", "config", "get", "python_include_dir"]),
@@ -1862,7 +1661,6 @@ if "IfcOpenShell-Python" in targets:
compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable)
else:
for python_version in PYTHON_VERSIONS:
python_version = python_consider_rc(python_version)
python_path = INSTALL_DIR / f"python-{python_version}"
module_dir = compile_python_wrapper(python_version, python_path=python_path)
assert module_dir
-346
View File
@@ -1,346 +0,0 @@
#!/usr/bin/env -S uv run --script
# /// script
# ///
import argparse
import os
import platform
import re
import shlex
import shutil
import subprocess
from pathlib import Path
from typing import Literal
def run(
*cmd: str,
cwd: Path | None = None,
env: dict[str, str] | None = None,
stderr: int | None = None,
) -> str:
print("$", shlex.join(cmd))
return subprocess.check_output(cmd, cwd=cwd, env=env, stderr=stderr, text=True)
REPO_ROOT = Path(run("git", "-C", str(Path(__file__).parent), "rev-parse", "--show-toplevel").strip())
VERSION = "v" + (REPO_ROOT / "VERSION").read_text().strip()
def is_platform(name: Literal["MAC", "LINUX"]) -> bool:
current = "MAC" if platform.system() == "Darwin" else "LINUX"
return current == name
def get_install_dir() -> Path:
if is_platform("MAC"):
pattern = "Darwin/*/*/install"
else:
pattern = "*/*/install"
for data in (REPO_ROOT / "build").glob(pattern):
return data
raise Exception("No install dir found")
def find_qt_dir(install_root: Path, qt6_version: str) -> Path | None:
for qt_candidate in install_root.glob(f"qt6-{qt6_version}-*/{qt6_version}/*"):
if (qt_candidate / "lib").is_dir():
return qt_candidate
return None
def ensure_soname_links(dest: Path) -> None:
"""Ensure that all shared libraries in `dest` are present using their SONAMEs (at least as symlinks)."""
for shared_object in dest.glob("*.so*"):
if not shared_object.is_file():
continue
try:
readelf_output = run("readelf", "-d", str(shared_object))
except subprocess.CalledProcessError:
continue
# TODO: actual pattern is "Library soname" instead of "Shared library"?
match = re.search(r"\(SONAME\).*Shared library: \[(.*)\]", readelf_output)
if not match:
continue
soname = match.group(1)
soname_path = dest / soname
if soname_path.exists():
continue
soname_path.symlink_to(shared_object.name)
def is_shared_library(path: Path) -> bool:
name = path.name.lower()
return name.endswith((".so", ".dylib", ".dll")) or ".so." in name
def stage_runtime_payload(ifcopenshell_install_dir: Path, dest: Path, *, include_geometry_writers: bool = True) -> None:
"""Copy all libs from `ifcopenshell_install_dir/{bin,lib,lib64}` into `dest`."""
for runtime_dir_name in ("bin", "lib", "lib64"):
runtime_dir = ifcopenshell_install_dir / runtime_dir_name
if not runtime_dir.is_dir():
continue
for runtime_file in runtime_dir.rglob("*"):
if not (runtime_file.is_symlink() or runtime_file.is_file()):
continue
if not is_shared_library(runtime_file):
continue
if not include_geometry_writers and runtime_file.name.startswith("ifcopenshell.geometry.writer."):
continue
shutil.copy(runtime_file, dest / runtime_file.name, follow_symlinks=False)
if not is_platform("MAC"):
ensure_soname_links(dest)
def stage_qt_runtime_payload(exe_path: Path, dest: Path, qt_dir: Path | None) -> None:
"""Copy QT libs/plugins from `qt_dir` next to `exe_path`, if it depends on QT."""
def is_so_file(path: Path) -> bool:
return (path.is_file() or path.is_symlink()) and ".so" in path.name
if not qt_dir or not (qt_dir / "lib").is_dir():
return
# Skip executables that don't depend on QT (don't have `libQt6` referenced).
env = os.environ.copy()
env["LD_LIBRARY_PATH"] = f"{qt_dir / 'lib'}:{env.get('LD_LIBRARY_PATH', '')}"
try:
ldd_output = run("ldd", str(exe_path), env=env)
except subprocess.CalledProcessError:
return
if "libQt6" not in ldd_output:
return
# Copy all QT libs to `dest`.
for lib_file in (qt_dir / "lib").iterdir():
if is_so_file(lib_file):
shutil.copy(lib_file, dest / lib_file.name, follow_symlinks=False)
ensure_soname_links(dest)
# Copy QT plugins.
plugins_dir = qt_dir / "plugins"
if plugins_dir.is_dir():
for plugin_file in plugins_dir.rglob("*"):
if not is_so_file(plugin_file):
continue
dest_plugin_file = dest / "plugins" / plugin_file.relative_to(plugins_dir)
dest_plugin_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(plugin_file, dest_plugin_file, follow_symlinks=False)
# Point plugins rpath to `dest`.
dest_plugins_dir = dest / "plugins"
if dest_plugins_dir.is_dir():
for plugin_so in dest_plugins_dir.rglob("*.so*"):
if plugin_so.is_file():
run("patchelf", "--set-rpath", "$ORIGIN/../..:$ORIGIN", str(plugin_so))
# Non-recursive, set rpath only for top-level libs.
for lib_so in dest.glob("*.so*"):
if lib_so.is_file():
run("patchelf", "--set-rpath", "$ORIGIN", str(lib_so))
qt_conf_path = dest / "qt.conf"
qt_conf_path.write_text("[Paths]\nPrefix = .\n")
def check_runtime_dependencies(package_dir: Path) -> None:
"""Check all binaries in `package_dir` and report if they're still missing dependencies or are static."""
def is_executable_or_so(path: Path) -> bool:
name = path.name
return os.access(path, os.X_OK) or name.endswith(".so") or ".so." in name
missing = False
env = os.environ.copy()
env.pop("LD_LIBRARY_PATH", None)
for binary_file in package_dir.rglob("*"):
if not binary_file.is_file() or not is_executable_or_so(binary_file):
continue
# Skip non-binaries.
try:
run("readelf", "-h", str(binary_file), stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
continue
try:
ldd_output = run("ldd", str(binary_file), env=env, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
print(f"ldd failed for {binary_file}")
print(e.output, end="")
missing = True
continue
if "not found" in ldd_output:
print(f"Missing runtime dependencies for {binary_file}")
for line in ldd_output.splitlines():
if "not found" in line:
print(line)
missing = True
# TODO: should error?
if missing:
print("Runtime dependency check found issues; continuing packaging.")
def package_python_wrapper(
py_dir: Path,
ifcopenshell_install_dir: Path,
github_sha: str,
output_dir: Path,
arch_suffix: str,
) -> None:
py_version = py_dir.name
postfix = "" if py_version[-1].isdigit() else py_version[-1]
# Match and convert `x.y` -> `xy`.
version_match = re.search(r"[0-9]+\.[0-9]+", py_version)
assert version_match
numbers = "".join(version_match.group().split("."))
py_version_major = f"python-{numbers}{postfix}"
ifcopenshell_dir = py_dir / "ifcopenshell"
staging_dir = py_dir.parent / "ifcopenshell_"
staging_dir.mkdir()
for item in list(py_dir.iterdir()):
shutil.move(str(item), str(staging_dir))
staging_dir.rename(ifcopenshell_dir)
# Cache from test run during build.
pycache_dir = ifcopenshell_dir / "__pycache__"
if pycache_dir.is_dir():
shutil.rmtree(pycache_dir)
for pyc_file in ifcopenshell_dir.rglob("*.pyc"):
pyc_file.unlink()
# TODO: packs qt libs also?
stage_runtime_payload(ifcopenshell_install_dir, ifcopenshell_dir)
zip_name = f"ifcopenshell-{py_version_major}-{VERSION}-{github_sha}-{arch_suffix}.zip"
run("zip", "-y", "-r", "-qq", zip_name, "ifcopenshell", cwd=py_dir)
shutil.move(str(py_dir / zip_name), str(output_dir / zip_name))
def is_packageable_executable(path: Path) -> bool:
if not path.is_file() or not os.access(path, os.X_OK):
return False
return not (path.name.lower().endswith(".zip") or is_shared_library(path))
def package_executable(
exe_path: Path,
ifcopenshell_install_dir: Path,
github_sha: str,
output_dir: Path,
autodesk_connector_dir: Path,
qt_dir: Path | None,
arch_suffix: str,
) -> None:
exe = exe_path.name
package_dir = ifcopenshell_install_dir / f".package-{exe}"
package_dir.mkdir(parents=True)
shutil.copy(exe_path, package_dir / exe)
# TODO: kept `is_platform(MAC)` to retain original bash script behaviour,
# but is this guard needed or it should be always False?
stage_runtime_payload(ifcopenshell_install_dir, package_dir, include_geometry_writers=is_platform("MAC"))
# On macOS, rpath is already set at build time via CMake's INSTALL_RPATH, and
# QT apps are packaged as .app bundles (`package_app_bundle`) instead.
if not is_platform("MAC"):
run("patchelf", "--set-rpath", "$ORIGIN", str(package_dir / exe))
stage_qt_runtime_payload(exe_path, package_dir, qt_dir)
if exe == "BonsaiViewer":
connectors_dir = package_dir / "connectors"
connectors_dir.mkdir()
shutil.copytree(autodesk_connector_dir, connectors_dir / autodesk_connector_dir.name, symlinks=True)
check_runtime_dependencies(package_dir)
zip_path = output_dir / f"{exe}-{VERSION}-{github_sha}-{arch_suffix}.zip"
run("zip", "-y", "-qq", "-r", str(zip_path), ".", cwd=package_dir)
shutil.rmtree(package_dir)
def package_app_bundle(
app_path: Path,
install_root: Path,
github_sha: str,
output_dir: Path,
autodesk_connector_dir: Path,
arch_suffix: str,
) -> None:
"""Zip a `.app` bundle (e.g. BonsaiViewer.app) living at the install-prefix root.
Their install rule uses `BUNDLE DESTINATION "."` - that's the layout Qt's
macdeployqt expects. macdeployqt has already embedded the Qt frameworks
inside each bundle during install/strip, so the only thing left to stage
is the connector.
"""
app = app_path.stem
if app == "BonsaiViewer":
# ConnectorDiscovery looks in applicationDirPath()/connectors,
# which for a bundle is Contents/MacOS.
connectors_dir = app_path / "Contents" / "MacOS" / "connectors"
connectors_dir.mkdir(parents=True)
shutil.copytree(autodesk_connector_dir, connectors_dir / autodesk_connector_dir.name, symlinks=True)
zip_path = output_dir / f"{app}-{VERSION}-{github_sha}-{arch_suffix}.zip"
run("zip", "-qq", "-r", str(zip_path), app_path.name, cwd=install_root)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("arch_suffix", help="Zip filename suffix, e.g. linux64 or linuxarm64.")
args = parser.parse_args()
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
# invokes `cargo build --release` and stages the binary +
# connector.json into dist/autodesk/. Same on-disk shape as the
# old PyInstaller flow so the symlink + zip steps below
# continue to work unchanged.
run("uv", "run", str(REPO_ROOT / "src/bonsaiviewer-autodesk/packaging/build.py"))
autodesk_connector_dir = REPO_ROOT / "src/bonsaiviewer-autodesk/dist/autodesk"
assert autodesk_connector_dir.is_dir()
# Locate the ifcopenshell install dir and stage QT6 alongside the zip output.
install_root = get_install_dir()
ifcopenshell_install_dir = install_root / "ifcopenshell"
output_dir = Path.home() / "output"
output_dir.mkdir(parents=True, exist_ok=True)
qt6_version = os.getenv("QT6_VERSION", "6.8.3")
qt_dir_env = os.getenv("QT_DIR")
qt_dir = Path(qt_dir_env) if qt_dir_env else find_qt_dir(install_root, qt6_version)
# Iterate over all built Python wrappers in `install/ifcopenshell/python-x.y.z`
# and zip them, bundling all dynamic libs from `lib`.
github_sha = os.environ["GITHUB_SHA"][:7]
for py_dir in sorted(ifcopenshell_install_dir.glob("python-*")):
package_python_wrapper(py_dir, ifcopenshell_install_dir, github_sha, output_dir, args.arch_suffix)
# Iterate over all executables in `install/ifcopenshell/bin` and zip them.
# Each zip bundles dynamic libs from `lib` and also qt libs.
bin_dir = ifcopenshell_install_dir / "bin"
for exe_path in sorted(bin_dir.iterdir()):
if is_packageable_executable(exe_path):
package_executable(
exe_path,
ifcopenshell_install_dir,
github_sha,
output_dir,
autodesk_connector_dir,
qt_dir,
args.arch_suffix,
)
if is_platform("MAC"):
for app_path in sorted(install_root.glob("*.app")):
package_app_bundle(app_path, install_root, github_sha, output_dir, autodesk_connector_dir, args.arch_suffix)
if __name__ == "__main__":
main()
+2 -5
View File
@@ -28,11 +28,8 @@ since it's pure cmake without any additional moving parts.
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
- it will produce Python package in `IfcOpenShell/ifcopenshell`
- run `python pyodide/build-all-pack-wheel-local.py`, it will
- clean up previous wheels
- run `pyodide build`
- prepare standalone and modular wheels
- produce final wheels in `IfcOpenShell/dist` and `IfcOpenshell/dist-modular`
- run `pyodide build`
- it will produce a wheel in `IfcOpenShell/dist`
- testing:
- ensure you're in pyodide environment
- `cd IfcOpenshell/pyodide`
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env python3
"""Intended to be run after nix/build-all.py has finished the wasm build."""
import shutil
import subprocess
from pathlib import Path
def get_repo_root() -> Path:
output = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True)
return Path(output.strip())
def run(cmd: list[str], **kwargs) -> None:
print("$", " ".join(cmd))
subprocess.check_call(cmd, **kwargs)
def main() -> None:
repo_root = get_repo_root()
shutil.rmtree(repo_root / "dist", ignore_errors=True)
shutil.rmtree(repo_root / "dist_modular", ignore_errors=True)
run(["pyodide", "build"], cwd=repo_root)
shutil.rmtree(repo_root / "ifcopenshell", ignore_errors=True)
(repo_root / "setup.py").unlink(missing_ok=True)
run(["git", "restore", "pyproject.toml"], cwd=repo_root)
wheel = next((repo_root / "dist").glob("ifcopenshell-*.whl"))
run(["uv", "run", "pyodide/order_pyodide_wheel_shared_objects.py", str(wheel)], cwd=repo_root)
run(
["uv", "run", "pyodide/split_pyodide_ifcopenshell_wheel.py", str(wheel), "dist-modular/"],
cwd=repo_root,
)
if __name__ == "__main__":
main()
+7 -9
View File
@@ -1,8 +1,10 @@
#!/usr/bin/bash
set -ex
PYODIDE_VERSION=0.29.4
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PYODIDE_VERSION=0.29.3
PYODIDE_BUILD_VERSION=0.33.0
PYODIDE_XBUILDENV_ROOT="${HOME}/.cache/.pyodide-xbuildenv-${PYODIDE_BUILD_VERSION}"
PYODIDE_XBUILDENV="${PYODIDE_XBUILDENV_ROOT}/${PYODIDE_VERSION}"
# Script is assuming that it will be possible to execute it multiple times
# therefore we're clearing venv each time and ignoring existing 'emsdk' folder.
@@ -14,14 +16,12 @@ source .venv/bin/activate
# Install pyodide cross build environment.
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
uv pip install -r "${SCRIPT_DIR}/requirements.txt"
uv pip install "pyodide-build==${PYODIDE_BUILD_VERSION}"
# `uv run` is required, so xbuildenv would skip using `pip`.
uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
uv run pyodide xbuildenv install-emscripten
# Cache path includes a hash segment that varies by pyodide-build version,
# so query it instead of constructing it manually.
EMSDK_ROOT=$(uv run pyodide config get emsdk_dir)
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
[ -f "${EMSDK_ROOT}/emsdk_env.sh" ] && source "${EMSDK_ROOT}/emsdk_env.sh"
[ -f "${EMSDK_ROOT}/../../emsdk_env.sh" ] && source "${EMSDK_ROOT}/../../emsdk_env.sh"
which emcc
@@ -29,10 +29,8 @@ emcc --version
mkdir -p packages/ifcopenshell
VERSION=`cat IfcOpenShell/VERSION`
# Normalize to the canonical PEP 440 form (e.g. 0.9.0alpha0 -> 0.9.0a0).
VERSION=`python3 -c "from packaging.version import Version; print(Version('$VERSION'))"`
cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell
sed -i s/9.9.9/$VERSION/g packages/ifcopenshell/meta.yaml
sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
# Use custom build ifcopenshell directory in build-all to make caching simpler
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
+1 -2
View File
@@ -1,7 +1,6 @@
package:
name: ifcopenshell
# Placeholder, replaced by build_pyodide.sh with the actual version from VERSION file.
version: 9.9.9
version: 0.8.0
source:
# meta.yaml is placed as `packages/ifcopenshell/meta.yaml`.
@@ -34,10 +34,10 @@ SCHEMA_ORDER = {
}
MAIN_SHARED_OBJECT_RE = re.compile(r"^_ifcopenshell_wrapper(?:\.|$)")
SCHEMA_PLUGIN_RE = re.compile(r"^ifcopenshell_parse_schema_(.+)\.so$")
MAPPING_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_mapping_(.+)\.so$")
DOCUMENT_PLUGIN_RE = re.compile(r"^ifcopenshell_document_[a-z0-9]+(?:_(.+))?\.so$")
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_writer_(.+)\.so$")
SCHEMA_PLUGIN_RE = re.compile(r"^ifcopenshell\.parse\.schema\.([^.]+)\.so$")
MAPPING_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.mapping\.([^.]+)\.so$")
DOCUMENT_PLUGIN_RE = re.compile(r"^ifcopenshell\.document\.[^.]+\.([^.]+)\.so$")
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.serialization\.([^.]+)\.so$")
def schema_key(schema: str) -> tuple[int, str]:
-1
View File
@@ -1 +0,0 @@
pyodide-build==0.39.0
+15 -36
View File
@@ -6,6 +6,11 @@ version = "0.0.0"
[tool.black]
line-length = 120
include = '''
src/.*.pyi?$
|nix/.*.pyi?$
|pyodide/.*.pyi?$
'''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/rules/*
|src/ifcopenshell-python/ifcopenshell/express/express_parser.py
@@ -14,15 +19,6 @@ extend-exclude = '''
|src/ifc2ca/templates/*
|src/svgfill
|src/exterior-shell-extractor
|choco/bonsai/tools/enable_blenderbim_addon.py
|choco/bonsai/tools/disable_blenderbim_addon.py
|docs/conf.py
|docs/generate_docs.py
|aws/lambda/example_handler/__init__.py
|conda/update_version_init.py
|test/bpy.py
|test/tests.py
|test/run.py
'''
[tool.pyright]
@@ -67,30 +63,20 @@ select = [
#
"FA", # future annotations
"UP", # pyupgrade
"unnecessary-iterable-allocation-for-first-element",
"unsorted-dunder-all",
"RUF015", # next() > list_comprehension[0]
"RUF022", # sort __all__
"I", # import sorting
"unused-noqa",
"rule-codes-in-selectors",
"noqa-comments",
"rule-codes-in-suppression-comments",
# General util rules.
"invalid-rule-code",
"redirected-noqa",
"invalid-pyproject-toml",
"invalid-suppression-comment",
]
ignore = [
# Conflicts with Blender using annotations for props definitions.
"future-rewritable-type-annotation",
"FA100", # Conflicts with Blender using annotations for props definitions.
# Maybe will enable later:
"non-pep604-annotation-union", # Union[X,Y] to X | Y
"non-pep604-annotation-optional", # Optional to X | None
"redundant-open-modes", # Unnecessary mode argument
"yield-in-for-loop", # yield for -> yield from
"format-literals", # implicit references for positional format fields
"printf-string-formatting", # Replace % with .format
"f-string", # Replace .format with f-string
"UP007", # Union[X,Y] to X | Y
"UP045", # Optional to X | None
"UP015", # Unnecessary mode argument
"UP028", # yield for -> yield from
"UP030", # implicit references for positional format fields
"UP031", # Replace % with .format
"UP032", # Replace .format with f-string
]
[tool.ty.rules]
@@ -113,13 +99,10 @@ unresolved-attribute = "ignore"
invalid-argument-type = "ignore"
invalid-method-override = "ignore"
invalid-assignment = "ignore"
unsound-assignment = "ignore"
invalid-parameter-default = "ignore"
missing-override-decorator = "ignore"
invalid-yield = "ignore"
unsound-yield = "ignore"
invalid-return-type = "ignore"
unsound-return-statement = "ignore"
non-callable-init-subclass = "ignore"
not-iterable = "ignore"
possibly-missing-attribute = "ignore"
@@ -127,8 +110,6 @@ no-matching-overload = "ignore"
not-subscriptable = "ignore"
unsupported-dynamic-base = "ignore"
unsupported-operator = "ignore"
# `@persistent` is incorrectly annotated as `Any` in fake-bpy, needs to be resolved upstream.
dynamic-function-decorator-return = "ignore"
[tool.ty.environment]
extra-paths = [
@@ -190,8 +171,6 @@ dev-setup.help = "Install repo packages in editable mode"
ruff = "ruff check"
check-whitespace = "uv run .github/scripts/check-whitespace.py"
black = "black ."
ty.sequence = ["ty-bonsai", "ty-ios"]
+3 -3
View File
@@ -1,5 +1,5 @@
black==26.5.1
ruff==0.16.4
black==26.3.1
ruff==0.16.0
poethepoet
ty==0.0.74
ty==0.0.63
gersemi==0.28.0
+3 -3
View File
@@ -10,7 +10,7 @@ name = "bcf-client"
# author = "IfcOpenShell"
description = "BCF-XML file handler."
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.8"
keywords = ["IFC", "BCF", "BIM"]
dependencies = [
"xsdata>=24.4",
@@ -65,6 +65,6 @@ commands = pytest --cov --cov-report=term tests
[tool.ruff]
extend = "../../pyproject.toml"
lint.extend-select = [
"unused-import", # unused imports
lint.select = [
"F401", # unused imports
]
+34 -17
View File
@@ -42,12 +42,10 @@ endif
IS_STABLE:=FALSE
VERSION:=$(shell cat ../../VERSION)
VERSION_BASE:=$(shell sed -E 's/[[:alpha:]]+[0-9]+$$//' ../../VERSION)
VERSION_PYTHON:=$(shell sed 's/alpha/a/' ../../VERSION)
VERSION_SEMVER:=$(shell sed -E 's/([[:alpha:]]+)([0-9]+)$$/-\\1\\2/' ../../VERSION)
VERSION_MAJOR:=$(shell cat '../../VERSION' | cut -d '.' -f 1)
VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
VERSION_DAILY:=$(VERSION_BASE)a$(VERSION_DATE)
VERSION_SEMVER_DAILY:=$(VERSION_BASE)-alpha$(VERSION_DATE)
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
@@ -69,17 +67,32 @@ endif # def PYVERSION
IFCMERGE_VERSION:=2026-04-07
ifdef PLATFORM
SUPPORTED_PLATFORMS := linux macosm1 win
SUPPORTED_PLATFORMS := linux macos macosm1 win
ifeq ($(filter $(PLATFORM),$(SUPPORTED_PLATFORMS)),)
$(error Unsupported PLATFORM=$(PLATFORM). Must be one of $(SUPPORTED_PLATFORMS))
endif
ifeq ($(PLATFORM),macos)
ifeq ($(PYVERSION),py313)
$(error Blender 5.1 with Python 3.13 doesn't support intel macOS.)
endif
endif
ifeq ($(PLATFORM), linux)
PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
BLENDER_PLATFORM:=linux-x64
endif
ifeq ($(PLATFORM), macos)
ifeq ($(PYVERSION), py311)
PYPI_PLATFORM:=--platform macosx_10_10_x86_64
else
PYPI_PLATFORM:=--platform macosx_10_13_x86_64
endif
BLENDER_PLATFORM:=macos-x64
endif
ifeq ($(PLATFORM), macosm1)
PYPI_PLATFORM:=--platform macosx_11_0_arm64
BLENDER_PLATFORM:=macos-arm64
@@ -93,7 +106,7 @@ endif
endif # def PLATFORM
# Current build commit hash.
OLD:=ad113e1
OLD:=3e7b739
.PHONY: bump
bump:
ifndef NEW
@@ -179,8 +192,10 @@ endif
# Provides networkx graph analysis for project dependency calculations
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download networkx --dest=./wheels
# Required by IFCDiff
# Pinned <9.1: deepdiff 9.1.0 adds the compiled dependency cachebox<6,>=5.2,
# which this platformless download cannot provide for every target platform.
# Pinned <9.1: deepdiff 9.1.0 adds cachebox<6,>=5.2 which only ships macOS x86_64
# wheels for macosx_10_12+ and is incompatible with our macos py311 --platform
# macosx_10_10_x86_64 target. Revisit once the macos py311 platform tag is bumped
# to 10_13 (matching py312/py313).
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download "deepdiff<9.1" --dest=./wheels
# Required by IFCCSV and ifcopenshell.util.selector
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lark --dest=./wheels
@@ -198,6 +213,8 @@ endif
# pyradiance is using different platform versions than defaults in our makefile.
ifeq ($(PLATFORM), linux)
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform manylinux_2_28_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
else ifeq ($(PLATFORM), macos)
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform macosx_10_13_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
else
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
endif
@@ -243,14 +260,14 @@ endif
cp pyproject.toml build/
ifeq ($(IS_STABLE), TRUE)
$(SED) "s/0.0.0/$(VERSION_SEMVER)/" build/bonsai/blender_manifest.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
$(SED) "s/0.0.0/$(VERSION)/" build/bonsai/blender_manifest.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
else
$(SED) "s/0.0.0/$(VERSION_SEMVER_DAILY)/" build/bonsai/blender_manifest.toml
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif
# Blender 5.1+ requires Python 3.13.
@@ -262,9 +279,9 @@ endif
# Provides bonsai Add-on functionality
ifeq ($(IS_STABLE), TRUE)
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
else
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
endif
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build
cp build/dist/*.whl build/wheels/
@@ -298,9 +315,9 @@ endif
rm -rf build/bonsai/libs/
ifeq ($(IS_STABLE), TRUE)
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER)-$(BLENDER_PLATFORM).zip ./bonsai
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-$(BLENDER_PLATFORM).zip ./bonsai
else
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER_DAILY)-$(BLENDER_PLATFORM).zip ./bonsai
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-alpha$(VERSION_DATE)-$(BLENDER_PLATFORM).zip ./bonsai
endif
mv build/bonsai*.zip dist/
+8 -3
View File
@@ -185,10 +185,13 @@ class IfcStore:
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
IfcStore.cache_path = cache_path
cache_path = Path(IfcStore.cache_path)
settings = ifcopenshell.geom.settings()
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
cache_preexists = cache_path.exists()
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
if cache_preexists:
print(f"Successfully loaded existing cache: {cache_path.name}.")
else:
@@ -203,7 +206,9 @@ class IfcStore:
os.remove(IfcStore.cache_path)
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
print("New cache was created.")
except Exception as e:
print(f"Failed to create a cache: {str(e)}.")
+5 -5
View File
@@ -740,7 +740,7 @@ class IfcImporter:
self.update_progress((percent_average / 100 * progress_range) + start_progress)
shape = iterator.get()
if shape:
assert isinstance(shape, W.triangulation_element)
assert isinstance(shape, W.TriangulationElement)
product = self.file.by_id(shape.id)
self.create_product(product, shape)
results.add(product)
@@ -1079,9 +1079,9 @@ class IfcImporter:
def create_curve(
self,
element: ifcopenshell.entity_instance,
shape: Union[W.triangulation, W.triangulation_element],
shape: Union[W.Triangulation, W.TriangulationElement],
) -> bpy.types.Curve:
if isinstance(shape, W.triangulation_element):
if isinstance(shape, W.TriangulationElement):
geometry = shape.geometry
else:
geometry = shape
@@ -1112,11 +1112,11 @@ class IfcImporter:
def create_mesh(
self,
element: ifcopenshell.entity_instance,
shape: Union[W.triangulation, W.triangulation_element],
shape: Union[W.Triangulation, W.TriangulationElement],
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
) -> Union[bpy.types.Mesh, None]:
try:
if isinstance(shape, W.triangulation_element):
if isinstance(shape, W.TriangulationElement):
# shape is ShapeElementType
geometry = shape.geometry
else:
@@ -678,7 +678,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
# Identify all potential building elements
# TODO: don't select everything, use AABB culling in Blender
building_elements = list(
building_elements = (
tool.Ifc.get().by_type("IfcWall")
+ tool.Ifc.get().by_type("IfcSlab")
+ tool.Ifc.get().by_type("IfcVirtualElement")
@@ -708,7 +708,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
while True:
tree.add_element(iterator.get_native())
shape = iterator.get()
assert isinstance(shape, W.triangulation_element)
assert isinstance(shape, W.TriangulationElement)
shapes[shape.id] = {
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
@@ -348,7 +348,10 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
if self.obj_type == "Object":
objects = [o.name for o in tool.Blender.get_selected_objects()]
if context.selected_objects:
objects = [o.name for o in context.selected_objects]
else:
objects = [context.active_object.name]
else:
objects = [self.obj]
props = tool.Classification.get_classification_props()
@@ -516,7 +516,7 @@ def _world_segment_to_screen_pixels(
# ---------------------------------------------------------------------------
class BIM_GT_box_face_quad(bpy.types.Gizmo):
class BIM_GT_box_face_quad(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
"""Near-invisible face-quad click target with drag-to-resize modal.
Geometry: a unit quad in the local XY plane at z=0. The adapter
@@ -620,7 +620,7 @@ class BIM_GT_box_face_quad(bpy.types.Gizmo):
return {"RUNNING_MODAL"}
class BIM_GT_box_face_outline(bpy.types.Gizmo):
class BIM_GT_box_face_outline(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
"""Thin non-interactive colored edge outline for one face.
Drawn as 4 line segments in the face plane. The layout helper
@@ -160,7 +160,7 @@ def _make_face_set_cb(gz: Any, group: Any, axis: int, is_max: bool):
return setter
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup):
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup): # noqa: N801 — Blender bl_idname convention
"""Face-quad resize handles on the active clip box.
Renders six near-invisible click-target quads and six colored edge
@@ -987,7 +987,7 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
@classmethod
def poll(cls, context):
try:
import typst # ruff: ignore[unused-import]
import typst # noqa: F401
return True
except ModuleNotFoundError:
@@ -313,7 +313,7 @@ class CreateAllShapes(bpy.types.Operator):
failures.append(element)
print("***** FAILURE *****")
if shape:
assert isinstance(shape, W.triangulation_element)
assert isinstance(shape, W.TriangulationElement)
geom = shape.geometry
print(
f"Success {time.time() - start:.3f}s "
@@ -28,7 +28,7 @@ operators via ``target_set_operator``; drag handles inherit modal state
from ``GizmoMovable``.
"""
__all__ = [ # ruff: ignore[unsorted-dunder-all]
__all__ = [ # noqa: RUF022 (unsorted `__all__`)
"GizmoColor",
"GizmoAxis",
"TextAlignment",
@@ -5660,7 +5660,7 @@ class BaseParametricGizmoGroup:
"""
return 0.0
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None:
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None: # noqa: ARG002
"""Update overall_width, overall_height, and lining_offset based on view direction.
This base implementation handles the common pattern for door/window gizmos.
@@ -5837,7 +5837,7 @@ class BaseParametricGizmoGroup:
self.update_dimension_gizmos(mw, props)
self._refresh_element_specific(context, mw, props)
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None: # noqa: ARG002
"""Override for element-specific refresh logic.
Called from both refresh() (on state change) and draw_prepare() (per frame),
@@ -6344,7 +6344,7 @@ class BaseParametricGizmoGroup:
"""
return (0.0, 0.0)
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float:
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float: # noqa: ARG002
"""Get Y offset for icons based on view direction.
Uses get_icon_y_extent() to determine how far to offset icons based on
@@ -6546,7 +6546,9 @@ class BaseParametricGizmoGroup:
self._refresh_element_specific(context, mw, props)
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
def _update_dimension_gizmo_positions(
self, context: bpy.types.Context, mw: "Matrix", props # noqa: ARG002
) -> None:
"""Update dimension gizmo positions based on view direction.
Override this method in subclasses to implement view-dependent
@@ -1406,28 +1406,31 @@ class CreateDrawing(bpy.types.Operator):
# Backwards compatibility with older ifcopenshell builds that don't expose these keys.
pass
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.svg_settings.set("svg-without-storeys", True)
self.svg_settings.set("svg-write-poly", True)
self.svg_settings.set("svg-poly", True)
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
self.serialiser_settings.set("svg-without-storeys", True)
self.serialiser_settings.set("svg-write-poly", True)
self.serialiser_settings.set("svg-poly", True)
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
self.svg_settings.set("profile-threshold", 10000)
self.svg_settings.set("svg-xmlns", True)
self.svg_settings.set("svg-project", True)
self.svg_settings.set("auto-elevation", False)
self.svg_settings.set("auto-section", False)
self.svg_settings.set("print-space-names", False)
self.svg_settings.set("print-space-areas", False)
self.svg_settings.set("door-arcs", False)
self.svg_settings.set("svg-no-css", True)
self.svg_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.svg_settings.set("scale", str(self.scale))
self.svg_settings.set("svg-subtract-before", "always")
self.svg_settings.set("svg-prefilter", True) # See #3359
self.svg_settings.set("svg-unify-inputs", True)
self.svg_settings.set("svg-segment-projection", True)
self.serialiser_settings.set("profile-threshold", 10000)
self.serialiser_settings.set("svg-xmlns", True)
self.serialiser_settings.set("svg-project", True)
self.serialiser_settings.set("auto-elevation", False)
self.serialiser_settings.set("auto-section", False)
self.serialiser_settings.set("print-space-names", False)
self.serialiser_settings.set("print-space-areas", False)
self.serialiser_settings.set("door-arcs", False)
self.serialiser_settings.set("svg-no-css", True)
self.serialiser_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.serialiser_settings.set("scale", str(self.scale))
self.serialiser_settings.set("svg-subtract-before", "always")
self.serialiser_settings.set("svg-prefilter", True) # See #3359
self.serialiser_settings.set("svg-unify-inputs", True)
self.serialiser_settings.set("svg-segment-projection", True)
if target_view == "REFLECTED_PLAN_VIEW":
self.svg_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
self.serialiser_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(
self.svg_buffer, self.svg_settings, self.serialiser_settings
)
# tree = ifcopenshell.geom.tree()
# This instructs the tree to explode BReps into faces and return
# the style of the face when running tree.select_ray()
+12 -12
View File
@@ -50,13 +50,11 @@ from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
from bonsai.bim.prop import Attribute, BIMFilterGroup
diagram_scales_enum = []
diagram_scales_enum_system = None
def purge():
global diagram_scales_enum, diagram_scales_enum_system
global diagram_scales_enum
diagram_scales_enum = []
diagram_scales_enum_system = None
def update_target_view_doc(self: "DocProperties", context: bpy.types.Context) -> None:
@@ -125,12 +123,14 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
global diagram_scales_enum, diagram_scales_enum_system
global diagram_scales_enum
assert context.scene
system = context.scene.unit_settings.system
if len(diagram_scales_enum) < 1 or diagram_scales_enum_system != system:
diagram_scales_enum_system = system
if system == "IMPERIAL":
if (
len(diagram_scales_enum) < 1
or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
or (context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31)
):
if context.scene.unit_settings.system == "IMPERIAL":
diagram_scales_enum = [
("CUSTOM", "Custom", ""),
("1'=1'-0\"|1/1", "1'=1'-0\"", ""),
@@ -144,21 +144,21 @@ def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context)
('1/4"=1\'-0"|1/48', '1/4"=1\'-0"', ""),
('3/16"=1\'-0"|1/64', '3/16"=1\'-0"', ""),
('1/8"=1\'-0"|1/96', '1/8"=1\'-0"', ""),
("1\"=10'|1/120", "1\"=10'", ""),
('3/32"=1\'-0"|1/128', '3/32"=1\'-0"', ""),
('1/16"=1\'-0"|1/192', '1/16"=1\'-0"', ""),
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
("1\"=10'|1/120", "1\"=10'", ""),
("1\"=20'|1/240", "1\"=20'", ""),
("1\"=30'|1/360", "1\"=30'", ""),
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
("1\"=40'|1/480", "1\"=40'", ""),
("1\"=50'|1/600", "1\"=50'", ""),
("1\"=60'|1/720", "1\"=60'", ""),
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
("1\"=70'|1/840", "1\"=70'", ""),
("1\"=80'|1/960", "1\"=80'", ""),
("1\"=90'|1/1080", "1\"=90'", ""),
("1\"=100'|1/1200", "1\"=100'", ""),
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
("1\"=150'|1/1800", "1\"=150'", ""),
("1\"=200'|1/2400", "1\"=200'", ""),
("1\"=300'|1/3600", "1\"=300'", ""),
@@ -72,10 +72,11 @@ class ExportOBJ(bpy.types.Operator):
# Conversion from IFC to OBJ
# Settings for obj
settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS)
settings.set("apply-default-materials", True)
settings.set("use-element-guids", True)
serializer_settings.set("use-element-guids", True)
settings.set("use-world-coords", True)
ifc_file: ifcopenshell.file
@@ -89,7 +90,7 @@ class ExportOBJ(bpy.types.Operator):
obj_file_path = os.path.join(output_dir, "model.obj")
mtl_file_path = os.path.join(output_dir, "model.mtl")
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings)
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings)
serialiser.setFile(ifc_file)
serialiser.setUnitNameAndMagnitude("METER", 1.0)
serialiser.writeHeader()
@@ -106,7 +107,7 @@ class ExportOBJ(bpy.types.Operator):
if iterator.initialize():
while True:
shape = iterator.get()
assert isinstance(shape, W.triangulation_element)
assert isinstance(shape, W.TriangulationElement)
materials = shape.geometry.materials
for material in materials:
+1 -1
View File
@@ -430,7 +430,7 @@ class SverchokData:
@classmethod
def has_sverchok(cls) -> bool:
try:
import sverchok # ruff: ignore[unused-import]
import sverchok # noqa: F401
return True
except ModuleNotFoundError:
+6 -4
View File
@@ -560,7 +560,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
)
update_door_modifier_representation(obj)
def _execute(self, context: bpy.types.Context) -> set[str]:
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
for obj in tool.Blender.get_selected_objects():
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
continue
@@ -638,7 +638,7 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
def _execute(self, context: bpy.types.Context) -> set[str]:
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
for obj in tool.Blender.get_selected_objects():
self.remove_door_on_object(obj)
return {"FINISHED"}
@@ -683,7 +683,7 @@ class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
return True
return False
def _execute(self, context: bpy.types.Context) -> set[str]:
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
obj = tool.Blender.get_active_object()
if not obj:
return {"CANCELLED"}
@@ -909,7 +909,9 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
setattr(self, f"gizmo_swing_arc_{cfg.name}", main)
setattr(self, f"gizmo_swing_arc_{cfg.name}_flip", flip)
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties") -> None:
def _refresh_element_specific(
self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties" # noqa: ARG002
) -> None:
"""Update door-specific swing arc gizmos."""
self.update_swing_gizmos(mw, props)
+2 -2
View File
@@ -765,7 +765,7 @@ class GizmoRoofEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
def is_element_type(cls, element: ifcopenshell.entity_instance) -> bool:
return tool.Parametric.is_roof(element)
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw, props) -> None:
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw, props) -> None: # noqa: ARG002
"""Anchor every dimension gizmo at the object origin. Each gizmo's
declared axis (height/slope along +Z, thickness along -Z) separates
them in 3D so they don't visually collide despite sharing a
@@ -776,7 +776,7 @@ class GizmoRoofEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
self.set_dimension_gizmo_position("angle", mw, origin, (0, 0, 1))
self.set_dimension_gizmo_position("roof_thickness", mw, origin, (0, 0, -1))
def get_element_height(self, props) -> float:
def get_element_height(self, props) -> float: # noqa: ARG002
"""Object-local Z of the mesh's topmost vertex, so the pen / validate /
cancel / cycle row anchors visibly above sloped or stepped roof
bodies rather than at the parametric ``props.height`` which may not
+5 -3
View File
@@ -405,7 +405,7 @@ class SetStairTreads(bpy.types.Operator):
bl_label = "Set Number of Treads"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: # noqa: ARG002
obj = context.active_object
if not obj:
return {"CANCELLED"}
@@ -658,7 +658,9 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
self.tread_count_label_gizmo.alpha = 0.8
self.tread_count_label_gizmo.target_set_operator("bim.input_stair_treads")
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties") -> None:
def _refresh_element_specific(
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
) -> None:
"""Update stair-specific lock and tread count gizmos. Lock positioning is
handled per-frame in the dimension-positioning hook."""
self.update_lock_gizmo(props)
@@ -705,7 +707,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
self.update_gizmo_visibility(self.tread_count_label_gizmo, props.is_editing)
def _update_dimension_gizmo_positions(
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties"
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
) -> None:
"""Update dimension gizmo positions based on camera view direction."""
viewing_from_negative_y, viewing_from_negative_x = self._frame_view_dir
+2 -2
View File
@@ -2174,7 +2174,7 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
return (far, near)
def _update_dimension_gizmo_positions(
self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties"
self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties" # noqa: ARG002
) -> None:
"""Re-position length / height / height_end dimensions to the camera-facing
Y-side of the wall every frame. Mirrors the door & stair pattern: when the
@@ -2530,7 +2530,7 @@ def _perpendicular_wall_params(
return clamped_x, abs(cursor_local_y), side_sign
def _commit_pending_wall_edits_for_selection(context: bpy.types.Context) -> None:
def _commit_pending_wall_edits_for_selection(context: bpy.types.Context) -> None: # noqa: ARG001
"""Thin wall-scoped alias for ``tool.Parametric.commit_pending_edits_for_selection``.
Encapsulates the ``names=("wall",)`` filter so the registry name is
+1 -1
View File
@@ -538,7 +538,7 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Remove Window"
bl_options = {"REGISTER"}
def _execute(self, context: bpy.types.Context) -> set[str]:
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
obj = context.active_object
assert obj
element = tool.Ifc.get_entity(obj)
@@ -2442,7 +2442,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
if iterator.initialize():
while True: # Main loop.
shape = iterator.get()
assert isinstance(shape, W.triangulation_element)
assert isinstance(shape, W.TriangulationElement)
results.add(self.file.by_id(shape.id))
geometry = shape.geometry
@@ -2518,7 +2518,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
print("Finished", time.time() - start)
return {"FINISHED"}
def process_occurrence(self, shape: W.triangulation_element) -> None:
def process_occurrence(self, shape: W.TriangulationElement) -> None:
element = self.file.by_id(shape.id)
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
@@ -558,7 +558,7 @@ class IntegerInputDialogMixin:
return None
return props
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: # noqa: ARG002
props = self._resolve_props(context)
if props is None:
return {"CANCELLED"}
+2 -29
View File
@@ -142,35 +142,8 @@ def assign_material(
else:
element_material_type = material_type
# TODO: this whole dance is a stopgap and wants rewriting.
#
# material.assign_material creates material sets with no items in them,
# ignoring the material it was handed -- an IfcMaterialLayerSet with no
# MaterialLayers is not valid IFC, since the list is mandatory and
# [1:?]. So we repair it below, after the fact. Worse, the API rejects a
# plain IfcMaterial outright when asked for a usage, which is exactly
# what the Object Materials dropdown gives us, so we cannot even pass it
# on and have to let the API invent an empty set and then fill it in.
#
# The fix is for assign_material to build the set around the material it
# is given, rather than leaving an invalid one behind for its callers to
# patch up. That is a wider change than it looks: add_material_set has
# the same behaviour, and the create-empty-then-add-items idiom is
# spread through the API's own docstrings, examples and tests. Until
# that is untangled, keep the repair here where it is at least visible.
# Only a usage refuses a plain IfcMaterial; every other type still wants
# it, and IfcMaterial and IfcMaterialList cannot be created without it.
pass_material = material_tool.is_a_material_set(material) or not element_material_type.endswith("Usage")
ifc.run(
"material.assign_material",
products=[element],
type=element_material_type,
material=material if pass_material else None,
)
# A usage points at the set rather than being one, and it is the set
# that needs an item adding to it below.
assigned_material = material_tool.get_material(element, should_skip_usage=True)
ifc.run("material.assign_material", products=[element], type=element_material_type, material=material)
assigned_material = material_tool.get_material(element)
assert assigned_material # Type checker.
if material_tool.is_a_material_set(material):
+1 -1
View File
@@ -651,7 +651,7 @@ class Material:
def get_default_material(cls): pass
def get_elements_by_material(cls, material): pass
def get_material_attributes(cls): pass
def get_material(cls, element, should_inherit: bool = False, should_skip_usage: bool = False): pass
def get_material(cls, element, should_inherit: bool = False): pass
def get_object_ui_active_material(cls): pass
def get_object_ui_material_type(cls): pass
def get_style(cls, material): pass
+1 -1
View File
@@ -17,7 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# Ignore unused imports.
# ruff: file-ignore[unused-import]
# ruff: noqa: F401
from bonsai.tool.aggregate import Aggregate
from bonsai.tool.array import Array
-2
View File
@@ -25,7 +25,6 @@ import importlib
import math
import os
import platform
import re
import subprocess
import sys
import tempfile
@@ -1757,7 +1756,6 @@ class Blender(bonsai.core.tool.Blender):
repo_path = repo.working_tree_dir
assert repo_path
version_ = (Path(repo_path) / "VERSION").read_text().strip()
version_ = re.sub(r"[A-Za-z]+\d+$", "", version_)
commit_date = bonsai.get_last_commit_date()
assert commit_date
commit_date = datetime.fromisoformat(commit_date)
+2 -2
View File
@@ -1187,7 +1187,7 @@ class Geometry(bonsai.core.tool.Geometry):
if iterator and iterator.initialize():
while True:
shape = iterator.get()
assert isinstance(shape, W.triangulation_element)
assert isinstance(shape, W.TriangulationElement)
element = tool.Ifc.get().by_id(shape.id)
if obj := tool.Ifc.get_object(element):
# It's possible that there will be multiple shapes for the same context,
@@ -2179,7 +2179,7 @@ class Geometry(bonsai.core.tool.Geometry):
item = tool.Ifc.get().by_id(props.ifc_definition_id)
allowed_attributes = [
a.name()
for a in item.declaration.as_entity().all_attributes()
for a in item.declaration().as_entity.all_attributes()
if a.type_of_attribute()._is("IfcLengthMeasure")
]
+2 -2
View File
@@ -872,7 +872,7 @@ class Loader(bonsai.core.tool.Loader):
cls,
element: ifcopenshell.entity_instance,
representation: ifcopenshell.entity_instance,
shape: W.triangulation_element,
shape: W.TriangulationElement,
) -> bpy.types.Camera:
"""Create camera data.
@@ -1026,7 +1026,7 @@ class Loader(bonsai.core.tool.Loader):
@classmethod
def convert_geometry_to_mesh(
cls,
geometry: W.triangulation,
geometry: W.Triangulation,
mesh: bpy.types.Mesh,
verts: Optional[npt.NDArray[np.float64]] = None,
*,
+2 -7
View File
@@ -220,14 +220,9 @@ class Material(bonsai.core.tool.Material):
@classmethod
def get_material(
cls,
element: ifcopenshell.entity_instance,
should_inherit: bool = False,
should_skip_usage: bool = False,
cls, element: ifcopenshell.entity_instance, should_inherit: bool = False
) -> Union[ifcopenshell.entity_instance, None]:
return ifcopenshell.util.element.get_material(
element, should_inherit=should_inherit, should_skip_usage=should_skip_usage
)
return ifcopenshell.util.element.get_material(element, should_inherit=should_inherit)
@classmethod
def is_a_material_set(cls, material: ifcopenshell.entity_instance) -> bool:
+1 -1
View File
@@ -2459,7 +2459,7 @@ class Model(bonsai.core.tool.Model):
polygons = {}
for curve in curves:
geometry = ifcopenshell.geom.create_shape(settings, curve)
assert isinstance(geometry, W.triangulation)
assert isinstance(geometry, W.Triangulation)
v = ifcopenshell.util.shape.get_vertices(geometry, is_2d=True)
v = np.round(v, 4) # Round to nearest 0.1mm, otherwise things like circles don't polygonise reliably
edges = ifcopenshell.util.shape.get_edges(geometry)
+1 -1
View File
@@ -53,7 +53,7 @@ class Profile(bonsai.core.tool.Profile):
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
assert isinstance(shape, W.triangulation)
assert isinstance(shape, W.Triangulation)
verts = ifcopenshell.util.shape.get_vertices(shape)
if verts.size == 0:
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
@@ -76,10 +76,6 @@ Release
Notes:
- Typically all packages are released at once using the same version schema
- ``VERSION`` uses Python/PEP 440-compatible spelling. For example, an alpha
release may be ``0.9.0alpha0`` (canonicalized to ``0.9.0a0``); build scripts
derive numeric-only and SemVer forms such as ``0.9.0`` and
``0.9.0-alpha0`` where required.
- The ``README.md`` badges can serve as a visual reference for what versions have been released
- Corrective Release (if needed after a standard release):
+1 -1
View File
@@ -33,7 +33,7 @@ exclude = ["test*"]
[tool.ruff]
extend = "../../pyproject.toml"
lint.extend-select = [
"unused-import", # unused imports
"F401", # unused imports
]
[tool.ruff.lint.isort]
-1
View File
@@ -42,7 +42,6 @@ markers =
type
unit
void
wall
web
# Provide plugins explicitly, so it will be possible run tests with PYTEST_DISABLE_PLUGIN_AUTOLOAD.
-1
View File
@@ -1,4 +1,3 @@
pytest
pytest-blender
pytest-bdd
fake-bpy-module-latest
+4 -4
View File
@@ -45,10 +45,10 @@ for dep in dependencies:
subprocess.check_call(command + [dep])
try:
import pygments # ruff: ignore[unused-import]
import pytest # ruff: ignore[unused-import]
import pytest_bdd # ruff: ignore[unused-import]
import pytest_blender # ruff: ignore[unused-import]
import pygments # noqa: F401
import pytest # noqa: F401
import pytest_bdd # noqa: F401
import pytest_blender # noqa: F401
print("Test dependency installation was successful!")
except Exception as e:
+24 -21
View File
@@ -163,29 +163,32 @@ class Drawer:
# self.svg_settings.set_deflection_tolerance(0.0001)
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.svg_settings.set("svg-without-storeys", True)
self.svg_settings.set("svg-write-poly", True)
self.svg_settings.set("svg-poly", True)
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
self.serialiser_settings.set("svg-without-storeys", True)
self.serialiser_settings.set("svg-write-poly", True)
self.serialiser_settings.set("svg-poly", True)
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
self.svg_settings.set("profile-threshold", 10000)
self.svg_settings.set("svg-xmlns", True)
self.svg_settings.set("svg-project", True)
self.svg_settings.set("auto-elevation", False)
self.svg_settings.set("auto-section", False)
self.svg_settings.set("print-space-names", False)
self.svg_settings.set("print-space-areas", False)
self.svg_settings.set("door-arcs", False)
self.svg_settings.set("svg-no-css", True)
self.svg_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.svg_settings.set("scale", "1/50")
self.svg_settings.set("svg-subtract-before", "always")
self.svg_settings.set("svg-prefilter", True) # See #3359
# self.svg_settings.set("svg-prefilter", False) # See #3359
self.svg_settings.set("svg-unify-inputs", True)
self.svg_settings.set("svg-segment-projection", True)
self.serialiser_settings.set("profile-threshold", 10000)
self.serialiser_settings.set("svg-xmlns", True)
self.serialiser_settings.set("svg-project", True)
self.serialiser_settings.set("auto-elevation", False)
self.serialiser_settings.set("auto-section", False)
self.serialiser_settings.set("print-space-names", False)
self.serialiser_settings.set("print-space-areas", False)
self.serialiser_settings.set("door-arcs", False)
self.serialiser_settings.set("svg-no-css", True)
self.serialiser_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.serialiser_settings.set("scale", "1/50")
self.serialiser_settings.set("svg-subtract-before", "always")
self.serialiser_settings.set("svg-prefilter", True) # See #3359
# self.serialiser_settings.set("svg-prefilter", False) # See #3359
self.serialiser_settings.set("svg-unify-inputs", True)
self.serialiser_settings.set("svg-segment-projection", True)
if target_view == "REFLECTED_PLAN_VIEW":
self.svg_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
self.serialiser_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(
self.svg_buffer, self.svg_settings, self.serialiser_settings
)
self.serialiser.setFile(ifc)
@@ -72,12 +72,12 @@ Scenario: Add classification reference - object
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
When I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
When I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
Then nothing happens
Scenario: Change classification level
@@ -88,8 +88,8 @@ Scenario: Change classification level
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
@@ -104,8 +104,8 @@ Scenario: Disable editing classification references
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
When I press "bim.disable_editing_classification_references"
@@ -119,12 +119,12 @@ Scenario: Enable editing classification reference
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
When I press "bim.enable_editing_classification_reference(reference={reference})"
Then nothing happens
@@ -137,12 +137,12 @@ Scenario: Disable editing classification reference
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.enable_editing_classification_reference(reference={reference})"
When I press "bim.disable_editing_classification_reference"
@@ -156,15 +156,15 @@ Scenario: Remove classification reference - object
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.enable_editing_classification_reference(reference={reference})"
When I press "bim.remove_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
When I press "bim.remove_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
Then nothing happens
Scenario: Edit classification reference
@@ -175,12 +175,12 @@ Scenario: Edit classification reference
And I press "bim.add_classification"
And I add a cube
And the object "Cube" is selected
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.change_classification_level(parent_id={classification})"
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
And I press "bim.enable_editing_classification_reference(reference={reference})"
When I press "bim.edit_classification_reference"
@@ -185,7 +185,6 @@ Scenario: Update representation - updating a layered extrusion
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -214,7 +213,6 @@ Scenario: Update representation - updating a profiled extrusion
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -418,7 +416,6 @@ Scenario: Override duplicate move - copying a layered extrusion
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -450,7 +447,6 @@ Scenario: Override duplicate move - copying a profiled extrusion
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -121,7 +121,6 @@ Scenario: Assign material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
Then the object "IfcWallType/Empty" does not have the material "Default"
@@ -135,7 +134,6 @@ Scenario: Unassign material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
When I press "bim.unassign_material"
@@ -157,7 +155,6 @@ Scenario: Unassign material - removing inherited material
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
@@ -184,7 +181,6 @@ Scenario: Enable editing assigned material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
When I press "bim.enable_editing_assigned_material"
@@ -204,7 +200,6 @@ Scenario: Disable editing assigned material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -225,7 +220,6 @@ Scenario: Edit assigned material - material layer set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -241,7 +235,6 @@ Scenario: Assign material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
Then the object "IfcWallType/Empty" does not have the material "Default"
@@ -255,7 +248,6 @@ Scenario: Unassign material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
When I press "bim.unassign_material"
@@ -275,7 +267,6 @@ Scenario: Enable editing assigned material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
When I press "bim.enable_editing_assigned_material"
@@ -295,7 +286,6 @@ Scenario: Disable editing assigned material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -316,7 +306,6 @@ Scenario: Edit assigned material - material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -465,7 +454,6 @@ Scenario: Add material set layer
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -489,7 +477,6 @@ Scenario: Remove material set layer
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
+97 -139
View File
@@ -314,12 +314,6 @@ Scenario: Load project elements - auto offset of cartesian points
Then the object "IfcBuildingElementProxy/NAME" is at "0,0,0"
Scenario: Load project elements - all georeferencing coordinate situations - disabled false origin mode
# D, G and J have their geometry far from their placement, so each is
# shifted onto one of its own verts to keep its precision. Which vert that
# is comes from the geometry kernel and has changed before, so these assert
# that the origin is on a vert rather than which one, and name verts rather
# than origins. In automatic mode the model origin is picked the same way
# and everything moves with it, so there they are relative to it.
Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/geolocation.ifc', is_advanced=True)"
When I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED"
@@ -332,19 +326,13 @@ Scenario: Load project elements - all georeferencing coordinate situations - dis
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "13000,4000,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "15000,2000,1000"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "15000,6000,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "17000,4000,1000"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "11000,3000,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "13000,1000,1000"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations - automatic false origin mode
@@ -354,27 +342,24 @@ Scenario: Load project elements - all georeferencing coordinate situations - aut
When I set "scene.BIMProjectProperties.distance_limit" to "5"
And I press "bim.load_project_elements"
Then "scene.BIMGeoreferenceProperties.has_blender_offset" is "True"
And the model origin is on an object vertex
And "scene.BIMGeoreferenceProperties.model_origin" is "13000.0,4000.0,-1000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_x" is "13000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_y" is "4000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_z" is "-1000.0"
And the object "IfcSite/My Site" is at "0,0,0"
And the object "IfcBuilding/My Building" is at "0,0,0"
And the object "IfcBuildingStorey/My Storey" is at "0,0,0"
And the object "IfcActuator/A" is at "7,3,0" relative to the model origin at map coordinates "7000,3000,0"
And the object "IfcActuator/B" is at "6,1,0" relative to the model origin at map coordinates "6000,1000,0"
And the object "IfcActuator/A" is at "-6,-1,1"
And the object "IfcActuator/B" is at "-7,-3,1"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" relative to the model origin at map coordinates "13000,4000,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" relative to the model origin at map coordinates "15000,2000,1000"
And the object "IfcActuator/E" is at "6,3,0" relative to the model origin at map coordinates "6000,3000,0"
And the object "IfcActuator/F" is at "3,3,0" relative to the model origin at map coordinates "3000,3000,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" relative to the model origin at map coordinates "15000,6000,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" relative to the model origin at map coordinates "17000,4000,1000"
And the object "IfcActuator/H" is at "9,2,0" relative to the model origin at map coordinates "9000,2000,0"
And the object "IfcActuator/I" is at "3,3,0" relative to the model origin at map coordinates "3000,3000,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" relative to the model origin at map coordinates "11000,3000,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" relative to the model origin at map coordinates "13000,1000,1000"
And the object "IfcActuator/K" is at "10,0,0" relative to the model origin at map coordinates "10000,0,0"
And the object "IfcActuator/D" is at "0,0,0"
And the object "IfcActuator/E" is at "-7,-1,1"
And the object "IfcActuator/F" is at "-10,-1,1"
And the object "IfcActuator/G" is at "2,2,0"
And the object "IfcActuator/H" is at "-4,-2,1"
And the object "IfcActuator/I" is at "-10,-1,1"
And the object "IfcActuator/J" is at "-2,-1,0"
And the object "IfcActuator/K" is at "-3,-4,1"
Scenario: Load project elements - all georeferencing coordinate situations - manual false origin mode
Given an empty Blender session
@@ -394,20 +379,23 @@ Scenario: Load project elements - all georeferencing coordinate situations - man
And the object "IfcActuator/A" is at "-3,3,0"
And the object "IfcActuator/B" is at "-4,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "3,4,-1" at map coordinates "13000,4000,-1000"
And the object "IfcActuator/D" has a vert at "5,2,1" at map coordinates "15000,2000,1000"
And the object "IfcActuator/D" is at "3,4,-1"
And the object "IfcActuator/E" is at "-4,3,0"
And the object "IfcActuator/F" is at "-7,3,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "5,6,-1" at map coordinates "15000,6000,-1000"
And the object "IfcActuator/G" has a vert at "7,4,1" at map coordinates "17000,4000,1000"
And the object "IfcActuator/G" is at "5,6,-1"
And the object "IfcActuator/H" is at "-1,2,0"
And the object "IfcActuator/I" is at "-7,3,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "1,3,-1" at map coordinates "11000,3000,-1000"
And the object "IfcActuator/J" has a vert at "3,1,1" at map coordinates "13000,1000,1000"
And the object "IfcActuator/J" is at "1,3,-1"
And the object "IfcActuator/K" is at "0,0,0"
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
And the object "IfcActuator/D" has a vertex at "3,2,-1"
And the object "IfcActuator/D" has a vertex at "5,2,-1"
And the object "IfcActuator/G" has a vertex at "5,4,-1"
And the object "IfcActuator/G" has a vertex at "7,4,-1"
And the object "IfcActuator/J" has a vertex at "1,1,-1"
And the object "IfcActuator/J" has a vertex at "3,1,-1"
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - disabled false origin mode
Given an empty Blender session
@@ -422,19 +410,13 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "5.985,14.71,0"
And the object "IfcActuator/B" is at "5.5367,12.519,0"
And the object "IfcActuator/C" is at "0,10,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "11.5218,17.2284,-1" at map coordinates "11521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "13.9712,15.8141,1" at map coordinates "13971.246,15814.136,1000"
And the object "IfcActuator/D" is at "11.522,17.228,-1"
And the object "IfcActuator/E" is at "5.0191,14.451,0"
And the object "IfcActuator/F" is at "2.1213,13.674,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "12.936,19.6778,-1" at map coordinates "12935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "15.3855,18.2636,1" at map coordinates "15385.465,18263.627,1000"
And the object "IfcActuator/G" is at "12.936,19.678,-1"
And the object "IfcActuator/H" is at "8.1757,14.261,0"
And the object "IfcActuator/I" is at "2.1213,13.674,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "9.8487,15.7448,-1" at map coordinates "9848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "12.2982,14.3306,1" at map coordinates "12298.216,14330.573,1000"
And the object "IfcActuator/J" is at "9.8487,15.745,-1"
And the object "IfcActuator/K" is at "9.6593,12.588,0"
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - automatic false origin mode
@@ -454,19 +436,13 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "11521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "13971.246,15814.136,1000"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "12935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "15385.465,18263.627,1000"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "9848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "12298.216,14330.573,1000"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - manual false origin mode
@@ -487,20 +463,23 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "5.985,4.71,0"
And the object "IfcActuator/B" is at "5.5367,2.519,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "11.5218,7.2284,-1" at map coordinates "11521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "13.9712,5.8141,1" at map coordinates "13971.246,15814.136,1000"
And the object "IfcActuator/D" is at "11.522,7.228,-1"
And the object "IfcActuator/E" is at "5.0191,4.451,0"
And the object "IfcActuator/F" is at "2.1213,3.674,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "12.936,9.6778,-1" at map coordinates "12935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "15.3855,8.2636,1" at map coordinates "15385.465,18263.627,1000"
And the object "IfcActuator/G" is at "12.936,9.678,-1"
And the object "IfcActuator/H" is at "8.1757,4.261,0"
And the object "IfcActuator/I" is at "2.1213,3.674,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "9.8487,5.7448,-1" at map coordinates "9848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "12.2982,4.3306,1" at map coordinates "12298.216,14330.573,1000"
And the object "IfcActuator/J" is at "9.8487,5.745,-1"
And the object "IfcActuator/K" is at "9.6593,2.588,0"
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
And the object "IfcActuator/D" has a vertex at "12.039,5.296,-1"
And the object "IfcActuator/D" has a vertex at "13.971,5.814,-1"
And the object "IfcActuator/G" has a vertex at "13.454,7.746,-1"
And the object "IfcActuator/G" has a vertex at "15.385,8.264,-1"
And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1"
And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1"
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - manual false origin mode - with custom project north
Given an empty Blender session
@@ -521,19 +500,13 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "11521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "13971.246,15814.136,1000"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "12935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "15385.465,18263.627,1000"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "9848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "12298.216,14330.573,1000"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - disabled false origin mode (this should be identical to the situation with no map conversion)
@@ -549,19 +522,13 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "28000,4000,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "30000,2000,1000"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "30000,6000,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "32000,4000,1000"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "26000,3000,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "28000,1000,1000"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - automatic false origin mode (this should affect the Blender eastings and northings, which is now different to the Blender offset XYZ, but is otherwise identical to the non-map conversion variant)
@@ -571,27 +538,24 @@ Scenario: Load project elements - all georeferencing coordinate situations with
When I set "scene.BIMProjectProperties.distance_limit" to "5"
And I press "bim.load_project_elements"
Then "scene.BIMGeoreferenceProperties.has_blender_offset" is "True"
And the model origin is on an object vertex
And "scene.BIMGeoreferenceProperties.model_origin" is "28000.0,4000.0,-1000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_x" is "13000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_y" is "4000.0"
And "scene.BIMGeoreferenceProperties.blender_offset_z" is "-1000.0"
And the object "IfcSite/My Site" is at "0,0,0"
And the object "IfcBuilding/My Building" is at "0,0,0"
And the object "IfcBuildingStorey/My Storey" is at "0,0,0"
And the object "IfcActuator/A" is at "22,3,0" relative to the model origin at map coordinates "22000,3000,0"
And the object "IfcActuator/B" is at "21,1,0" relative to the model origin at map coordinates "21000,1000,0"
And the object "IfcActuator/A" is at "-6,-1,1"
And the object "IfcActuator/B" is at "-7,-3,1"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "28,4,-1" relative to the model origin at map coordinates "28000,4000,-1000"
And the object "IfcActuator/D" has a vert at "30,2,1" relative to the model origin at map coordinates "30000,2000,1000"
And the object "IfcActuator/E" is at "21,3,0" relative to the model origin at map coordinates "21000,3000,0"
And the object "IfcActuator/F" is at "18,3,0" relative to the model origin at map coordinates "18000,3000,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "30,6,-1" relative to the model origin at map coordinates "30000,6000,-1000"
And the object "IfcActuator/G" has a vert at "32,4,1" relative to the model origin at map coordinates "32000,4000,1000"
And the object "IfcActuator/H" is at "24,2,0" relative to the model origin at map coordinates "24000,2000,0"
And the object "IfcActuator/I" is at "18,3,0" relative to the model origin at map coordinates "18000,3000,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "26,3,-1" relative to the model origin at map coordinates "26000,3000,-1000"
And the object "IfcActuator/J" has a vert at "28,1,1" relative to the model origin at map coordinates "28000,1000,1000"
And the object "IfcActuator/K" is at "25,0,0" relative to the model origin at map coordinates "25000,0,0"
And the object "IfcActuator/D" is at "0,0,0"
And the object "IfcActuator/E" is at "-7,-1,1"
And the object "IfcActuator/F" is at "-10,-1,1"
And the object "IfcActuator/G" is at "2,2,0"
And the object "IfcActuator/H" is at "-4,-2,1"
And the object "IfcActuator/I" is at "-10,-1,1"
And the object "IfcActuator/J" is at "-2,-1,0"
And the object "IfcActuator/K" is at "-3,-4,1"
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - manual false origin mode (this should affect the Blender eastings and northings, which is now different to the Blender offset XYZ, but is otherwise identical to the non-map conversion variant)
Given an empty Blender session
@@ -611,20 +575,23 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "-3,3,0"
And the object "IfcActuator/B" is at "-4,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "3,4,-1" at map coordinates "28000,4000,-1000"
And the object "IfcActuator/D" has a vert at "5,2,1" at map coordinates "30000,2000,1000"
And the object "IfcActuator/D" is at "3,4,-1"
And the object "IfcActuator/E" is at "-4,3,0"
And the object "IfcActuator/F" is at "-7,3,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "5,6,-1" at map coordinates "30000,6000,-1000"
And the object "IfcActuator/G" has a vert at "7,4,1" at map coordinates "32000,4000,1000"
And the object "IfcActuator/G" is at "5,6,-1"
And the object "IfcActuator/H" is at "-1,2,0"
And the object "IfcActuator/I" is at "-7,3,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "1,3,-1" at map coordinates "26000,3000,-1000"
And the object "IfcActuator/J" has a vert at "3,1,1" at map coordinates "28000,1000,1000"
And the object "IfcActuator/J" is at "1,3,-1"
And the object "IfcActuator/K" is at "0,0,0"
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
And the object "IfcActuator/D" has a vertex at "3,2,-1"
And the object "IfcActuator/D" has a vertex at "5,2,-1"
And the object "IfcActuator/G" has a vertex at "5,4,-1"
And the object "IfcActuator/G" has a vertex at "7,4,-1"
And the object "IfcActuator/J" has a vertex at "1,1,-1"
And the object "IfcActuator/J" has a vertex at "3,1,-1"
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - disabled false origin mode
Given an empty Blender session
@@ -639,19 +606,13 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "5.985,14.71,0"
And the object "IfcActuator/B" is at "5.5367,12.519,0"
And the object "IfcActuator/C" is at "0,10,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "11.5218,17.2284,-1" at map coordinates "26521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "13.9712,15.8141,1" at map coordinates "28971.246,15814.136,1000"
And the object "IfcActuator/D" is at "11.522,17.228,-1"
And the object "IfcActuator/E" is at "5.0191,14.451,0"
And the object "IfcActuator/F" is at "2.1213,13.674,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "12.936,19.6778,-1" at map coordinates "27935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "15.3855,18.2636,1" at map coordinates "30385.465,18263.627,1000"
And the object "IfcActuator/G" is at "12.936,19.678,-1"
And the object "IfcActuator/H" is at "8.1757,14.261,0"
And the object "IfcActuator/I" is at "2.1213,13.674,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "9.8487,15.7448,-1" at map coordinates "24848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "12.2982,14.3306,1" at map coordinates "27298.216,14330.573,1000"
And the object "IfcActuator/J" is at "9.8487,15.745,-1"
And the object "IfcActuator/K" is at "9.6593,12.588,0"
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - automatic false origin mode
@@ -671,19 +632,13 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "7,3,0"
And the object "IfcActuator/B" is at "6,1,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "26521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "28971.246,15814.136,1000"
And the object "IfcActuator/D" is at "13,4,-1"
And the object "IfcActuator/E" is at "6,3,0"
And the object "IfcActuator/F" is at "3,3,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "27935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "30385.465,18263.627,1000"
And the object "IfcActuator/G" is at "15,6,-1"
And the object "IfcActuator/H" is at "9,2,0"
And the object "IfcActuator/I" is at "3,3,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "24848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "27298.216,14330.573,1000"
And the object "IfcActuator/J" is at "11,3,-1"
And the object "IfcActuator/K" is at "10,0,0"
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - manual false origin mode
@@ -704,20 +659,23 @@ Scenario: Load project elements - all georeferencing coordinate situations with
And the object "IfcActuator/A" is at "5.985,4.71,0"
And the object "IfcActuator/B" is at "5.5367,2.519,0"
And the object "IfcActuator/C" is at "0,0,0"
And the object "IfcActuator/D" has its origin on a vertex
And the object "IfcActuator/D" has a vert at "11.5218,7.2284,-1" at map coordinates "26521.758,17228.35,-1000"
And the object "IfcActuator/D" has a vert at "13.9712,5.8141,1" at map coordinates "28971.246,15814.136,1000"
And the object "IfcActuator/D" is at "11.522,7.228,-1"
And the object "IfcActuator/E" is at "5.0191,4.451,0"
And the object "IfcActuator/F" is at "2.1213,3.674,0"
And the object "IfcActuator/G" has its origin on a vertex
And the object "IfcActuator/G" has a vert at "12.936,9.6778,-1" at map coordinates "27935.975,19677.841,-1000"
And the object "IfcActuator/G" has a vert at "15.3855,8.2636,1" at map coordinates "30385.465,18263.627,1000"
And the object "IfcActuator/G" is at "12.936,9.678,-1"
And the object "IfcActuator/H" is at "8.1757,4.261,0"
And the object "IfcActuator/I" is at "2.1213,3.674,0"
And the object "IfcActuator/J" has its origin on a vertex
And the object "IfcActuator/J" has a vert at "9.8487,5.7448,-1" at map coordinates "24848.726,15744.786,-1000"
And the object "IfcActuator/J" has a vert at "12.2982,4.3306,1" at map coordinates "27298.216,14330.573,1000"
And the object "IfcActuator/J" is at "9.8487,5.745,-1"
And the object "IfcActuator/K" is at "9.6593,2.588,0"
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
And the object "IfcActuator/D" has a vertex at "12.039,5.296,-1"
And the object "IfcActuator/D" has a vertex at "13.971,5.814,-1"
And the object "IfcActuator/G" has a vertex at "13.454,7.746,-1"
And the object "IfcActuator/G" has a vertex at "15.385,8.264,-1"
And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1"
And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1"
Scenario: Link IFC - from an empty IFC project
Given an empty IFC project
-4
View File
@@ -81,7 +81,6 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()"
@@ -103,7 +102,6 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()"
@@ -127,7 +125,6 @@ Scenario: Assign type - assign to a different type with a LAYER2 material layer
And I press "bim.assign_class"
And the variable "type" is "{ifc}.by_type('IfcWallType')[-1].id()"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
And I press "bim.assign_material"
And I add an empty
@@ -183,7 +180,6 @@ Scenario: Assign type - assign to a type with a material profile set
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
And I press "bim.assign_class"
And I press "bim.add_material()"
And the object "IfcWallType/Empty" is selected
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
And I press "bim.assign_material"
And I press "bim.enable_editing_assigned_material"
@@ -45,14 +45,14 @@ def test_text_formatter_defaults_to_none():
def test_text_formatter_field_stores_callable():
formatter = lambda props, value: f"{value:.2f}m"
formatter = lambda props, value: f"{value:.2f}m" # noqa: E731
config = DimensionGizmoConfig(attr_name="length", axis=(1, 0, 0), text_formatter=formatter)
assert config.text_formatter is not None
assert callable(config.text_formatter)
def test_text_formatter_receives_props_and_value():
formatter = lambda props, value: f"{props.label}={value}"
formatter = lambda props, value: f"{props.label}={value}" # noqa: E731
config = DimensionGizmoConfig(attr_name="length", axis=(1, 0, 0), text_formatter=formatter)
props = SimpleNamespace(label="L")
assert config.text_formatter(props, 3.14) == "L=3.14"
@@ -104,7 +104,7 @@ class TestParametricGizmoPollsHideDuringTransformModal:
continue
try:
result = poll(bpy.context)
except Exception as exc:
except Exception as exc: # noqa: BLE001
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
continue
if result:
@@ -98,7 +98,7 @@ class TestWallGizmoGroupsHideDuringPreview:
continue
try:
result = poll(bpy.context)
except Exception as exc:
except Exception as exc: # noqa: BLE001
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
continue
if result:
@@ -119,7 +119,7 @@ class TestWallGizmoGroupsHideOnArrayChildSelection:
for name, cls in groups:
try:
result = cls.poll(bpy.context)
except Exception as exc:
except Exception as exc: # noqa: BLE001
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
continue
if result:
@@ -159,7 +159,7 @@ class TestWallOperatorsRejectArrayChildSelection:
for name, cls in ops:
try:
result = cls.poll(bpy.context)
except Exception as exc:
except Exception as exc: # noqa: BLE001
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
continue
if result:
-107
View File
@@ -36,7 +36,6 @@ import bpy
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.unit
import numpy as np
import pytest
from mathutils import Vector
@@ -1001,7 +1000,6 @@ def i_click_button_and_expect_error_error_msg(button, error_msg):
@given(parsers.parse('I evaluate expression "{expression}"'))
@when(parsers.parse('I evaluate expression "{expression}"'))
@then(parsers.parse('I evaluate expression "{expression}"'))
def i_evaluate_expression(expression):
expression = replace_variables(expression)
exec(expression)
@@ -1682,111 +1680,6 @@ def the_object_name_has_a_vertex_at_location(name, location):
assert is_pass, f"No verts found at {location}: {verts}"
def get_model_origin() -> Vector:
"""Where the model was shifted to, in Blender units.
Geometry far from the origin is moved next to it so it keeps its precision,
and the shift is recorded as the model origin. Which vert of which object it
lands on is not something to depend on, so anything measured from it stays
put even when that choice changes.
"""
props = bpy.context.scene.BIMGeoreferenceProperties
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(an_ifc_file_exists())
return Vector([float(co) for co in props.model_origin.split(",")]) * unit_scale
def get_world_verts(obj: bpy.types.Object) -> list[Vector]:
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh) and len(mesh.vertices), f"Object {obj.name} has no mesh"
return [obj.matrix_world @ v.co for v in mesh.vertices]
def assert_vert_at_map_coordinates(obj: bpy.types.Object, vert: Vector, coordinates: str) -> None:
# Same conversion as the georeferencing calculator, which works in project
# units rather than Blender ones.
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(an_ifc_file_exists())
enh = Vector(tool.Georeference.xyz2enh(tuple(co / unit_scale for co in vert)))
expected = Vector([float(co) for co in coordinates.split(",")])
assert (enh - expected).length < 0.05, f"Vert {vert} is at map coordinates {enh[:]} instead of {coordinates}"
@then(
parsers.parse(
'the object "{name}" is at "{location}" relative to the model origin at map coordinates "{coordinates}"'
)
)
def the_object_name_is_at_location_relative_to_the_model_origin_at_map_coordinates(name, location, coordinates):
"""For objects with no geometry to name a vert on.
The Blender location is only meaningful next to the origin everything was
shifted by, since the two move together, but the map coordinates hold still
either way.
"""
obj = the_object_name_exists(name)
obj_location = obj.location + get_model_origin()
assert (
obj_location - Vector([float(co) for co in location.split(",")])
).length < 0.05, f"Object is at {obj_location} relative to the model origin instead of {location}"
assert_vert_at_map_coordinates(obj, obj.matrix_world.translation, coordinates)
@then(parsers.parse('the object "{name}" has a vert at "{location}" at map coordinates "{coordinates}"'))
def the_object_name_has_a_vert_at_location_at_map_coordinates(name, location, coordinates):
"""Check where a vert sits in Blender and where it is in the world.
Both matter: the Blender location is what the user sees, and checking only
the map coordinates would pass just as happily if the georeferencing maths
or the offsets it reads were wrong, since the same maths produces both.
"""
obj = the_object_name_exists(name)
target = Vector([float(co) for co in location.split(",")])
verts = get_world_verts(obj)
vert = next((v for v in verts if (v - target).length < 0.001), None)
assert vert is not None, f"No vert found at {location}: {verts}"
assert_vert_at_map_coordinates(obj, vert, coordinates)
@then(
parsers.parse(
'the object "{name}" has a vert at "{location}" relative to the model origin at map coordinates "{coordinates}"'
)
)
def the_object_name_has_a_vert_at_location_relative_to_the_model_origin_at_map_coordinates(name, location, coordinates):
"""As above, for when the whole model has been shifted onto the origin.
Blender locations are then only meaningful relative to that origin, since
everything moves together with it.
"""
obj = the_object_name_exists(name)
target = Vector([float(co) for co in location.split(",")]) - get_model_origin()
verts = get_world_verts(obj)
vert = next((v for v in verts if (v - target).length < 0.001), None)
assert vert is not None, f"No vert found at {location} relative to the model origin: {verts}"
assert_vert_at_map_coordinates(obj, vert, coordinates)
@then(parsers.parse('the object "{name}" has its origin on a vertex'))
def the_object_name_has_its_origin_on_a_vertex(name):
"""Far away geometry is shifted onto one of its own verts, which keeps the
origin on the geometry and the local coordinates small enough to keep their
precision. Which vert that is does not matter."""
obj = the_object_name_exists(name)
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh) and len(mesh.vertices), f"Object {obj.name} has no mesh"
nearest = min(v.co.length for v in mesh.vertices)
assert nearest < 0.001, f"Object origin is {nearest} away from its nearest vert"
@then("the model origin is on an object vertex")
def the_model_origin_is_on_an_object_vertex():
for obj in bpy.data.objects:
if not isinstance(obj.data, bpy.types.Mesh):
continue
if any(v.length < 0.001 for v in get_world_verts(obj)):
return
assert False, "No object has a vert at the model origin"
@then(parsers.parse('the object "{name}" has no scale'))
def the_object_name_has_no_scale(name):
assert the_object_name_exists(name).scale == Vector(
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.ruff]
extend = "../pyproject.toml"
lint.ignore = [
"unused-import", # unused imports
"F401", # unused imports
]
+3 -3
View File
@@ -168,7 +168,7 @@ ifcopenshell_deploy_qt_runtime(BonsaiViewer)
# them explicitly. (In a static build these are absent from lib/
# and the glob just no-ops, so this rule is safe in both modes.)
#
# 2. Plug-ins (ifcopenshell_*.dylib, no `lib` prefix) dlopen-only
# 2. Plug-ins (ifcopenshell.*.dylib, no `lib` prefix) dlopen-only
# deps the plug-in loader resolves at runtime. macdeployqt has
# no way to know about these.
#
@@ -177,7 +177,7 @@ ifcopenshell_deploy_qt_runtime(BonsaiViewer)
# inside the bundle), so plug-ins and core libs both find each other
# on the first probe.
#
# The geometry-writer filter drops ifcopenshell_geometry_writer_*.dylib
# The geometry-writer filter drops ifcopenshell.geometry.writer.*.dylib
# (the per-schema OBJ / glTF / DAE / STP / IGS / SVG / TTL export
# converters heavy, viewer-irrelevant). Mirrors the Rocky workflow's
# filter in `stage_runtime_payload` (see 27249770e).
@@ -195,7 +195,7 @@ if(APPLE)
install(CODE [[
set(_fw "${CMAKE_INSTALL_PREFIX}/BonsaiViewer.app/Contents/Frameworks")
file(GLOB _ifc_dylibs "${CMAKE_INSTALL_PREFIX}/lib/*.dylib")
list(FILTER _ifc_dylibs EXCLUDE REGEX "ifcopenshell_geometry_writer_")
list(FILTER _ifc_dylibs EXCLUDE REGEX "ifcopenshell\\.geometry\\.writer\\.")
if(_ifc_dylibs)
message(STATUS "Staging IfcOpenShell dylibs (linked core + plug-ins) into BonsaiViewer.app/Contents/Frameworks")
file(COPY ${_ifc_dylibs} DESTINATION "${_fw}")
+1 -1
View File
@@ -70,7 +70,7 @@ std::optional<BasicElementInfo> ElementRegistry::findBasicElementInfo(uint32_t o
return it->second;
}
std::optional<express::base> ElementRegistry::findEntity(uint32_t object_id) const {
std::optional<express::Base> ElementRegistry::findEntity(uint32_t object_id) const {
if (!loader_) return std::nullopt;
auto info = findBasicElementInfo(object_id);
+1 -1
View File
@@ -54,7 +54,7 @@ public:
void removeModel(uint32_t session_model_id);
std::vector<BasicElementInfo> basicElementInfoForModel(uint32_t session_model_id) const;
std::optional<BasicElementInfo> findBasicElementInfo(uint32_t object_id) const;
std::optional<express::base> findEntity(uint32_t object_id) const;
std::optional<express::Base> findEntity(uint32_t object_id) const;
private:
void onSidecarElementsReady(uint32_t session_model_id,
+4 -56
View File
@@ -428,8 +428,7 @@ void MainWindow::setupPanels() {
spatial_panel_ = new modules::spatial_hierarchy::SpatialHierarchyPanel(this);
properties_panel_ = new modules::properties::PropertiesPanel(this);
models_view_ = new modules::models::ModelsPanelView(
models_panel_, session_state_, viewport_widget_->viewport(), this);
models_view_ = new modules::models::ModelsPanelView(models_panel_, session_state_, this);
spatial_view_ = new modules::spatial_hierarchy::SpatialHierarchyPanelView(spatial_panel_, session_state_, this);
properties_view_ = new modules::properties::PropertiesPanelView(properties_panel_, session_state_, this);
@@ -482,13 +481,6 @@ void MainWindow::setupStatus() {
status_mode_label_ = new QLabel("Ready", this);
status_selection_label_ = new QLabel("No selection", this);
status_perf_label_ = new QLabel(this);
status_memory_label_ = new QLabel(this);
status_memory_label_->setVisible(false);
status_memory_label_->setToolTip(
"The geometry in view needs more GPU memory than is available, so the "
"viewer keeps the largest on-screen parts resident and streams the rest "
"as you move. Right-click a model in the Models panel and choose "
"\"Unload Model\" to free its GPU memory for the others.");
status_progress_bar_ = new QProgressBar(this);
status_perf_label_->setVisible(AppSettings::instance().showStats());
status_progress_bar_->setMaximumWidth(200);
@@ -497,7 +489,6 @@ void MainWindow::setupStatus() {
statusBar()->setSizeGripEnabled(false);
statusBar()->addWidget(status_mode_label_);
statusBar()->addWidget(status_selection_label_, 1);
statusBar()->addPermanentWidget(status_memory_label_);
statusBar()->addPermanentWidget(status_perf_label_);
statusBar()->addPermanentWidget(status_progress_bar_);
@@ -563,59 +554,16 @@ void MainWindow::setupLoader() {
connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this,
[this](const ViewportWindow::FrameStats& stats) {
const double mb = 1.0 / (1024.0 * 1024.0);
// Missing chunks are normal for a moment after every camera move
// while streaming catches up; only a shortfall that persists means
// the view does not fit, and only that is worth telling the user.
constexpr qint64 kShortfallNoticeMs = 3000;
if (stats.chunks_wanted_missing == 0) {
memory_shortfall_since_.invalidate();
status_memory_label_->setVisible(false);
} else {
if (!memory_shortfall_since_.isValid()) memory_shortfall_since_.start();
if (memory_shortfall_since_.elapsed() >= kShortfallNoticeMs) {
status_memory_label_->setText(
QString("GPU memory full: %1 of %2 visible chunks (%3 MB) not loaded")
.arg(stats.chunks_wanted_missing)
.arg(stats.chunks_wanted)
.arg(double(stats.wanted_missing_bytes) * mb, 0, 'f', 0));
status_memory_label_->setVisible(true);
}
}
if (!status_perf_label_->isVisible()) return;
QString text =
QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws | VRAM %8/%9 MB")
status_perf_label_->setText(
QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws")
.arg(stats.fps, 0, 'f', 1)
.arg(stats.frame_time_ms, 0, 'f', 1)
.arg(stats.visible_objects)
.arg(stats.total_objects)
.arg(stats.visible_triangles)
.arg(stats.total_triangles)
.arg(stats.gl_draw_calls)
.arg(double(stats.vram_used_bytes) * mb, 0, 'f', 0)
.arg(double(stats.vram_capacity_bytes) * mb, 0, 'f', 0);
// The budget is where the pool may grow to; the pool can also sit
// a sub-buffer above it (a release would undershoot). Show it
// only when it tells the user something capacity does not.
if (stats.vram_budget_bytes > 0
&& stats.vram_budget_bytes != stats.vram_capacity_bytes) {
text += QString(" (budget %1)")
.arg(double(stats.vram_budget_bytes) * mb, 0, 'f', 0);
}
// Device total is only known when a driver backend answered.
if (stats.device_vram_total_bytes > 0) {
text += QString(" | Device %1/%2 MB")
.arg(double(stats.device_vram_used_bytes) * mb, 0, 'f', 0)
.arg(double(stats.device_vram_total_bytes) * mb, 0, 'f', 0);
}
if (stats.chunks_wanted_missing > 0) {
text += QString(" | %1/%2 chunks waiting")
.arg(stats.chunks_wanted_missing)
.arg(stats.chunks_wanted);
}
status_perf_label_->setText(text);
.arg(stats.gl_draw_calls));
});
connect(viewport_widget_->viewport(), &ViewportWindow::objectPicked,
this, [this](uint32_t object_id) {
-5
View File
@@ -26,7 +26,6 @@
#include <QStringList>
class QLabel;
#include <QElapsedTimer>
class QDockWidget;
class QMenu;
class QProgressBar;
@@ -71,10 +70,6 @@ private:
QLabel* status_mode_label_ = nullptr;
QLabel* status_selection_label_ = nullptr;
QLabel* status_perf_label_ = nullptr;
// Shown while the visible geometry persistently exceeds what fits in
// GPU memory (see onFrameStats): the user's cue to unload models.
QLabel* status_memory_label_ = nullptr;
QElapsedTimer memory_shortfall_since_;
QProgressBar* status_progress_bar_ = nullptr;
bonsaiviewer::components::TabBar* ribbon_tabs_ = nullptr;
QStackedWidget* ribbon_pages_ = nullptr;
-4
View File
@@ -199,10 +199,6 @@ void SessionState::notifyModelGeometryReady(uint32_t session_model_id) {
emit modelGeometryReady(session_model_id);
}
void SessionState::notifyModelLoadStateChanged(const QString& model_id) {
emit modelLoadStateChanged(model_id);
}
void SessionState::notifyProjectOpened(const QString& path) {
emit projectOpened(path);
}
-5
View File
@@ -89,7 +89,6 @@ public:
void notifyFederationChanged();
void notifyVisibilityChanged();
void notifyModelGeometryReady(uint32_t session_model_id);
void notifyModelLoadStateChanged(const QString& model_id);
void notifyProjectOpened(const QString& path);
void notifyProjectSaved(const QString& path);
void notifyProjectReset();
@@ -108,10 +107,6 @@ signals:
// for both sidecar-cache and stream loads; subscribers that just need to
// re-derive view state (e.g. ViewportView::refresh) listen to this.
void modelGeometryReady(uint32_t session_model_id);
// Fires when a model was unloaded from, or loaded back onto, the GPU
// (commands::unloadModel / loadModel). The viewport is the authority
// for the state itself — ViewportWindow::isModelUnloaded.
void modelLoadStateChanged(const QString& model_id);
// Fires when a model's live IFC data source (the .ifc/.rdb, opened in the
// background after a sidecar-cache hit) becomes available for queries —
// e.g. so the spatial hierarchy can be built once the file is loaded.
@@ -44,7 +44,7 @@ Main components
code.
``GeometryStreamer``
Runs ``ifcopenshell::geom::iterator`` on a worker thread for raw IFC loads. It emits a
Runs ``IfcGeom::Iterator`` on a worker thread for raw IFC loads. It emits a
``StreamedMesh`` once for each unique representation mesh and a
``StreamedInstance`` for each placed occurrence.
-3
View File
@@ -22,7 +22,6 @@
#include "ViewerSettings.h"
#include "components/Style.h"
#include "modules/models/Commands.h"
#include "../ifcparse/parse.h"
#include <QApplication>
#include <QCommandLineParser>
@@ -56,7 +55,6 @@ int main(int argc, char* argv[]) {
QApplication app(argc, argv);
app.setApplicationName("Bonsai Viewer");
app.setOrganizationName("IfcOpenShell");
app.setApplicationVersion(QString::fromUtf8(IFCOPENSHELL_VERSION));
// Clear any .rdbview extractions left in temp by a previous session.
bonsaiviewer::modules::models::commands::cleanupRdbviewCache();
@@ -72,7 +70,6 @@ int main(int argc, char* argv[]) {
QCommandLineParser parser;
parser.setApplicationDescription("Bonsai Viewer — IfcOpenShell IFC viewer");
parser.addHelpOption();
parser.addVersionOption();
parser.process(app);
installUiFont();
+3 -25
View File
@@ -32,7 +32,7 @@
#include "../../../ifcviewer/SceneLoader.h"
#include "../../../ifcviewer/SidecarBuilder.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include "../../../ifcgeom/serializer.h"
#include "../../../ifcgeom/Serializer.h"
#include "../../../serializers/document_serializer_plugin.h"
#include <QDebug>
@@ -262,28 +262,6 @@ void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host,
session.setStatusMessage("Models", "Model removed");
}
void unloadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id) {
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
if (session_model_id == 0) return;
if (session.loader()->isLoadingModel(session_model_id)) return;
const double freed_mb = double(viewport.modelVramBytes(session_model_id)) / (1024.0 * 1024.0);
viewport.unloadModel(session_model_id);
session.notifyModelLoadStateChanged(model_id);
session.setStatusMessage("Models", QString("Model unloaded (freed %1 MB of GPU memory)")
.arg(freed_mb, 0, 'f', 0));
}
void loadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id) {
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
if (session_model_id == 0) return;
if (!viewport.loadModel(session_model_id)) {
session.setStatusMessage("Models", "Not enough GPU memory to load this model");
return;
}
session.notifyModelLoadStateChanged(model_id);
session.setStatusMessage("Models", "Model loaded");
}
void viewModels(SessionState& session, ViewportWindow& viewport, const QStringList& model_ids) {
// Federation ids are the panel's currency; the viewport speaks session
// model ids. sessionModelIdForModelId returns 0 for a model the viewport
@@ -707,7 +685,7 @@ void convertIfcToDatabase(SessionState& session, QWidget& host) {
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
}
std::shared_ptr<ifcopenshell::geom::serializer> serializer = registry.create("rdb", context);
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
serializer->finalize();
} catch (const std::exception& e) {
*error_message = QString::fromUtf8(e.what());
@@ -813,7 +791,7 @@ void exportGeometryDatabase(SessionState& session, QWidget& host) {
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
}
std::shared_ptr<ifcopenshell::geom::serializer> serializer = registry.create("rdb", context);
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
serializer->finalize();
serializer.reset();
@@ -60,12 +60,6 @@ void moveGroup(SessionState& session, const QString& id, const QString& parent_g
void moveModels(SessionState& session, const QStringList& ids, const QString& parent_group_id);
void removeGroup(SessionState& session, QWidget& host, const QString& group_id);
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& model_id);
// GPU residency, distinct from visibility (hide) and from membership
// (remove): unloadModel frees everything the model holds on the device
// while it stays in the federation; loadModel brings it back. Both emit
// modelLoadStateChanged.
void unloadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id);
void loadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id);
// "View Selected Model" — frame the camera on just these models' geometry, the
// way View All frames the whole federation. Models that carry no loaded
// geometry (never loaded, or still streaming their metadata) contribute
@@ -25,25 +25,16 @@
#include "../../../ifcviewer/Federation.h"
#include <QBrush>
#include <QFont>
#include <QColor>
namespace bonsaiviewer::modules::models {
namespace {
QStandardItem* siblingItem(QStandardItem* name_item, Column column) {
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
QStandardItem* parent = name_item->parent();
if (!parent) parent = name_item->model()->invisibleRootItem();
return parent->child(name_item->row(), int(column));
}
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
return siblingItem(name_item, VisibilityColumn);
}
QString formatMegabytes(quint64 bytes) {
return QString("%1 MB").arg(double(bytes) / (1024.0 * 1024.0), 0, 'f', 0);
return parent->child(name_item->row(), 1);
}
template <typename F>
@@ -60,7 +51,7 @@ FederationItemModel::FederationItemModel(Federation* federation, QObject* parent
: QStandardItemModel(parent)
, federation_(federation)
{
setColumnCount(ColumnCount);
setColumnCount(2);
rebuildAll();
connect(federation_, &Federation::groupAdded, this, &FederationItemModel::onGroupAdded);
@@ -76,7 +67,7 @@ FederationItemModel::FederationItemModel(Federation* federation, QObject* parent
void FederationItemModel::rebuildAll() {
clear();
setColumnCount(ColumnCount);
setColumnCount(2);
id_to_name_item_.clear();
for (const auto& root_group : federation_->rootGroups()) {
@@ -130,14 +121,6 @@ QStandardItem* FederationItemModel::makeVisibilityItem(ItemKind kind, bool visib
return item;
}
QStandardItem* FederationItemModel::makeMemoryItem() const {
auto* item = new QStandardItem(QString());
item->setEditable(false);
item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
item->setForeground(QBrush(QColor(bonsaiviewer::ViewerSettings::instance().color("disabled_text"))));
return item;
}
void FederationItemModel::styleRowVisibility(QStandardItem* name_item, bool visible) const {
QStandardItem* vis_item = siblingVisibilityItem(name_item);
if (visible) {
@@ -150,22 +133,6 @@ void FederationItemModel::styleRowVisibility(QStandardItem* name_item, bool visi
}
}
void FederationItemModel::setModelResidency(const QString& model_id, bool unloaded, quint64 vram_bytes) {
QStandardItem* name_item = findItem(model_id);
if (!name_item) return;
QStandardItem* memory_item = siblingItem(name_item, MemoryColumn);
if (!memory_item) return;
const QString text = unloaded ? QStringLiteral("unloaded")
: vram_bytes > 0 ? formatMegabytes(vram_bytes)
: QString();
if (memory_item->text() != text) memory_item->setText(text);
QFont font = name_item->font();
if (font.italic() != unloaded) {
font.setItalic(unloaded);
name_item->setFont(font);
}
}
QStandardItem* FederationItemModel::findItem(const QString& id) const {
return id_to_name_item_.value(id, nullptr);
}
@@ -181,7 +148,7 @@ void FederationItemModel::appendModelTo(QStandardItem* parent_item, const QStrin
if (!model) return;
auto* name_item = makeModelNameItem(model_id, model->display_name);
auto* vis_item = makeVisibilityItem(ItemKind::Model, federation_->isModelEffectivelyVisible(model_id));
parent_item->appendRow({name_item, makeMemoryItem(), vis_item});
parent_item->appendRow({name_item, vis_item});
id_to_name_item_.insert(model_id, name_item);
styleRowVisibility(name_item, federation_->isModelEffectivelyVisible(model_id));
}
@@ -191,7 +158,7 @@ void FederationItemModel::appendGroupSubtreeTo(QStandardItem* parent_item, const
if (!group) return;
auto* name_item = makeGroupNameItem(group_id, group->display_name);
auto* vis_item = makeVisibilityItem(ItemKind::Group, group->visible);
parent_item->appendRow({name_item, makeMemoryItem(), vis_item});
parent_item->appendRow({name_item, vis_item});
id_to_name_item_.insert(group_id, name_item);
styleRowVisibility(name_item, group->visible);
@@ -31,7 +31,7 @@ class Federation;
namespace bonsaiviewer::modules::models {
// QStandardItemModel that mirrors the Federation tree (groups + models in
// three columns: name, GPU memory, visibility icon). Subscribes directly to Federation's
// two columns: name + visibility icon). Subscribes directly to Federation's
// granular signals so each mutation only touches the affected rows — view
// state (expansion, selection, scroll) is preserved automatically.
//
@@ -57,12 +57,6 @@ public:
// previously- and newly-active model rows.
void setActiveModelId(const QString& model_id);
// GPU residency is viewport state, not Federation state, so it is pushed
// in by the owning View: the memory column shows `vram_bytes` for a
// loaded model and "unloaded" for one the user unloaded (which is also
// drawn in italics). Models the viewport knows nothing about show blank.
void setModelResidency(const QString& model_id, bool unloaded, quint64 vram_bytes);
private slots:
void onGroupAdded(const QString& group_id);
void onGroupRemoved(const QString& group_id);
@@ -78,7 +72,6 @@ private:
QStandardItem* makeGroupNameItem(const QString& group_id, const QString& display_name) const;
QStandardItem* makeModelNameItem(const QString& model_id, const QString& display_name) const;
QStandardItem* makeVisibilityItem(ItemKind kind, bool visible) const;
QStandardItem* makeMemoryItem() const;
void styleRowVisibility(QStandardItem* name_item, bool visible) const;
QStandardItem* findItem(const QString& id) const;
+6 -28
View File
@@ -28,7 +28,6 @@
#include "../../components/Section.h"
#include "../../components/SvgIcon.h"
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include <QDataStream>
#include <QDrag>
@@ -80,7 +79,6 @@ QStringList selectedModelIdsAt(QTreeView* tree, const QModelIndex& clicked_index
}
constexpr int kVisibilityColumnWidth = 28;
constexpr int kMemoryColumnWidth = 72; // "1234 MB" / "unloaded"
// QTreeView subclass that handles drag-and-drop. Drop logic dispatches
// through commands (not directly into the model) so notifications + status
@@ -252,7 +250,7 @@ ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
if (!index.isValid()) return;
if (index.column() == VisibilityColumn) {
if (index.column() == 1) {
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
return;
}
@@ -382,24 +380,6 @@ ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
commands::saveModelAsToCloud(*session_state_, *this, id);
});
// GPU residency. Unload keeps the model in the federation (and
// its visibility) but frees everything it holds on the GPU — the
// lever when the scene does not fit in VRAM. Load brings it back.
menu.addSeparator();
const uint32_t session_model_id = session_state_->sessionModelIdForModelId(id);
const bool unloaded = session_model_id != 0 && viewport_->isModelUnloaded(session_model_id);
QAction* residency = menu.addAction(
components::icons::makeSvgIcon(":/icons/cube.svg"),
unloaded ? "Load Model" : "Unload Model");
residency->setEnabled(session_model_id != 0);
residency->setToolTip(unloaded
? "Allocate GPU memory for this model again and stream its geometry back in."
: "Free this model's GPU memory while keeping it in the federation.");
connect(residency, &QAction::triggered, this, [this, id, unloaded]() {
if (unloaded) commands::loadModel(*session_state_, *viewport_, id);
else commands::unloadModel(*session_state_, *viewport_, id);
});
menu.addSeparator();
QAction* remove = menu.addAction(
components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model");
@@ -429,16 +409,14 @@ void ModelsPanel::setModel(FederationItemModel* model) {
}
void ModelsPanel::applyColumnLayout() {
// The name stretches to fill; memory and visibility are fixed.
// Column 0 (name) stretches to fill; column 1 (visibility icon) is fixed.
QHeaderView* header = tree_->header();
if (header->count() < ColumnCount) return;
if (header->count() < 2) return;
header->setStretchLastSection(false);
header->setMinimumSectionSize(kVisibilityColumnWidth);
header->setSectionResizeMode(NameColumn, QHeaderView::Stretch);
header->setSectionResizeMode(MemoryColumn, QHeaderView::Fixed);
header->resizeSection(MemoryColumn, kMemoryColumnWidth);
header->setSectionResizeMode(VisibilityColumn, QHeaderView::Fixed);
header->resizeSection(VisibilityColumn, kVisibilityColumnWidth);
header->setSectionResizeMode(0, QHeaderView::Stretch);
header->setSectionResizeMode(1, QHeaderView::Fixed);
header->resizeSection(1, kVisibilityColumnWidth);
}
} // namespace bonsaiviewer::modules::models
@@ -75,16 +75,16 @@ QString formatCachedUnitScale(double meters_per_unit) {
return QString("Cached scale: 1 unit = %1 m").arg(formatNumber(meters_per_unit));
}
std::string enumString(const ifcopenshell::attribute_value& av) {
std::string enumString(const attribute_value& av) {
if (av.isNull()) return {};
if (av.type() != ifcopenshell::Argument_ENUMERATION) return {};
ifcopenshell::enumeration_reference enumeration = av;
enumeration_reference enumeration = av;
return std::string(enumeration.value() ? enumeration.value() : "");
}
QString formatNamedUnit(const express::base& unit) {
QString formatNamedUnit(const express::Base& unit) {
if (!unit) return "";
auto entity = unit.as<express::entity>();
auto entity = unit.as<express::Entity>();
if (unit.declaration().is("IfcSIUnit")) {
const std::string prefix = enumString(entity.get("Prefix"));
const std::string name = enumString(entity.get("Name"));
-8
View File
@@ -31,14 +31,6 @@ enum class ItemKind {
Model,
};
// Columns of the models tree: name | GPU memory | visibility eye.
enum Column : int {
NameColumn = 0,
MemoryColumn = 1,
VisibilityColumn = 2,
ColumnCount = 3,
};
struct TreeNode {
QString id;
QString name;
+1 -30
View File
@@ -26,9 +26,6 @@
#include "../../ViewerSettings.h"
#include "../../SessionState.h"
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include <QTimer>
namespace bonsaiviewer::modules::models {
@@ -57,19 +54,17 @@ QList<GroupOption> validMoveTargets(const Federation& federation,
ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
bonsaiviewer::SessionState* session_state,
ViewportWindow* viewport,
QObject* parent)
: QObject(parent)
, widget_(widget)
, session_state_(session_state)
, viewport_(viewport)
, model_(new FederationItemModel(session_state->federation(), this))
{
widget_->setModel(model_);
// Coarse signals: full rebuild + re-style. The granular Federation
// signals are handled inside FederationItemModel and don't reach here.
auto rebuild = [this]() { model_->rebuildAll(); refreshResidency(); };
auto rebuild = [this]() { model_->rebuildAll(); };
connect(session_state_, &SessionState::projectReset, this, rebuild);
connect(session_state_, &SessionState::projectOpened, this, rebuild);
connect(&bonsaiviewer::ViewerSettings::instance(),
@@ -78,30 +73,6 @@ ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
connect(session_state_, &SessionState::activeModelChanged, this, [this](const QString& model_id) {
model_->setActiveModelId(model_id);
});
// Residency: immediately on the events that change it, and on a slow
// tick for the memory figures, which move as chunks stream.
auto refresh = [this]() { refreshResidency(); };
connect(session_state_, &SessionState::modelLoadStateChanged, this, refresh);
connect(session_state_, &SessionState::modelGeometryReady, this, refresh);
connect(session_state_, &SessionState::modelsChanged, this, refresh);
auto* tick = new QTimer(this);
tick->setInterval(1000);
connect(tick, &QTimer::timeout, this, refresh);
tick->start();
}
void ModelsPanelView::refreshResidency() {
for (const auto& model : session_state_->federation()->models()) {
const uint32_t session_model_id = session_state_->sessionModelIdForModelId(model.id);
if (session_model_id == 0) {
model_->setModelResidency(model.id, false, 0);
continue;
}
model_->setModelResidency(model.id,
viewport_->isModelUnloaded(session_model_id),
viewport_->modelVramBytes(session_model_id));
}
}
} // namespace bonsaiviewer::modules::models

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