mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-25 14:26:47 +00:00
Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78758771fd | |||
| d808104725 | |||
| b71b217814 | |||
| d9f218eacf | |||
| 0faba0fdd8 | |||
| ad113e1283 | |||
| 4a761b51f5 | |||
| 73f8e6aea7 | |||
| e5072460cd | |||
| 311b75a955 | |||
| 6f24133d35 | |||
| 091b4d4113 | |||
| 06d87e21a7 | |||
| b7f1f1728f | |||
| 8074541057 | |||
| d755ca3a59 | |||
| 777b728205 | |||
| 24616ed655 | |||
| 6201c4052b | |||
| ab99024307 | |||
| b7d2b2fa3a | |||
| 5fa14c3dea | |||
| 7fa8506fac | |||
| 9d68e7b9ca | |||
| 9ffbfe0dbb | |||
| 5b00c8b451 | |||
| a5b6f83a3d | |||
| 908d85a51a | |||
| 9089a20ce3 | |||
| d4a5420851 | |||
| 87bc6bfbab | |||
| a5e94cf0d8 | |||
| 2c1d445d5b | |||
| d86f89090b | |||
| 973f61c6dc | |||
| f47aa4d81a | |||
| fa9a3383aa | |||
| 104591a80b | |||
| 030e6e5bb4 | |||
| a031310a66 | |||
| 436e3f7b2a | |||
| 335d571854 | |||
| f78b380b71 | |||
| 2cebc3f60b | |||
| 0a8159505d | |||
| 301fba5a8b | |||
| ba90cf220d | |||
| 6318892a97 | |||
| 1a6336bd20 | |||
| 511584b36f | |||
| f65de78c46 | |||
| 59b957daff | |||
| 81a0941d5a | |||
| dba735f1ee | |||
| e100cf5a34 | |||
| 665502cbc5 | |||
| 252831d7f0 | |||
| 3a6055a558 | |||
| 7b1b0b986c | |||
| cd34d92fdb | |||
| 1391c7d974 | |||
| 223d6da3b1 | |||
| 171e899eb0 | |||
| e2561ffa3b | |||
| 4b87ab5d0d | |||
| 8cc36f0d4d | |||
| 13cc190849 | |||
| b71354ce19 | |||
| 17042f6f80 | |||
| beb0db89e5 | |||
| 572f718007 | |||
| 6bab0603e6 | |||
| b408e64e5e | |||
| f580f7255f | |||
| b252cd25f8 | |||
| 935562142e | |||
| d1d0fb4636 | |||
| e0b226f4ca | |||
| 79bd3563de | |||
| 4e887e1c59 | |||
| fa9f3b5cb7 | |||
| 77dc679a6e | |||
| ce9e2b94d5 | |||
| 58dcaed89a | |||
| 109bd58384 | |||
| b6dccce12a | |||
| daa7d98b3f | |||
| 321760cea4 | |||
| 7b9615f4e5 | |||
| ff22a9d1f3 | |||
| 055f64fa9b | |||
| 7370d07db1 | |||
| 717d6aa2af | |||
| 69b0409aa0 | |||
| 4095d5c8d6 | |||
| 19a3707f72 | |||
| 262117c4f8 | |||
| 246fa24be0 | |||
| 35d2fb43e2 | |||
| f10f7eba83 | |||
| 785936000a | |||
| 83fc219a8a | |||
| 7a1dcd07c8 | |||
| b63137e859 | |||
| 3d15500976 | |||
| 64aed6a766 | |||
| a08eed7ac9 | |||
| 076f46cfeb | |||
| a353edb9e0 | |||
| 30fb379e32 | |||
| dbea3f0362 | |||
| b5eca83357 | |||
| e5aaf7c602 | |||
| 9e53d0dcc9 | |||
| 17c4d8faff | |||
| c9c7edd4d6 | |||
| 96653029cf | |||
| e9fffc221b | |||
| a441757080 | |||
| c818f48a47 | |||
| 28c9c1d34d | |||
| f05dd4aea5 | |||
| dcfc22e29e | |||
| be3c2ee770 | |||
| fbfa51c451 | |||
| 19f3261dc3 | |||
| 7ed8584edc | |||
| 61f30dd200 | |||
| b706121f53 | |||
| 99a09a2a3c | |||
| 2ba55ba984 | |||
| 616c7a00d5 | |||
| 8c003110fe | |||
| 4e49b640a7 | |||
| c30841aad6 | |||
| 4597929df9 | |||
| 2859c1ef17 | |||
| 7ae6bf4374 | |||
| 02481b3247 | |||
| 2c47c9d4fa | |||
| c2abc3f844 | |||
| 4dcd644a32 | |||
| 6fea72b045 | |||
| 1573730f18 | |||
| 8f4832651a | |||
| af58eaf79f | |||
| 8870ffb018 | |||
| c5ba22451f | |||
| 1a931ddfd9 | |||
| f7876a97ee | |||
| 4d0e5f6aee | |||
| dfc60196ec | |||
| ef4bba8b33 | |||
| 048242783e | |||
| 6f3acc84ee | |||
| e077390e3d | |||
| 80cc603932 |
@@ -1,5 +1,4 @@
|
||||
Checks: 'bugprone-*,cert-*,clang-analyzer-*,readability-*'
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
AnalyzeTemporaryDtors: false
|
||||
FormatStyle: none
|
||||
@@ -0,0 +1,382 @@
|
||||
# /// 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())
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
working-directory: src/bonsaiviewer-autodesk
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ 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
|
||||
@@ -61,7 +64,7 @@ jobs:
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
python ../nix/cache_dependencies.py unpack
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
@@ -102,7 +105,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}" \
|
||||
python3 ./nix/build-all.py -v --diskcleanup --ifcopenshell-shared ${MAC_INTEL} \
|
||||
uv run ./nix/build-all.py -v --diskcleanup --ifcopenshell-shared ${MAC_INTEL} \
|
||||
| tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
@@ -119,7 +122,7 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
python ../nix/cache_dependencies.py pack
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
@@ -136,7 +139,7 @@ jobs:
|
||||
# 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
|
||||
uv run src/bonsaiviewer-autodesk/packaging/build.py
|
||||
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
|
||||
test -d "$autodesk_connector_dir"
|
||||
|
||||
|
||||
@@ -92,10 +92,6 @@ 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:
|
||||
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
- 'src/ifc5d/ifc5d/**'
|
||||
- 'src/ifccityjson/**'
|
||||
branches:
|
||||
- v0.8.0
|
||||
- v0.9.0
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -51,19 +51,10 @@ 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
|
||||
@@ -72,7 +63,7 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
- name: Compile
|
||||
run: |
|
||||
cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
|
||||
@@ -128,7 +119,7 @@ jobs:
|
||||
blender --command extension install-file -r user_default -e $bonsai_zip
|
||||
blender --command extension list
|
||||
|
||||
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
|
||||
git clone --branch ${{ github.ref_name }} --single-branch https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
|
||||
|
||||
# Reregister Bonsai.
|
||||
# Note that running it in background might miss some errors
|
||||
|
||||
@@ -34,19 +34,10 @@ 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,7 +27,9 @@ jobs:
|
||||
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
# 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
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
- '.github/workflows/ci-ifcsverchok-build.yml'
|
||||
- 'src/ifcsverchok/*'
|
||||
branches:
|
||||
- v0.8.0
|
||||
- v0.9.0
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -3,11 +3,13 @@ name: ci-ifctester-org
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- v0.9.0
|
||||
paths:
|
||||
- src/ifctester/**
|
||||
|
||||
jobs:
|
||||
publish_website:
|
||||
publish_ifctester_org:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -43,6 +43,7 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt-get install --no-install-recommends -y \
|
||||
cmake \
|
||||
bison \
|
||||
gcc \
|
||||
g++ \
|
||||
libboost-date-time-dev \
|
||||
@@ -61,13 +62,23 @@ jobs:
|
||||
libocct-ocaf-dev \
|
||||
libocct-visualization-dev \
|
||||
libpcre3-dev \
|
||||
libpcre2-dev \
|
||||
libtbb-dev \
|
||||
libxml2-dev \
|
||||
libxi-dev \
|
||||
occt-misc \
|
||||
tcl-dev \
|
||||
tk-dev \
|
||||
swig
|
||||
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
|
||||
|
||||
- name: Configure minimal IfcOpenShell
|
||||
run: |
|
||||
|
||||
@@ -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/allow_static_libraries_config_on_unix.patch
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/config_select_libs_by_use_shared.patch
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# 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
|
||||
+7
-3
@@ -111,10 +111,11 @@ 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
|
||||
|
||||
@@ -125,6 +126,9 @@ 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
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
[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
|
||||
|
||||
+19
-14
@@ -36,6 +36,14 @@ 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+
|
||||
@@ -55,9 +63,15 @@ endif()
|
||||
# Include utility macros and functions
|
||||
include(utilities.cmake)
|
||||
|
||||
# use extra version to make pre-release using eg semver
|
||||
# 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.
|
||||
if(NOT DEFINED EXTRA_VERSION)
|
||||
set(EXTRA_VERSION "-alpha.3")
|
||||
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()
|
||||
endif()
|
||||
|
||||
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
|
||||
@@ -119,7 +133,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 "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" OFF)
|
||||
option(VERSION_OVERRIDE "Use VERSION as the branch label when commit information is embedded" OFF)
|
||||
|
||||
set(
|
||||
PYTHON_MODULE_INSTALL_DIR
|
||||
@@ -127,15 +141,7 @@ set(
|
||||
"Directory to install IfcPython package to. By default package is installed in found Python's site-packages."
|
||||
)
|
||||
|
||||
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})
|
||||
project(IfcOpenShell VERSION ${PROJECT_VERSION_NUMERIC})
|
||||
|
||||
# Make sure CMake modules in this project are found first
|
||||
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
|
||||
@@ -693,8 +699,7 @@ endif()
|
||||
|
||||
# Documentation
|
||||
if(BUILD_DOCUMENTATION)
|
||||
set(CMAKE_MODULE_PATH "../docs/cmake")
|
||||
add_subdirectory(../docs docs)
|
||||
add_subdirectory(../docs/cpp-api docs/cpp-api)
|
||||
endif()
|
||||
|
||||
if(BUILD_EXAMPLES)
|
||||
|
||||
@@ -52,6 +52,7 @@ 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})
|
||||
|
||||
+13
-33
@@ -1,35 +1,15 @@
|
||||
#Look for an executable called sphinx-build
|
||||
find_program(SPHINX_EXECUTABLE NAMES sphinx-build DOC "Path to sphinx-build executable")
|
||||
|
||||
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)
|
||||
find_program(
|
||||
SPHINX_EXECUTABLE
|
||||
NAMES sphinx-build
|
||||
REQUIRED
|
||||
DOC "Path to the sphinx-build executable"
|
||||
)
|
||||
|
||||
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)
|
||||
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
|
||||
)
|
||||
|
||||
+63
-22
@@ -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
|
||||
OUTPUT_DIRECTORY = ./output/doxygen
|
||||
|
||||
# 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 = YES
|
||||
WARN_IF_UNDOCUMENTED = NO
|
||||
|
||||
# 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 = NO
|
||||
WARN_AS_ERROR = FAIL_ON_WARNINGS
|
||||
|
||||
# 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,7 +944,6 @@ WARN_LOGFILE =
|
||||
# Note: If this tag is empty the current directory is searched.
|
||||
|
||||
INPUT = ../../src/ifcgeom \
|
||||
../../src/ifcgeom_schema_agnostic \
|
||||
../../src/ifcparse \
|
||||
../../src/serializers \
|
||||
|
||||
@@ -1001,7 +1000,7 @@ RECURSIVE = YES
|
||||
# Note that relative paths are relative to the directory from which doxygen is
|
||||
# run.
|
||||
|
||||
EXCLUDE =
|
||||
EXCLUDE = ../../src/ifcparse/schemas
|
||||
|
||||
# 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
|
||||
@@ -1025,7 +1024,33 @@ EXCLUDE_PATTERNS =
|
||||
# wildcard * is used, a substring. Examples: ANamespace, AClass,
|
||||
# ANamespace::AClass, ANamespace::*Test
|
||||
|
||||
EXCLUDE_SYMBOLS =
|
||||
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
|
||||
|
||||
# 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
|
||||
@@ -1236,7 +1261,7 @@ IGNORE_PREFIX =
|
||||
# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
|
||||
# The default value is: YES.
|
||||
|
||||
GENERATE_HTML = YES
|
||||
GENERATE_HTML = NO
|
||||
|
||||
# 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
|
||||
@@ -1311,7 +1336,7 @@ HTML_STYLESHEET =
|
||||
# documentation.
|
||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||
|
||||
HTML_EXTRA_STYLESHEET = assets/doxygen-awesome-css/doxygen-awesome.css
|
||||
HTML_EXTRA_STYLESHEET =
|
||||
|
||||
# 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
|
||||
@@ -2166,7 +2191,7 @@ MAN_LINKS = NO
|
||||
# captures the structure of the code including all documentation.
|
||||
# The default value is: NO.
|
||||
|
||||
GENERATE_XML = NO
|
||||
GENERATE_XML = YES
|
||||
|
||||
# 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
|
||||
@@ -2303,7 +2328,7 @@ ENABLE_PREPROCESSING = YES
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
MACRO_EXPANSION = NO
|
||||
MACRO_EXPANSION = YES
|
||||
|
||||
# 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
|
||||
@@ -2311,7 +2336,7 @@ MACRO_EXPANSION = NO
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
EXPAND_ONLY_PREDEF = YES
|
||||
|
||||
# If the SEARCH_INCLUDES tag is set to YES, the include files in the
|
||||
# INCLUDE_PATH will be searched if a #include is found.
|
||||
@@ -2344,7 +2369,17 @@ 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 =
|
||||
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=
|
||||
|
||||
# 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
|
||||
@@ -2353,7 +2388,22 @@ PREDEFINED =
|
||||
# definition found in the source code.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
EXPAND_AS_DEFINED =
|
||||
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
|
||||
|
||||
# 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
|
||||
@@ -2731,15 +2781,6 @@ 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.
|
||||
|
||||
+41
-18
@@ -1,33 +1,56 @@
|
||||
# IfcOpenShell C++ API documentation
|
||||
|
||||
This folder contains the setup to build the IfcOpenShell C++ API documentation from the source code.
|
||||
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"
|
||||
```
|
||||
|
||||
## Generating the documentation
|
||||
|
||||
> 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):
|
||||
From this directory, run:
|
||||
|
||||
```shell
|
||||
$ doxygen
|
||||
python -m sphinx -M html . output -W --keep-going
|
||||
```
|
||||
|
||||
To include the current git commit hash into the build documentation, use the following command:
|
||||
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:
|
||||
|
||||
```shell
|
||||
$ PROJECT_NUMBER=$(git rev-parse --short HEAD) doxygen
|
||||
PROJECT_NUMBER=$(git rev-parse --short HEAD) python -m sphinx -M html . output -W --keep-going
|
||||
```
|
||||
|
||||
This will extract the current commit hash in short version and sets the propper ENV variable used by doxygen.
|
||||
Alternatively, configure the main CMake project with
|
||||
`-DBUILD_DOCUMENTATION=ON` and build the `cpp_api_docs` target.
|
||||
|
||||
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 documentation is written to `output/html/index.html`. The
|
||||
generated Doxygen XML and Exhale sources are kept under `output/` as build
|
||||
artifacts.
|
||||
|
||||
The resulting documentation is located unter `/cpp-api/output/html` and can be directly accessed with your browser:
|
||||
|
||||
```shell
|
||||
$ open ./output/html/index.html
|
||||
```
|
||||
The generated headers under `src/ifcparse/schemas` are intentionally excluded
|
||||
from this documentation build.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 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",
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
.. This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
IfcOpenShell C++ API
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
output/api/library_root
|
||||
@@ -0,0 +1,5 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
Sphinx==8.1.3
|
||||
breathe==4.36.0
|
||||
exhale==0.3.7
|
||||
+269
-79
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/python
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "typing_extensions",
|
||||
# ]
|
||||
# ///
|
||||
###############################################################################
|
||||
# #
|
||||
@@ -29,17 +32,7 @@ Example usage:
|
||||
python build-all.py IfcParse IfcOpenShell-Python
|
||||
|
||||
|
||||
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
|
||||
Run with --help to see available arguments.
|
||||
|
||||
|
||||
Used environment variables:
|
||||
@@ -68,6 +61,8 @@ 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 #
|
||||
# #
|
||||
@@ -112,6 +107,9 @@ Used environment variables:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import logging
|
||||
import multiprocessing
|
||||
@@ -123,14 +121,17 @@ 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
|
||||
from typing import Literal, NamedTuple
|
||||
from urllib.request import urlretrieve
|
||||
|
||||
from typing_extensions import assert_never
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
ch = logging.StreamHandler()
|
||||
@@ -154,6 +155,7 @@ 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"]
|
||||
JSON_VERSION = "3.11.3"
|
||||
@@ -194,10 +196,142 @@ strip = "strip"
|
||||
xz = "xz" # Used implicitly for `tar -xf *.tar.xz`.
|
||||
brew = "brew"
|
||||
|
||||
explicit_targets = [s for s in sys.argv[1:] if not s.startswith("-")]
|
||||
|
||||
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)
|
||||
"""Targets provided by CLI."""
|
||||
flags = set(s.lstrip("-") for s in sys.argv[1:] if s.startswith("-"))
|
||||
"""CLI flags."""
|
||||
|
||||
# Helper function for coloured printing
|
||||
|
||||
@@ -216,12 +350,11 @@ def cecho(message, color=NO_COLOR):
|
||||
logger.info(f"{color}{message}\033[0m")
|
||||
|
||||
|
||||
# Flags.
|
||||
APPLE = platform.system() == "Darwin"
|
||||
MAC_CROSS_COMPILE_INTEL = "mac-cross-compile-intel" in flags
|
||||
MAC_CROSS_COMPILE_INTEL = ARGS.mac_cross_compile_intel
|
||||
assert platform.system() == "Darwin" or not MAC_CROSS_COMPILE_INTEL
|
||||
|
||||
WASM = "wasm" in flags
|
||||
WASM = ARGS.wasm
|
||||
"""Build WASM outside pyodide build environment."""
|
||||
WASM_CMAKE_IS_USING_INIT_VARS = False
|
||||
if WASM:
|
||||
@@ -367,12 +500,12 @@ dependency_tree: dict[str, tuple[str, ...]] = {
|
||||
def gather_dependencies(dep: str) -> Generator[str]:
|
||||
yield dep
|
||||
for d in dependency_tree[dep]:
|
||||
if f"without-{d.lower()}" not in flags:
|
||||
if d.lower() not in DYNAMIC_ARGS.without:
|
||||
for x in gather_dependencies(d):
|
||||
yield x
|
||||
|
||||
|
||||
if "v" in flags:
|
||||
if ARGS.verbose:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
ch.setFormatter(formatter)
|
||||
@@ -393,29 +526,26 @@ else:
|
||||
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
|
||||
|
||||
OFF_ON = ["OFF", "ON"]
|
||||
BUILD_STATIC = "shared" not in flags
|
||||
BUILD_STATIC = not ARGS.shared
|
||||
"""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 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.py_versions:
|
||||
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if "".join(pyv.split(".")[:2]) in DYNAMIC_ARGS.py_versions]
|
||||
|
||||
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 DYNAMIC_ARGS.occt_version is not None:
|
||||
OCCT_VERSION = DYNAMIC_ARGS.occt_version
|
||||
|
||||
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 "without-%s" % t.lower() not in flags)
|
||||
targets = set(t for t in targets if t.lower() not in DYNAMIC_ARGS.without)
|
||||
if not explicit_targets and not BUILD_BONSAIVIEWER:
|
||||
targets.difference_update({"BonsaiViewer", "qt6"})
|
||||
if BUILD_BONSAIVIEWER:
|
||||
@@ -432,7 +562,6 @@ if WASM:
|
||||
SKIP_TARGETS_FOR_WASM = {
|
||||
"rocksdb",
|
||||
"opencollada",
|
||||
"swig",
|
||||
"pcre",
|
||||
"IfcGeom",
|
||||
"IfcConvert",
|
||||
@@ -454,12 +583,8 @@ bison = "bison"
|
||||
|
||||
missing_commands: list[str] = []
|
||||
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz, bison]
|
||||
if "wasm" in flags:
|
||||
# Skip swig build for WASM.
|
||||
required_commands.append("swig")
|
||||
if WASM:
|
||||
required_commands.append("pyodide")
|
||||
required_commands.remove(yacc)
|
||||
required_commands.remove(bison)
|
||||
if platform.system() == "Linux" and "BonsaiViewer" in targets:
|
||||
required_commands.append("patchelf")
|
||||
|
||||
@@ -503,7 +628,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) -> str:
|
||||
def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False, env: dict[str, str] | None = None) -> str:
|
||||
"""
|
||||
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
|
||||
sets up logging `stderr` to `LOG_FILE` (in append mode) and returns stdout
|
||||
@@ -527,7 +652,7 @@ def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False) ->
|
||||
# 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")
|
||||
proc = sp.Popen(cmds, cwd=cwd, stdout=sp.PIPE, stderr=sp.PIPE, encoding="utf-8", env=env)
|
||||
assert proc.stdout and proc.stderr
|
||||
|
||||
t_out = threading.Thread(target=stream_reader, args=(proc.stdout, stdout, log_file_handle))
|
||||
@@ -576,7 +701,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" in flags:
|
||||
if WASM:
|
||||
wasm.append("emconfigure")
|
||||
|
||||
run(
|
||||
@@ -584,7 +709,7 @@ def run_autoconf(dependency_name: str, configure_args: list[str], cwd: str) -> N
|
||||
*wasm,
|
||||
"/bin/sh",
|
||||
"../configure",
|
||||
*(["--host=wasm32"] if "wasm" in flags and not any(s.startswith("--host") for s in configure_args) else []),
|
||||
*(["--host=wasm32"] if WASM and not any(s.startswith("--host") for s in configure_args) else []),
|
||||
*configure_args,
|
||||
f"--prefix={prefix}",
|
||||
],
|
||||
@@ -592,18 +717,20 @@ def run_autoconf(dependency_name: str, configure_args: list[str], cwd: str) -> N
|
||||
)
|
||||
|
||||
|
||||
def run_cmake(arg1, cmake_args: list[str], cmake_dir: str | None = None, cwd: str | None = None):
|
||||
def run_cmake(
|
||||
name, cmake_args: list[str], cmake_dir: str | None = None, cwd: str | None = None, native: bool = False
|
||||
) -> None:
|
||||
if cmake_dir is None:
|
||||
P = ".."
|
||||
else:
|
||||
P = cmake_dir
|
||||
|
||||
wasm = []
|
||||
if "wasm" in flags:
|
||||
if WASM and not native:
|
||||
wasm.append("emcmake")
|
||||
|
||||
cmake_flags: list[str] = []
|
||||
if not WASM or not WASM_CMAKE_IS_USING_INIT_VARS:
|
||||
if not native and (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.
|
||||
@@ -619,6 +746,10 @@ def run_cmake(arg1, cmake_args: list[str], cmake_dir: str | None = None, cwd: st
|
||||
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,
|
||||
@@ -627,7 +758,7 @@ def run_cmake(arg1, cmake_args: list[str], cmake_dir: str | None = None, cwd: st
|
||||
*cmake_flags,
|
||||
*cmake_args,
|
||||
f"-DCMAKE_BUILD_TYPE={BUILD_CFG}",
|
||||
f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}",
|
||||
*([] if native else [f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}"]),
|
||||
],
|
||||
cwd=cwd,
|
||||
)
|
||||
@@ -675,7 +806,7 @@ def build_dependency(
|
||||
additional_files: dict[str, str] | None = None,
|
||||
no_append_name=False,
|
||||
cmake_dir=None,
|
||||
**kwargs,
|
||||
cmake_native: bool = False,
|
||||
) -> None:
|
||||
"""Handles building of dependencies with different tools (which are
|
||||
distinguished with the `mode` argument. `build_tool_args` is expected to be
|
||||
@@ -684,7 +815,8 @@ def build_dependency(
|
||||
|
||||
:param pre_compile_subs: A sequence of ``(fn, before, after)``
|
||||
:param additional_files: Mapping path->url.
|
||||
:param kwargs: Additional ``mode`` related kwargs.
|
||||
:param cmake_native: For ``mode="cmake"``, force a native (host) build
|
||||
even when building for WASM. Needed for build-time tools like swig.
|
||||
"""
|
||||
check_dir = os.path.join(DEPS_DIR, "install", name)
|
||||
if os.path.exists(check_dir):
|
||||
@@ -720,7 +852,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:
|
||||
raise ValueError(f"download tool '{download_tool}' is not supported")
|
||||
assert_never(download_tool)
|
||||
download_dir = os.path.join(build_dir, download_name)
|
||||
|
||||
if os.path.isdir(download_dir):
|
||||
@@ -781,9 +913,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)
|
||||
run_cmake(name, build_tool_args, cwd=extract_build_dir, native=cmake_native)
|
||||
else:
|
||||
raise ValueError()
|
||||
assert_never(mode)
|
||||
for fn, before, after in pre_compile_subs:
|
||||
with open(os.path.join(extract_dir, fn), "r") as f:
|
||||
s = f.read()
|
||||
@@ -799,14 +931,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" in flags)
|
||||
run(["./b2", f"-j{IFCOS_NUM_BUILD_PROCS}"] + build_tool_args, cwd=extract_dir, can_fail=WASM)
|
||||
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 "diskcleanup" in flags:
|
||||
if ARGS.diskcleanup:
|
||||
shutil.rmtree(build_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@@ -912,7 +1044,9 @@ ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
|
||||
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
if "wasm" in flags:
|
||||
CXXFLAGS_SHARED = CXXFLAGS_MINIMAL
|
||||
CFLAGS_SHARED = CFLAGS_MINIMAL
|
||||
if WASM:
|
||||
# WASM `SIDE_MODULE_` are absorbed by `emcmake` automatically.
|
||||
CXXFLAGS = CXXFLAGS_MINIMAL
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
@@ -921,19 +1055,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_MINIMAL
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
CXXFLAGS = CXXFLAGS_SHARED
|
||||
CFLAGS = CFLAGS_SHARED
|
||||
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_MINIMAL
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
CXXFLAGS = CXXFLAGS_SHARED
|
||||
CFLAGS = CFLAGS_SHARED
|
||||
LDFLAGS = f"{LDFLAGS} {ADDITIONAL_ARGS_STR}"
|
||||
|
||||
if "lto" in flags:
|
||||
if ARGS.lto:
|
||||
for f in compiler_flags:
|
||||
locals()[f] += f" -flto={IFCOS_NUM_BUILD_PROCS}"
|
||||
|
||||
@@ -1010,31 +1144,46 @@ 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 IgesSerializer), we use a patch to skip only ExpToCasExe.
|
||||
# Since we do need DataExchange (used for iges_serializer), 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" in flags:
|
||||
if WASM:
|
||||
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=f"occt-{OCCT_VERSION}",
|
||||
name=occt_name,
|
||||
mode="cmake",
|
||||
build_tool_args=[
|
||||
f"-DINSTALL_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}",
|
||||
f"-DBUILD_LIBRARY_TYPE={LINK_TYPE_UCFIRST}",
|
||||
f"-DINSTALL_DIR={OCCT_INSTALL_PATH}",
|
||||
f"-DBUILD_LIBRARY_TYPE={occt_link_type}",
|
||||
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.
|
||||
@@ -1053,6 +1202,11 @@ 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}",
|
||||
@@ -1110,7 +1264,7 @@ if "libxml2" in targets:
|
||||
"--without-iconv",
|
||||
"--without-lzma",
|
||||
]
|
||||
if "wasm" in flags:
|
||||
if WASM:
|
||||
build_tool_args.append("--without-threads")
|
||||
build_dependency(
|
||||
f"libxml2-{LIBXML2_VERSION}",
|
||||
@@ -1132,7 +1286,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" in flags:
|
||||
if WASM:
|
||||
# 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
|
||||
@@ -1162,7 +1316,7 @@ if "OpenCOLLADA" in targets:
|
||||
revision=OPENCOLLADA_VERSION,
|
||||
)
|
||||
|
||||
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flags:
|
||||
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and not WASM:
|
||||
# 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"]
|
||||
@@ -1223,7 +1377,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
if "boost" in targets:
|
||||
str_concat = lambda prefix: lambda postfix: "" if postfix.strip() == "" else "=".join((prefix, postfix.strip()))
|
||||
toolset = []
|
||||
if "wasm" in flags:
|
||||
if WASM:
|
||||
toolset.append("toolset=emscripten")
|
||||
build_dependency(
|
||||
f"boost-{BOOST_VERSION}",
|
||||
@@ -1251,7 +1405,7 @@ if "boost" in targets:
|
||||
# patch="./patches/boost/boostorg_regex_62.patch",
|
||||
download_name=f"boost-{BOOST_VERSION}-b2-nodocs.tar.gz",
|
||||
)
|
||||
if "wasm" in flags:
|
||||
if WASM:
|
||||
# only supported on nix for now
|
||||
run(
|
||||
("find", ".", "-name", "*.bc", "-exec", "bash", "-c", "emar q ${1%.bc}.a $1", "bash", "{}", ";"),
|
||||
@@ -1293,9 +1447,7 @@ 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" in flags else []
|
||||
),
|
||||
pre_compile_subs=([("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if WASM 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/",
|
||||
@@ -1417,7 +1569,7 @@ if "qt6" in targets:
|
||||
cecho("Building IfcOpenShell:", GREEN)
|
||||
|
||||
IFCOS_DIR = os.path.join(DEPS_DIR, "build", "ifcopenshell")
|
||||
if os.environ.get("NO_CLEAN", "").lower() not in {"1", "on", "true"}:
|
||||
if not is_on_off(os.getenv("NO_CLEAN"), default=False):
|
||||
if os.path.exists(IFCOS_DIR):
|
||||
shutil.rmtree(IFCOS_DIR)
|
||||
os.makedirs(IFCOS_DIR, exist_ok=True)
|
||||
@@ -1427,8 +1579,8 @@ os.makedirs(ifcos_build_dir, exist_ok=True)
|
||||
|
||||
cmake_args = [
|
||||
"-DUSE_MMAP=OFF",
|
||||
"-DBUILD_EXAMPLES=OFF",
|
||||
"-DBUILD_SHARED_LIBS=" + OFF_ON[not IFCOPENSHELL_STATIC],
|
||||
f"-DBUILD_EXAMPLES={OFF_ON[ARGS.build_examples]}",
|
||||
"-DBUILD_SHARED_LIBS=" + OFF_ON[ARGS.ifcopenshell_shared],
|
||||
"-DGLTF_SUPPORT=ON",
|
||||
"-DBoost_NO_BOOST_CMAKE=On",
|
||||
"-DCREATE_BUNDLE=On",
|
||||
@@ -1457,7 +1609,7 @@ def get_cmake_args_prefix_path(additional_paths: Sequence[str] = ()) -> list[str
|
||||
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
|
||||
|
||||
|
||||
if "wasm" in flags:
|
||||
if WASM:
|
||||
# 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")
|
||||
@@ -1473,7 +1625,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(f"{DEPS_DIR}/install/occt-{OCCT_VERSION}")
|
||||
cmake_args_prefix_path.append(OCCT_INSTALL_PATH)
|
||||
|
||||
elif "occ" in targets:
|
||||
# We don't support find_package for OCE.
|
||||
@@ -1523,7 +1675,10 @@ if "rocksdb" in targets:
|
||||
)
|
||||
|
||||
if "swig" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/swig-{SWIG_VERSION}")
|
||||
# `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")
|
||||
|
||||
if os.environ.get("QT_DIR"):
|
||||
cmake_args_prefix_path.append(os.environ["QT_DIR"])
|
||||
@@ -1556,6 +1711,42 @@ 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":
|
||||
@@ -1607,8 +1798,7 @@ 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"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
|
||||
"-DUSERSPACE_PYTHON_PREFIX=" + OFF_ON[PYTHON_USER_SITE],
|
||||
],
|
||||
cmake_dir=CMAKE_DIR,
|
||||
cwd=ifcos_build_dir,
|
||||
@@ -1635,7 +1825,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" in flags:
|
||||
if WASM:
|
||||
run(["wasm-strip", so, "-k", "dylink.0"])
|
||||
elif os.path.basename(so).startswith("_ifcopenshell_wrapper"):
|
||||
# TODO: This symbol name depends on the Python version?
|
||||
@@ -1645,7 +1835,7 @@ if "IfcOpenShell-Python" in targets:
|
||||
|
||||
return module_dir
|
||||
|
||||
if "wasm" in flags:
|
||||
if WASM:
|
||||
compile_python_wrapper(
|
||||
run(["pyodide", "config", "get", "python_version"]),
|
||||
run(["pyodide", "config", "get", "python_include_dir"]),
|
||||
|
||||
+5
-2
@@ -28,8 +28,11 @@ 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 `pyodide build`
|
||||
- it will produce a wheel in `IfcOpenShell/dist`
|
||||
- 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`
|
||||
- testing:
|
||||
- ensure you're in pyodide environment
|
||||
- `cd IfcOpenshell/pyodide`
|
||||
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/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()
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
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}"
|
||||
PYODIDE_VERSION=0.29.4
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
# 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.
|
||||
@@ -16,12 +14,14 @@ source .venv/bin/activate
|
||||
|
||||
# Install pyodide cross build environment.
|
||||
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
|
||||
uv pip install "pyodide-build==${PYODIDE_BUILD_VERSION}"
|
||||
uv pip install -r "${SCRIPT_DIR}/requirements.txt"
|
||||
# `uv run` is required, so xbuildenv would skip using `pip`.
|
||||
uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
|
||||
uv run pyodide xbuildenv install-emscripten
|
||||
|
||||
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
|
||||
# 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)
|
||||
[ -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,8 +29,10 @@ 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/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
|
||||
sed -i s/9.9.9/$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.
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package:
|
||||
name: ifcopenshell
|
||||
version: 0.8.0
|
||||
# Placeholder, replaced by build_pyodide.sh with the actual version from VERSION file.
|
||||
version: 9.9.9
|
||||
|
||||
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\.[^.]+\.([^.]+)\.so$")
|
||||
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.serialization\.([^.]+)\.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_[a-z0-9]+(?:_(.+))?\.so$")
|
||||
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_writer_(.+)\.so$")
|
||||
|
||||
|
||||
def schema_key(schema: str) -> tuple[int, str]:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pyodide-build==0.39.0
|
||||
+33
-15
@@ -6,11 +6,6 @@ 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
|
||||
@@ -19,6 +14,15 @@ 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]
|
||||
@@ -63,20 +67,30 @@ select = [
|
||||
#
|
||||
"FA", # future annotations
|
||||
"UP", # pyupgrade
|
||||
"RUF015", # next() > list_comprehension[0]
|
||||
"RUF022", # sort __all__
|
||||
"unnecessary-iterable-allocation-for-first-element",
|
||||
"unsorted-dunder-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 = [
|
||||
"FA100", # Conflicts with Blender using annotations for props definitions.
|
||||
# Conflicts with Blender using annotations for props definitions.
|
||||
"future-rewritable-type-annotation",
|
||||
# Maybe will enable later:
|
||||
"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
|
||||
"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
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
@@ -102,7 +116,9 @@ invalid-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"
|
||||
@@ -171,6 +187,8 @@ 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"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
black==26.3.1
|
||||
ruff==0.16.0
|
||||
poethepoet
|
||||
ty==0.0.63
|
||||
ty==0.0.72
|
||||
gersemi==0.28.0
|
||||
|
||||
@@ -10,7 +10,7 @@ name = "bcf-client"
|
||||
# author = "IfcOpenShell"
|
||||
description = "BCF-XML file handler."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.10"
|
||||
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.select = [
|
||||
"F401", # unused imports
|
||||
lint.extend-select = [
|
||||
"unused-import", # unused imports
|
||||
]
|
||||
|
||||
+17
-34
@@ -42,10 +42,12 @@ endif
|
||||
|
||||
IS_STABLE:=FALSE
|
||||
VERSION:=$(shell cat ../../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_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_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)
|
||||
@@ -67,32 +69,17 @@ endif # def PYVERSION
|
||||
IFCMERGE_VERSION:=2026-04-07
|
||||
|
||||
ifdef PLATFORM
|
||||
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
||||
SUPPORTED_PLATFORMS := linux 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
|
||||
@@ -106,7 +93,7 @@ endif
|
||||
endif # def PLATFORM
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=3e7b739
|
||||
OLD:=ad113e1
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -192,10 +179,8 @@ 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 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).
|
||||
# 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.
|
||||
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
|
||||
@@ -213,8 +198,6 @@ 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
|
||||
@@ -260,14 +243,14 @@ endif
|
||||
|
||||
cp pyproject.toml build/
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
$(SED) "s/0.0.0/$(VERSION)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
|
||||
$(SED) "s/0.0.0/$(VERSION_SEMVER)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
|
||||
else
|
||||
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) "s/0.0.0/$(VERSION_SEMVER_DAILY)/" 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)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
|
||||
endif
|
||||
|
||||
# Blender 5.1+ requires Python 3.13.
|
||||
@@ -279,9 +262,9 @@ endif
|
||||
|
||||
# Provides bonsai Add-on functionality
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
|
||||
else
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
|
||||
endif
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build
|
||||
cp build/dist/*.whl build/wheels/
|
||||
@@ -315,9 +298,9 @@ endif
|
||||
rm -rf build/bonsai/libs/
|
||||
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
else
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-alpha$(VERSION_DATE)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER_DAILY)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
endif
|
||||
|
||||
mv build/bonsai*.zip dist/
|
||||
|
||||
@@ -185,13 +185,10 @@ class IfcStore:
|
||||
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
|
||||
IfcStore.cache_path = cache_path
|
||||
cache_path = Path(IfcStore.cache_path)
|
||||
cache_settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
settings = ifcopenshell.geom.settings()
|
||||
cache_preexists = cache_path.exists()
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
|
||||
IfcStore.cache_path, cache_settings, serializer_settings
|
||||
)
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
|
||||
if cache_preexists:
|
||||
print(f"Successfully loaded existing cache: {cache_path.name}.")
|
||||
else:
|
||||
@@ -206,9 +203,7 @@ class IfcStore:
|
||||
|
||||
os.remove(IfcStore.cache_path)
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
|
||||
IfcStore.cache_path, cache_settings, serializer_settings
|
||||
)
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
|
||||
print("New cache was created.")
|
||||
except Exception as e:
|
||||
print(f"Failed to create a cache: {str(e)}.")
|
||||
|
||||
@@ -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.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
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.TriangulationElement],
|
||||
shape: Union[W.triangulation, W.triangulation_element],
|
||||
) -> bpy.types.Curve:
|
||||
if isinstance(shape, W.TriangulationElement):
|
||||
if isinstance(shape, W.triangulation_element):
|
||||
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.TriangulationElement],
|
||||
shape: Union[W.triangulation, W.triangulation_element],
|
||||
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
|
||||
) -> Union[bpy.types.Mesh, None]:
|
||||
try:
|
||||
if isinstance(shape, W.TriangulationElement):
|
||||
if isinstance(shape, W.triangulation_element):
|
||||
# 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 = (
|
||||
building_elements = list(
|
||||
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.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
shapes[shape.id] = {
|
||||
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
|
||||
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
|
||||
|
||||
@@ -348,10 +348,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
if self.obj_type == "Object":
|
||||
if context.selected_objects:
|
||||
objects = [o.name for o in context.selected_objects]
|
||||
else:
|
||||
objects = [context.active_object.name]
|
||||
objects = [o.name for o in tool.Blender.get_selected_objects()]
|
||||
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): # noqa: N801 — Blender bl_idname convention
|
||||
class BIM_GT_box_face_quad(bpy.types.Gizmo):
|
||||
"""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): # noqa: N801 — Blender bl_idname
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class BIM_GT_box_face_outline(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
|
||||
class BIM_GT_box_face_outline(bpy.types.Gizmo):
|
||||
"""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): # noqa: N801 — Blender bl_idname convention
|
||||
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup):
|
||||
"""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 # noqa: F401
|
||||
import typst # ruff: ignore[unused-import]
|
||||
|
||||
return True
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -313,7 +313,7 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
failures.append(element)
|
||||
print("***** FAILURE *****")
|
||||
if shape:
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
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__ = [ # noqa: RUF022 (unsorted `__all__`)
|
||||
__all__ = [ # ruff: ignore[unsorted-dunder-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: # noqa: ARG002
|
||||
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None:
|
||||
"""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: # noqa: ARG002
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
|
||||
"""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: # noqa: ARG002
|
||||
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float:
|
||||
"""Get Y offset for icons based on view direction.
|
||||
|
||||
Uses get_icon_y_extent() to determine how far to offset icons based on
|
||||
@@ -6546,9 +6546,7 @@ class BaseParametricGizmoGroup:
|
||||
|
||||
self._refresh_element_specific(context, mw, props)
|
||||
|
||||
def _update_dimension_gizmo_positions(
|
||||
self, context: bpy.types.Context, mw: "Matrix", props # noqa: ARG002
|
||||
) -> None:
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
|
||||
"""Update dimension gizmo positions based on view direction.
|
||||
|
||||
Override this method in subclasses to implement view-dependent
|
||||
|
||||
@@ -1406,31 +1406,28 @@ 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.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)
|
||||
self.svg_settings.set("svg-without-storeys", True)
|
||||
self.svg_settings.set("svg-write-poly", True)
|
||||
self.svg_settings.set("svg-poly", True)
|
||||
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
|
||||
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)
|
||||
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)
|
||||
if target_view == "REFLECTED_PLAN_VIEW":
|
||||
self.serialiser_settings.set("svg-mirror-y", True)
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(
|
||||
self.svg_buffer, self.svg_settings, self.serialiser_settings
|
||||
)
|
||||
self.svg_settings.set("svg-mirror-y", True)
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_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()
|
||||
|
||||
@@ -72,11 +72,10 @@ 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)
|
||||
serializer_settings.set("use-element-guids", True)
|
||||
settings.set("use-element-guids", True)
|
||||
settings.set("use-world-coords", True)
|
||||
|
||||
ifc_file: ifcopenshell.file
|
||||
@@ -90,7 +89,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, serializer_settings)
|
||||
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings)
|
||||
serialiser.setFile(ifc_file)
|
||||
serialiser.setUnitNameAndMagnitude("METER", 1.0)
|
||||
serialiser.writeHeader()
|
||||
@@ -107,7 +106,7 @@ class ExportOBJ(bpy.types.Operator):
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
materials = shape.geometry.materials
|
||||
|
||||
for material in materials:
|
||||
|
||||
@@ -430,7 +430,7 @@ class SverchokData:
|
||||
@classmethod
|
||||
def has_sverchok(cls) -> bool:
|
||||
try:
|
||||
import sverchok # noqa: F401
|
||||
import sverchok # ruff: ignore[unused-import]
|
||||
|
||||
return True
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -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]: # noqa: ARG002
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
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]: # noqa: ARG002
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
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]: # noqa: ARG002
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
@@ -909,9 +909,7 @@ 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" # noqa: ARG002
|
||||
) -> None:
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties") -> None:
|
||||
"""Update door-specific swing arc gizmos."""
|
||||
self.update_swing_gizmos(mw, props)
|
||||
|
||||
|
||||
@@ -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: # noqa: ARG002
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw, props) -> None:
|
||||
"""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: # noqa: ARG002
|
||||
def get_element_height(self, props) -> float:
|
||||
"""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
|
||||
|
||||
@@ -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]: # noqa: ARG002
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
@@ -658,9 +658,7 @@ 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" # noqa: ARG002
|
||||
) -> None:
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties") -> 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)
|
||||
@@ -707,7 +705,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" # noqa: ARG002
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties"
|
||||
) -> None:
|
||||
"""Update dimension gizmo positions based on camera view direction."""
|
||||
viewing_from_negative_y, viewing_from_negative_x = self._frame_view_dir
|
||||
|
||||
@@ -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" # noqa: ARG002
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties"
|
||||
) -> 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: # noqa: ARG001
|
||||
def _commit_pending_wall_edits_for_selection(context: bpy.types.Context) -> None:
|
||||
"""Thin wall-scoped alias for ``tool.Parametric.commit_pending_edits_for_selection``.
|
||||
|
||||
Encapsulates the ``names=("wall",)`` filter so the registry name is
|
||||
|
||||
@@ -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]: # noqa: ARG002
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
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.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
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.TriangulationElement) -> None:
|
||||
def process_occurrence(self, shape: W.triangulation_element) -> 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]: # noqa: ARG002
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
props = self._resolve_props(context)
|
||||
if props is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -142,8 +142,35 @@ def assign_material(
|
||||
else:
|
||||
element_material_type = material_type
|
||||
|
||||
ifc.run("material.assign_material", products=[element], type=element_material_type, material=material)
|
||||
assigned_material = material_tool.get_material(element)
|
||||
# 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)
|
||||
assert assigned_material # Type checker.
|
||||
|
||||
if material_tool.is_a_material_set(material):
|
||||
|
||||
@@ -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): pass
|
||||
def get_material(cls, element, should_inherit: bool = False, should_skip_usage: 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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Ignore unused imports.
|
||||
# ruff: noqa: F401
|
||||
# ruff: file-ignore[unused-import]
|
||||
|
||||
from bonsai.tool.aggregate import Aggregate
|
||||
from bonsai.tool.array import Array
|
||||
|
||||
@@ -25,6 +25,7 @@ import importlib
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -1756,6 +1757,7 @@ 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)
|
||||
|
||||
@@ -1187,7 +1187,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
if iterator and iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
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")
|
||||
]
|
||||
|
||||
|
||||
@@ -872,7 +872,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
cls,
|
||||
element: ifcopenshell.entity_instance,
|
||||
representation: ifcopenshell.entity_instance,
|
||||
shape: W.TriangulationElement,
|
||||
shape: W.triangulation_element,
|
||||
) -> 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,
|
||||
*,
|
||||
|
||||
@@ -220,9 +220,14 @@ class Material(bonsai.core.tool.Material):
|
||||
|
||||
@classmethod
|
||||
def get_material(
|
||||
cls, element: ifcopenshell.entity_instance, should_inherit: bool = False
|
||||
cls,
|
||||
element: ifcopenshell.entity_instance,
|
||||
should_inherit: bool = False,
|
||||
should_skip_usage: bool = False,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return ifcopenshell.util.element.get_material(element, should_inherit=should_inherit)
|
||||
return ifcopenshell.util.element.get_material(
|
||||
element, should_inherit=should_inherit, should_skip_usage=should_skip_usage
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_a_material_set(cls, material: ifcopenshell.entity_instance) -> bool:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,6 +76,10 @@ 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):
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ exclude = ["test*"]
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
lint.extend-select = [
|
||||
"F401", # unused imports
|
||||
"unused-import", # unused imports
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
|
||||
@@ -42,6 +42,7 @@ markers =
|
||||
type
|
||||
unit
|
||||
void
|
||||
wall
|
||||
web
|
||||
|
||||
# Provide plugins explicitly, so it will be possible run tests with PYTEST_DISABLE_PLUGIN_AUTOLOAD.
|
||||
|
||||
@@ -45,10 +45,10 @@ for dep in dependencies:
|
||||
subprocess.check_call(command + [dep])
|
||||
|
||||
try:
|
||||
import pygments # noqa: F401
|
||||
import pytest # noqa: F401
|
||||
import pytest_bdd # noqa: F401
|
||||
import pytest_blender # noqa: F401
|
||||
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]
|
||||
|
||||
print("Test dependency installation was successful!")
|
||||
except Exception as e:
|
||||
|
||||
@@ -163,32 +163,29 @@ class Drawer:
|
||||
|
||||
# self.svg_settings.set_deflection_tolerance(0.0001)
|
||||
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
|
||||
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)
|
||||
self.svg_settings.set("svg-without-storeys", True)
|
||||
self.svg_settings.set("svg-write-poly", True)
|
||||
self.svg_settings.set("svg-poly", True)
|
||||
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
|
||||
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)
|
||||
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)
|
||||
if target_view == "REFLECTED_PLAN_VIEW":
|
||||
self.serialiser_settings.set("svg-mirror-y", True)
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(
|
||||
self.svg_buffer, self.svg_settings, self.serialiser_settings
|
||||
)
|
||||
self.svg_settings.set("svg-mirror-y", True)
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_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 "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
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='IfcWallType/Cube', obj_type='Object')"
|
||||
When I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/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 "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
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 "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
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 "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
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='IfcWallType/Cube', obj_type='Object')"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/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 "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
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='IfcWallType/Cube', obj_type='Object')"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/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 "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
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='IfcWallType/Cube', obj_type='Object')"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/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='IfcWallType/Cube', obj_type='Object')"
|
||||
When I press "bim.remove_classification_reference(reference={reference}, obj='IfcWall/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 "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
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='IfcWallType/Cube', obj_type='Object')"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/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,6 +185,7 @@ 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"
|
||||
@@ -213,6 +214,7 @@ 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"
|
||||
@@ -416,6 +418,7 @@ 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"
|
||||
@@ -447,6 +450,7 @@ 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,6 +121,7 @@ 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"
|
||||
@@ -134,6 +135,7 @@ 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"
|
||||
@@ -155,6 +157,7 @@ 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"
|
||||
|
||||
@@ -181,6 +184,7 @@ 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"
|
||||
@@ -200,6 +204,7 @@ 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"
|
||||
@@ -220,6 +225,7 @@ 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"
|
||||
@@ -235,6 +241,7 @@ 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"
|
||||
@@ -248,6 +255,7 @@ 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"
|
||||
@@ -267,6 +275,7 @@ 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"
|
||||
@@ -286,6 +295,7 @@ 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"
|
||||
@@ -306,6 +316,7 @@ 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"
|
||||
@@ -454,6 +465,7 @@ 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"
|
||||
@@ -477,6 +489,7 @@ 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"
|
||||
|
||||
|
||||
@@ -314,6 +314,12 @@ 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"
|
||||
@@ -326,13 +332,19 @@ 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" is at "13,4,-1"
|
||||
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/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
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/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
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/K" is at "10,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations - automatic false origin mode
|
||||
@@ -342,24 +354,27 @@ 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 "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 model origin is on an object vertex
|
||||
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 "-6,-1,1"
|
||||
And the object "IfcActuator/B" is at "-7,-3,1"
|
||||
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/C" is at "0,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"
|
||||
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"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations - manual false origin mode
|
||||
Given an empty Blender session
|
||||
@@ -379,23 +394,20 @@ 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" is at "3,4,-1"
|
||||
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/E" is at "-4,3,0"
|
||||
And the object "IfcActuator/F" is at "-7,3,0"
|
||||
And the object "IfcActuator/G" is at "5,6,-1"
|
||||
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/H" is at "-1,2,0"
|
||||
And the object "IfcActuator/I" is at "-7,3,0"
|
||||
And the object "IfcActuator/J" is at "1,3,-1"
|
||||
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/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
|
||||
@@ -410,13 +422,19 @@ 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" is at "11.522,17.228,-1"
|
||||
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/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" is at "12.936,19.678,-1"
|
||||
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/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" is at "9.8487,15.745,-1"
|
||||
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/K" is at "9.6593,12.588,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - automatic false origin mode
|
||||
@@ -436,13 +454,19 @@ 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" is at "13,4,-1"
|
||||
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/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
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/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
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/K" is at "10,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - manual false origin mode
|
||||
@@ -463,23 +487,20 @@ 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" is at "11.522,7.228,-1"
|
||||
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/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" is at "12.936,9.678,-1"
|
||||
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/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" is at "9.8487,5.745,-1"
|
||||
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/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
|
||||
@@ -500,13 +521,19 @@ 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" is at "13,4,-1"
|
||||
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/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
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/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
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/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)
|
||||
@@ -522,13 +549,19 @@ 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" is at "13,4,-1"
|
||||
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/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
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/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
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/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)
|
||||
@@ -538,24 +571,27 @@ 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 "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 model origin is on an object vertex
|
||||
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 "-6,-1,1"
|
||||
And the object "IfcActuator/B" is at "-7,-3,1"
|
||||
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/C" is at "0,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"
|
||||
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"
|
||||
|
||||
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
|
||||
@@ -575,23 +611,20 @@ 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" is at "3,4,-1"
|
||||
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/E" is at "-4,3,0"
|
||||
And the object "IfcActuator/F" is at "-7,3,0"
|
||||
And the object "IfcActuator/G" is at "5,6,-1"
|
||||
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/H" is at "-1,2,0"
|
||||
And the object "IfcActuator/I" is at "-7,3,0"
|
||||
And the object "IfcActuator/J" is at "1,3,-1"
|
||||
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/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
|
||||
@@ -606,13 +639,19 @@ 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" is at "11.522,17.228,-1"
|
||||
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/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" is at "12.936,19.678,-1"
|
||||
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/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" is at "9.8487,15.745,-1"
|
||||
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/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
|
||||
@@ -632,13 +671,19 @@ 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" is at "13,4,-1"
|
||||
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/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
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/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
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/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
|
||||
@@ -659,23 +704,20 @@ 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" is at "11.522,7.228,-1"
|
||||
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/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" is at "12.936,9.678,-1"
|
||||
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/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" is at "9.8487,5.745,-1"
|
||||
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/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
|
||||
|
||||
@@ -81,6 +81,7 @@ 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()"
|
||||
@@ -102,6 +103,7 @@ 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()"
|
||||
@@ -125,6 +127,7 @@ 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
|
||||
@@ -180,6 +183,7 @@ 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" # noqa: E731
|
||||
formatter = lambda props, value: f"{value:.2f}m"
|
||||
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}" # noqa: E731
|
||||
formatter = lambda props, value: f"{props.label}={value}"
|
||||
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: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
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: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
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: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
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: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
|
||||
continue
|
||||
if result:
|
||||
|
||||
@@ -36,6 +36,7 @@ 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
|
||||
@@ -1000,6 +1001,7 @@ 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)
|
||||
@@ -1680,6 +1682,111 @@ 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,5 +1,5 @@
|
||||
[tool.ruff]
|
||||
extend = "../pyproject.toml"
|
||||
lint.ignore = [
|
||||
"F401", # unused imports
|
||||
"unused-import", # unused imports
|
||||
]
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -428,7 +428,8 @@ 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_, this);
|
||||
models_view_ = new modules::models::ModelsPanelView(
|
||||
models_panel_, session_state_, viewport_widget_->viewport(), this);
|
||||
spatial_view_ = new modules::spatial_hierarchy::SpatialHierarchyPanelView(spatial_panel_, session_state_, this);
|
||||
properties_view_ = new modules::properties::PropertiesPanelView(properties_panel_, session_state_, this);
|
||||
|
||||
@@ -481,6 +482,13 @@ 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);
|
||||
@@ -489,6 +497,7 @@ 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_);
|
||||
|
||||
@@ -554,16 +563,59 @@ 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;
|
||||
status_perf_label_->setText(
|
||||
QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws")
|
||||
QString text =
|
||||
QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws | VRAM %8/%9 MB")
|
||||
.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(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);
|
||||
});
|
||||
connect(viewport_widget_->viewport(), &ViewportWindow::objectPicked,
|
||||
this, [this](uint32_t object_id) {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <QStringList>
|
||||
|
||||
class QLabel;
|
||||
#include <QElapsedTimer>
|
||||
class QDockWidget;
|
||||
class QMenu;
|
||||
class QProgressBar;
|
||||
@@ -70,6 +71,10 @@ 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;
|
||||
|
||||
@@ -199,6 +199,10 @@ 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);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ 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();
|
||||
@@ -107,6 +108,10 @@ 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 ``IfcGeom::Iterator`` on a worker thread for raw IFC loads. It emits a
|
||||
Runs ``ifcopenshell::geom::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.
|
||||
|
||||
|
||||
@@ -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,6 +262,28 @@ 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
|
||||
@@ -685,7 +707,7 @@ void convertIfcToDatabase(SessionState& session, QWidget& host) {
|
||||
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
|
||||
}
|
||||
|
||||
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
|
||||
std::shared_ptr<ifcopenshell::geom::serializer> serializer = registry.create("rdb", context);
|
||||
serializer->finalize();
|
||||
} catch (const std::exception& e) {
|
||||
*error_message = QString::fromUtf8(e.what());
|
||||
@@ -791,7 +813,7 @@ void exportGeometryDatabase(SessionState& session, QWidget& host) {
|
||||
throw ifcopenshell::exception("RDB serializer does not support streaming from an input filename");
|
||||
}
|
||||
|
||||
boost::shared_ptr<Serializer> serializer = registry.create("rdb", context);
|
||||
std::shared_ptr<ifcopenshell::geom::serializer> serializer = registry.create("rdb", context);
|
||||
serializer->finalize();
|
||||
serializer.reset();
|
||||
|
||||
|
||||
@@ -60,6 +60,12 @@ 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,16 +25,25 @@
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QFont>
|
||||
#include <QColor>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
|
||||
QStandardItem* siblingItem(QStandardItem* name_item, Column column) {
|
||||
QStandardItem* parent = name_item->parent();
|
||||
if (!parent) parent = name_item->model()->invisibleRootItem();
|
||||
return parent->child(name_item->row(), 1);
|
||||
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);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
@@ -51,7 +60,7 @@ FederationItemModel::FederationItemModel(Federation* federation, QObject* parent
|
||||
: QStandardItemModel(parent)
|
||||
, federation_(federation)
|
||||
{
|
||||
setColumnCount(2);
|
||||
setColumnCount(ColumnCount);
|
||||
rebuildAll();
|
||||
|
||||
connect(federation_, &Federation::groupAdded, this, &FederationItemModel::onGroupAdded);
|
||||
@@ -67,7 +76,7 @@ FederationItemModel::FederationItemModel(Federation* federation, QObject* parent
|
||||
|
||||
void FederationItemModel::rebuildAll() {
|
||||
clear();
|
||||
setColumnCount(2);
|
||||
setColumnCount(ColumnCount);
|
||||
id_to_name_item_.clear();
|
||||
|
||||
for (const auto& root_group : federation_->rootGroups()) {
|
||||
@@ -121,6 +130,14 @@ 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) {
|
||||
@@ -133,6 +150,22 @@ 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);
|
||||
}
|
||||
@@ -148,7 +181,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, vis_item});
|
||||
parent_item->appendRow({name_item, makeMemoryItem(), vis_item});
|
||||
id_to_name_item_.insert(model_id, name_item);
|
||||
styleRowVisibility(name_item, federation_->isModelEffectivelyVisible(model_id));
|
||||
}
|
||||
@@ -158,7 +191,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, vis_item});
|
||||
parent_item->appendRow({name_item, makeMemoryItem(), 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
|
||||
// two columns: name + visibility icon). Subscribes directly to Federation's
|
||||
// three columns: name, GPU memory, 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,6 +57,12 @@ 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);
|
||||
@@ -72,6 +78,7 @@ 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;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDrag>
|
||||
@@ -79,6 +80,7 @@ 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
|
||||
@@ -250,7 +252,7 @@ ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
|
||||
|
||||
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
|
||||
if (!index.isValid()) return;
|
||||
if (index.column() == 1) {
|
||||
if (index.column() == VisibilityColumn) {
|
||||
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
|
||||
return;
|
||||
}
|
||||
@@ -380,6 +382,24 @@ 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");
|
||||
@@ -409,14 +429,16 @@ void ModelsPanel::setModel(FederationItemModel* model) {
|
||||
}
|
||||
|
||||
void ModelsPanel::applyColumnLayout() {
|
||||
// Column 0 (name) stretches to fill; column 1 (visibility icon) is fixed.
|
||||
// The name stretches to fill; memory and visibility are fixed.
|
||||
QHeaderView* header = tree_->header();
|
||||
if (header->count() < 2) return;
|
||||
if (header->count() < ColumnCount) return;
|
||||
header->setStretchLastSection(false);
|
||||
header->setMinimumSectionSize(kVisibilityColumnWidth);
|
||||
header->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
header->setSectionResizeMode(1, QHeaderView::Fixed);
|
||||
header->resizeSection(1, kVisibilityColumnWidth);
|
||||
header->setSectionResizeMode(NameColumn, QHeaderView::Stretch);
|
||||
header->setSectionResizeMode(MemoryColumn, QHeaderView::Fixed);
|
||||
header->resizeSection(MemoryColumn, kMemoryColumnWidth);
|
||||
header->setSectionResizeMode(VisibilityColumn, QHeaderView::Fixed);
|
||||
header->resizeSection(VisibilityColumn, 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 attribute_value& av) {
|
||||
std::string enumString(const ifcopenshell::attribute_value& av) {
|
||||
if (av.isNull()) return {};
|
||||
if (av.type() != ifcopenshell::Argument_ENUMERATION) return {};
|
||||
enumeration_reference enumeration = av;
|
||||
ifcopenshell::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"));
|
||||
|
||||
@@ -31,6 +31,14 @@ 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;
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
@@ -54,17 +57,19 @@ 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(); };
|
||||
auto rebuild = [this]() { model_->rebuildAll(); refreshResidency(); };
|
||||
connect(session_state_, &SessionState::projectReset, this, rebuild);
|
||||
connect(session_state_, &SessionState::projectOpened, this, rebuild);
|
||||
connect(&bonsaiviewer::ViewerSettings::instance(),
|
||||
@@ -73,6 +78,30 @@ 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
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <QObject>
|
||||
|
||||
class Federation;
|
||||
class ViewportWindow;
|
||||
namespace bonsaiviewer { class SessionState; }
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
@@ -45,16 +46,25 @@ QList<GroupOption> validMoveTargets(const Federation& federation,
|
||||
// coarse session signals (project open/reset, theme change) — those are the
|
||||
// "rebuild from scratch" cases the model itself doesn't subscribe to.
|
||||
// Granular Federation events are handled inside the model.
|
||||
//
|
||||
// Also the bridge for the one thing the tree shows that is not Federation
|
||||
// state: each model's GPU residency (memory column, unloaded styling). The
|
||||
// viewport owns that state, so this view polls it once a second — the
|
||||
// numbers move continuously while geometry streams — and pushes it in.
|
||||
class ModelsPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ModelsPanelView(ModelsPanel* widget,
|
||||
bonsaiviewer::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void refreshResidency();
|
||||
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
bonsaiviewer::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
FederationItemModel* model_ = nullptr;
|
||||
};
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
|
||||
state.entity = {"No item selected", ""};
|
||||
|
||||
auto entity = registry ? registry->findEntity(object_id)
|
||||
: std::optional<express::Base>{};
|
||||
: std::optional<express::base>{};
|
||||
if (entity) {
|
||||
state.entity.entity_class = QString::fromStdString(entity->declaration().name());
|
||||
if (auto predefined_type = get_predefined_type(*entity)) {
|
||||
@@ -121,16 +121,16 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
|
||||
}
|
||||
// Relationships: the construction type and the spatial container, shown
|
||||
// by name (falling back to the entity class when unnamed).
|
||||
auto display_name = [](const express::Base& related) -> QString {
|
||||
auto display_name = [](const express::base& related) -> QString {
|
||||
if (auto name = get_string_attribute(related, "Name"); name && !name->empty()) {
|
||||
return QString::fromStdString(*name);
|
||||
}
|
||||
return QString::fromStdString(related.declaration().name());
|
||||
};
|
||||
if (express::Base type = get_type(*entity)) {
|
||||
if (express::base type = get_type(*entity)) {
|
||||
state.relationships.append({"Type", display_name(type)});
|
||||
}
|
||||
if (express::Base container = get_container(*entity)) {
|
||||
if (express::base container = get_container(*entity)) {
|
||||
state.relationships.append({"Container", display_name(container)});
|
||||
}
|
||||
// Property sets (Pset_*) and quantity sets (Qto_* / BaseQuantities),
|
||||
|
||||
@@ -60,7 +60,7 @@ TreeNode* findNodeRecursive(QList<TreeNode>& nodes, const NodePath& path, int de
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ItemKind kindOf(const express::Base& element) {
|
||||
ItemKind kindOf(const express::base& element) {
|
||||
const auto& declaration = element.declaration();
|
||||
if (declaration.is("IfcSite")) return ItemKind::Site;
|
||||
if (declaration.is("IfcBuilding")) return ItemKind::Building;
|
||||
@@ -68,14 +68,14 @@ ItemKind kindOf(const express::Base& element) {
|
||||
return ItemKind::Space; // IfcSpace, IfcSpatialZone, …
|
||||
}
|
||||
|
||||
QString displayName(const express::Base& element) {
|
||||
QString displayName(const express::base& element) {
|
||||
if (auto name = get_string_attribute(element, "Name"); name && !name->empty()) {
|
||||
return QString::fromStdString(*name);
|
||||
}
|
||||
return QString::fromStdString(element.declaration().name());
|
||||
}
|
||||
|
||||
TreeNode buildNode(const express::Base& element) {
|
||||
TreeNode buildNode(const express::base& element) {
|
||||
TreeNode node;
|
||||
node.name = displayName(element);
|
||||
node.kind = kindOf(element);
|
||||
|
||||
@@ -46,6 +46,6 @@ py-modules = ["bsdd","bsdd_json","type_hints"]
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
lint.select = [
|
||||
"F401", # unused imports
|
||||
lint.extend-select = [
|
||||
"unused-import", # unused imports
|
||||
]
|
||||
|
||||
+9
-6
@@ -3,7 +3,10 @@ IS_STABLE:=FALSE
|
||||
PYTHON:=python3
|
||||
PIP:=pip3
|
||||
VERSION:=$(shell cat ../../VERSION)
|
||||
VERSION_BASE:=$(shell sed -E 's/[[:alpha:]]+[0-9]+$$//' ../../VERSION)
|
||||
VERSION_PYTHON:=$(shell sed 's/alpha/a/' ../../VERSION)
|
||||
VERSION_DATE:=$(shell date '+%y%m%d')
|
||||
VERSION_DAILY:=$(VERSION_BASE)a$(VERSION_DATE)
|
||||
SED:=sed -i
|
||||
VENV_BIN:=bin
|
||||
|
||||
@@ -30,18 +33,18 @@ dist:
|
||||
cp pyproject.toml build/
|
||||
if [ -f README.md ]; then cp README.md build/; fi
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
|
||||
ifdef IS_MODULE
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/$(PACKAGE_NAME)
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/$(PACKAGE_NAME)
|
||||
else
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/$(PACKAGE_NAME)/__init__.py
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/$(PACKAGE_NAME)/__init__.py
|
||||
endif
|
||||
else
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
|
||||
ifdef IS_MODULE
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/$(PACKAGE_NAME)
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/$(PACKAGE_NAME)
|
||||
else
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/$(PACKAGE_NAME)/__init__.py
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/$(PACKAGE_NAME)/__init__.py
|
||||
endif
|
||||
endif
|
||||
cd build && $(PYTHON) -m venv env && . env/$(VENV_ACTIVATE) && $(PIP) install build
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user