mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-27 15:26:46 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e084f2830 | |||
| dd5bd58916 | |||
| c3abe0b3c7 | |||
| 7017d5400d | |||
| fb3cd09d6d | |||
| 332435416a | |||
| f0c6de4bdf | |||
| 665c5fa77e | |||
| de13379162 | |||
| 3b16356181 | |||
| 44860cd615 | |||
| 5651cd6494 | |||
| 29b9d8807e | |||
| 37f557b4b5 | |||
| 881fb10fe6 | |||
| 1949adda44 | |||
| 87193ac323 | |||
| c34a6ddac6 | |||
| 92cc601a85 | |||
| adf01be1d0 | |||
| c8f8196b09 | |||
| cb22dd7a43 | |||
| 243f13de09 | |||
| 0d5c9a0ce2 | |||
| ea6f03409f | |||
| 21b4cd2403 | |||
| a543022bba | |||
| 6f9d5c4005 | |||
| a7b6c66f77 | |||
| 6dc671f24d | |||
| 9b28444255 | |||
| 7c04a0d533 | |||
| 02126a8d82 | |||
| c9fcabef65 | |||
| 99c89c3f44 | |||
| b78396051d | |||
| 6715e684a8 | |||
| 1695571256 | |||
| 4f0e572e0f |
@@ -7,7 +7,7 @@ body:
|
||||
label: Bug Description
|
||||
placeholder: |
|
||||
Describe what problem occurred and what you expected to happen instead.
|
||||
|
||||
|
||||
1. To reproduce this, open file '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
|
||||
@@ -1,383 +0,0 @@
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "pytest",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""Check (and by default fix) whitespace issues in tracked source files:
|
||||
- stray CR, e.g. 'hello\\rworld' -> 'helloworld'
|
||||
- line ending mismatch, e.g. 'hello\\r\\n' -> 'hello\\n' (or vice versa)
|
||||
- missing newline at end of file
|
||||
- extra newline(s) at end of file
|
||||
- trailing whitespace at end of line
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO, Literal, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class C:
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
CR = b"\r"
|
||||
CRLF = b"\r\n"
|
||||
LF = b"\n"
|
||||
|
||||
LineSeparator = Literal[b"\r\n", b"\n"]
|
||||
SYSTEM_LINE_SEPARATOR = cast(LineSeparator, os.linesep.encode())
|
||||
|
||||
|
||||
class Checker:
|
||||
def __init__(self, newline: LineSeparator = SYSTEM_LINE_SEPARATOR) -> None:
|
||||
self.newline = newline
|
||||
self.issues = 0
|
||||
|
||||
def report(self, label: str, issue: str) -> None:
|
||||
self.issues += 1
|
||||
print(f"{label}: {C.RED}{issue}{C.RESET}")
|
||||
|
||||
def check_stray_cr(self, filepath: Path, check: bool) -> None:
|
||||
with filepath.open("r+b") as f:
|
||||
self._check_stray_cr(f, str(filepath), check)
|
||||
|
||||
def _check_stray_cr(self, f: BinaryIO, label: str, check: bool) -> None:
|
||||
# a CR is "stray" if it isn't immediately followed by a LF, i.e. not part of a CRLF pair
|
||||
# CRLF/CR mismatch will be reported separately.
|
||||
stray_cr = re.compile(rb"\r(?!\n)")
|
||||
|
||||
content = f.read()
|
||||
matches = list(stray_cr.finditer(content))
|
||||
if not matches:
|
||||
return
|
||||
|
||||
line_numbers = dict.fromkeys(content.count(b"\n", 0, m.start()) + 1 for m in matches)
|
||||
for line_number in line_numbers:
|
||||
self.report(f"{label}:{line_number}", "stray carriage return")
|
||||
if check:
|
||||
return
|
||||
|
||||
f.seek(0)
|
||||
f.write(stray_cr.sub(b"", content))
|
||||
f.truncate()
|
||||
|
||||
def check_line_endings_mismatch(self, filepath: Path, check: bool) -> None:
|
||||
with filepath.open("r+b") as f:
|
||||
self._check_line_endings_mismatch(f, str(filepath), check)
|
||||
|
||||
def _check_line_endings_mismatch(self, f: BinaryIO, label: str, check: bool) -> None:
|
||||
NEWLINE = self.newline
|
||||
|
||||
def get_line_ending(line: bytes) -> LineSeparator | None:
|
||||
if line.endswith(CRLF):
|
||||
return CRLF
|
||||
if line.endswith(LF):
|
||||
return LF
|
||||
# last line with no trailing newline at all; check_eof_newline handles that
|
||||
return None
|
||||
|
||||
changed = False
|
||||
fixed_lines = []
|
||||
for line_number, line in enumerate(f, start=1):
|
||||
found = get_line_ending(line)
|
||||
if found in (NEWLINE, None):
|
||||
fixed_lines.append(line)
|
||||
continue
|
||||
|
||||
self.report(f"{label}:{line_number}", f"line ending mismatch (expected {NEWLINE!r}, found {found!r})")
|
||||
changed = True
|
||||
content = line[: -len(found)]
|
||||
fixed_lines.append(content + NEWLINE)
|
||||
|
||||
if changed and not check:
|
||||
f.seek(0)
|
||||
f.write(b"".join(fixed_lines))
|
||||
f.truncate()
|
||||
|
||||
def check_eof_newline(self, filepath: Path, check: bool) -> None:
|
||||
with filepath.open("r+b") as f:
|
||||
self._check_eof_newline(f, str(filepath), check)
|
||||
|
||||
def _check_eof_newline(self, f: BinaryIO, label: str, check: bool) -> None:
|
||||
NEWLINE = self.newline
|
||||
NEWLINE_SIZE = len(NEWLINE)
|
||||
|
||||
size = f.seek(0, os.SEEK_END)
|
||||
if size == 0:
|
||||
return
|
||||
|
||||
trailing_newlines = 0
|
||||
while True:
|
||||
pos = f.seek((-trailing_newlines - 1) * NEWLINE_SIZE, os.SEEK_END)
|
||||
if f.read(NEWLINE_SIZE) != NEWLINE:
|
||||
break
|
||||
trailing_newlines += 1
|
||||
if pos == 0:
|
||||
break
|
||||
|
||||
if trailing_newlines == 0:
|
||||
self.report(label, "missing newline at end of file")
|
||||
if check:
|
||||
return
|
||||
f.seek(0, os.SEEK_END)
|
||||
f.write(NEWLINE)
|
||||
elif trailing_newlines > 1:
|
||||
self.report(label, f"{trailing_newlines} trailing newlines at end of file")
|
||||
if check:
|
||||
return
|
||||
f.truncate(size - (trailing_newlines - 1) * NEWLINE_SIZE)
|
||||
|
||||
def check_trailing_whitespaces(self, filepath: Path, check: bool) -> None:
|
||||
with filepath.open("r+b") as f:
|
||||
self._check_trailing_whitespaces(f, str(filepath), check)
|
||||
|
||||
def _check_trailing_whitespaces(self, f: BinaryIO, label: str, check: bool) -> None:
|
||||
NEWLINE = self.newline
|
||||
NEWLINE_SIZE = len(NEWLINE)
|
||||
|
||||
changed = False
|
||||
fixed_lines = []
|
||||
for line_number, line in enumerate(f, start=1):
|
||||
has_newline = line.endswith(NEWLINE)
|
||||
content = line[:-NEWLINE_SIZE] if has_newline else line
|
||||
stripped = content.rstrip()
|
||||
if stripped != content:
|
||||
self.report(f"{label}:{line_number}", "trailing whitespace")
|
||||
changed = True
|
||||
fixed_lines.append(stripped + (NEWLINE if has_newline else b""))
|
||||
|
||||
if changed and not check:
|
||||
f.seek(0)
|
||||
f.write(b"".join(fixed_lines))
|
||||
f.truncate()
|
||||
|
||||
|
||||
CheckMethod = Callable[[Checker, BinaryIO, str, bool], None]
|
||||
|
||||
|
||||
class TestChecker:
|
||||
def _assert_check(
|
||||
self,
|
||||
method: CheckMethod,
|
||||
content: bytes,
|
||||
expected_issues: int,
|
||||
fixed: bytes,
|
||||
check: bool,
|
||||
line_ending: LineSeparator,
|
||||
*,
|
||||
transform: bool = True,
|
||||
) -> None:
|
||||
checker = Checker(line_ending)
|
||||
if line_ending == CRLF and transform:
|
||||
content = content.replace(LF, CRLF)
|
||||
fixed = fixed.replace(LF, CRLF)
|
||||
buffer = io.BytesIO(content)
|
||||
method(checker, buffer, "test", check)
|
||||
assert buffer.getvalue() == (content if check else fixed)
|
||||
assert checker.issues == expected_issues
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected_issues", "fixed"),
|
||||
(
|
||||
# OK
|
||||
(b"", 0, b""),
|
||||
(b"hello\n", 0, b"hello\n"),
|
||||
(b"line1\r\nline2\n", 0, b"line1\r\nline2\n"),
|
||||
# ERR
|
||||
(b"hello\rworld\n", 1, b"helloworld\n"),
|
||||
(b"a\rb\rc\n", 1, b"abc\n"),
|
||||
(b"hello\r", 1, b"hello"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("check", [False, True])
|
||||
def test_check_stray_cr(self, content: bytes, expected_issues: int, fixed: bytes, check: bool) -> None:
|
||||
# Don't parametrize by line endings, since in this case it doesn't matter.
|
||||
self._assert_check(Checker._check_stray_cr, content, expected_issues, fixed, check, LF)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected_issues", "fixed", "line_ending"),
|
||||
(
|
||||
# OK
|
||||
(b"", 0, b"", LF),
|
||||
(b"hello\n", 0, b"hello\n", LF),
|
||||
(b"hello\r\n", 0, b"hello\r\n", CRLF),
|
||||
# ERR
|
||||
(b"hello\r\n", 1, b"hello\n", LF),
|
||||
(b"a\nb\r\nc\n", 1, b"a\nb\nc\n", LF),
|
||||
(b"a\r\nb\r\n", 2, b"a\nb\n", LF),
|
||||
(b"hello\n", 1, b"hello\r\n", CRLF),
|
||||
(b"a\r\nb\nc\r\n", 1, b"a\r\nb\r\nc\r\n", CRLF),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("check", [False, True])
|
||||
def test_check_line_endings_mismatch(
|
||||
self, content: bytes, expected_issues: int, fixed: bytes, line_ending: LineSeparator, check: bool
|
||||
) -> None:
|
||||
self._assert_check(
|
||||
Checker._check_line_endings_mismatch, content, expected_issues, fixed, check, line_ending, transform=False
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected_issues", "fixed"),
|
||||
(
|
||||
# OK
|
||||
(b"", 0, b""),
|
||||
(b"hello\n", 0, b"hello\n"),
|
||||
# ERR
|
||||
(b"hello", 1, b"hello\n"),
|
||||
(b"hello\n\n\n", 1, b"hello\n"),
|
||||
(b"\n\n\n", 1, b"\n"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("check", [False, True])
|
||||
@pytest.mark.parametrize("line_ending", [LF, CRLF])
|
||||
def test_check_eof_newline(
|
||||
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
|
||||
) -> None:
|
||||
self._assert_check(Checker._check_eof_newline, content, expected_issues, fixed, check, line_ending)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected_issues", "fixed"),
|
||||
(
|
||||
# OK
|
||||
(b"", 0, b""),
|
||||
(b"hello\n", 0, b"hello\n"),
|
||||
(b"hello", 0, b"hello"),
|
||||
# ERR
|
||||
(b" ", 1, b""),
|
||||
(b"hello ", 1, b"hello"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("check", [False, True])
|
||||
@pytest.mark.parametrize("line_ending", [LF, CRLF])
|
||||
def test_check_trailing_whitespaces(
|
||||
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
|
||||
) -> None:
|
||||
self._assert_check(Checker._check_trailing_whitespaces, content, expected_issues, fixed, check, line_ending)
|
||||
|
||||
@staticmethod
|
||||
def run_tests(extra_args: list[str] | None = None) -> None:
|
||||
pytest.main([__file__, *(extra_args or [])])
|
||||
|
||||
|
||||
def existing_path(value: str) -> Path:
|
||||
path = Path(value)
|
||||
if not path.exists():
|
||||
raise argparse.ArgumentTypeError(f"path not found: {value}")
|
||||
return path
|
||||
|
||||
|
||||
# Python files are covered by `black`.
|
||||
PATTERNS = (
|
||||
"*.cpp",
|
||||
"*.h",
|
||||
"*.i",
|
||||
"*.cmake",
|
||||
"*/CMakeLists.txt",
|
||||
"*.yml",
|
||||
)
|
||||
|
||||
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@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
|
||||
@@ -135,7 +135,79 @@ jobs:
|
||||
|
||||
- name: Package .zip archives
|
||||
run: |
|
||||
uv run nix/package-zip-archives.py "macos${{ matrix.oldarch }}64"
|
||||
VERSION=v`cat VERSION`
|
||||
# packaging/build.py stages the connector binary + connector.json
|
||||
# into dist/autodesk/; the .app loop below copies that folder into
|
||||
# the bundle. Same on-disk shape as the Linux and Windows builds.
|
||||
uv run src/bonsaiviewer-autodesk/packaging/build.py
|
||||
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
|
||||
test -d "$autodesk_connector_dir"
|
||||
|
||||
cd ./build/`uname`/*/10.15/install/ifcopenshell
|
||||
mkdir -p ~/output
|
||||
install_root="$PWD"
|
||||
|
||||
stage_runtime_payload() {
|
||||
dest="$1"
|
||||
while IFS= read -r runtime_file; do
|
||||
cp -L "$runtime_file" "$dest/"
|
||||
done < <(
|
||||
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
|
||||
[ -d "$runtime_dir" ] || continue
|
||||
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
|
||||
py_version_major=python-${numbers}$postfix
|
||||
pushd . > /dev/null
|
||||
cd $py_version
|
||||
if [ ! -d ifcopenshell ]; then
|
||||
mkdir ../ifcopenshell_
|
||||
mv * ../ifcopenshell_
|
||||
mv ../ifcopenshell_ ifcopenshell
|
||||
fi
|
||||
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
|
||||
find ifcopenshell -name "*.pyc" -delete
|
||||
stage_runtime_payload ifcopenshell
|
||||
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell
|
||||
mv *.zip ~/output
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
|
||||
exe=`basename "$exe_path"`
|
||||
package_dir="$install_root/.package-${exe}"
|
||||
rm -rf "$package_dir"
|
||||
mkdir -p "$package_dir"
|
||||
cp "$exe_path" "$package_dir/"
|
||||
stage_runtime_payload "$package_dir"
|
||||
pushd "$package_dir" > /dev/null
|
||||
zip -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" .
|
||||
popd > /dev/null
|
||||
rm -rf "$package_dir"
|
||||
done
|
||||
|
||||
# .app bundles (e.g. BonsaiViewer.app) live at the install-prefix
|
||||
# root because their install rule uses `BUNDLE DESTINATION "."` —
|
||||
# that's the layout Qt's macdeployqt expects. macdeployqt has
|
||||
# already embedded the Qt frameworks inside each bundle during
|
||||
# install/strip, so the only thing left to stage is the connector.
|
||||
find "$install_root" -maxdepth 1 -type d -name "*.app" | while read app_path; do
|
||||
app=`basename "$app_path" .app`
|
||||
if [ "$app" = "BonsaiViewer" ]; then
|
||||
# ConnectorDiscovery looks in applicationDirPath()/connectors,
|
||||
# which for a bundle is Contents/MacOS.
|
||||
mkdir -p "$app_path/Contents/MacOS/connectors"
|
||||
cp -a "$autodesk_connector_dir" "$app_path/Contents/MacOS/connectors/"
|
||||
fi
|
||||
pushd "$install_root" > /dev/null
|
||||
zip -qq -r "$HOME/output/${app}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" "$(basename "$app_path")"
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3" ADD_COMMIT_SHA=1 BUILD_CFG=Release BUILD_BONSAIVIEWER=ON \
|
||||
uv run --with aqtinstall ./nix/build-all.py \
|
||||
-v --diskcleanup --ifcopenshell-shared --occt-shared 2>&1 \
|
||||
-v --diskcleanup --ifcopenshell-shared 2>&1 \
|
||||
| tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
@@ -110,7 +110,170 @@ jobs:
|
||||
- name: Package .zip archives
|
||||
shell: bash
|
||||
run: |
|
||||
uv run nix/package-zip-archives.py linux64 --occt-shared
|
||||
VERSION=v`cat VERSION`
|
||||
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
|
||||
# invokes `cargo build --release` and stages the binary +
|
||||
# connector.json into dist/autodesk/. Same on-disk shape as the
|
||||
# old PyInstaller flow so the symlink + zip steps below
|
||||
# continue to work unchanged.
|
||||
uv run src/bonsaiviewer-autodesk/packaging/build.py
|
||||
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
|
||||
test -d "$autodesk_connector_dir"
|
||||
|
||||
cd ./build/`uname`/*/install/ifcopenshell
|
||||
mkdir -p ~/output
|
||||
install_root="$PWD"
|
||||
QT6_VERSION="${QT6_VERSION:-6.8.3}"
|
||||
|
||||
if [ -z "${QT_DIR:-}" ]; then
|
||||
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
|
||||
if [ -d "$qt_candidate/lib" ]; then
|
||||
QT_DIR="$qt_candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Ensure that all shared libraries in provided dest `$1`
|
||||
# are present using their SONAMEs (at least as symlinks).
|
||||
ensure_soname_links() {
|
||||
dest="$1"
|
||||
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
|
||||
# TODO: actual pattern is "Library soname" instead of "Shared library"?
|
||||
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
|
||||
[ -n "$soname" ] || continue
|
||||
[ -e "$dest/$soname" ] && continue
|
||||
ln -s "$(basename "$shared_object")" "$dest/$soname"
|
||||
done
|
||||
}
|
||||
|
||||
# Copy all libs from `install/ifcopenshell` to the provided `$1`.
|
||||
# Set `$2` to `0` to skip including geometry writers.
|
||||
stage_runtime_payload() {
|
||||
dest="$1"
|
||||
include_geometry_writers="${2:-1}"
|
||||
while IFS= read -r runtime_file; do
|
||||
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
|
||||
continue
|
||||
fi
|
||||
cp -P "$runtime_file" "$dest/"
|
||||
done < <(
|
||||
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
|
||||
[ -d "$runtime_dir" ] || continue
|
||||
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
|
||||
done
|
||||
)
|
||||
ensure_soname_links "$dest"
|
||||
}
|
||||
|
||||
# Copy all libs from `QT_DIR` to the provided `$2`.
|
||||
stage_qt_runtime_payload() {
|
||||
exe_path="$1"
|
||||
dest="$2"
|
||||
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
|
||||
|
||||
# Skip executables that don't depend on QT (don't have `libQt6` referenced).
|
||||
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Copy all QT libs to `dest`.
|
||||
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
|
||||
ensure_soname_links "$dest"
|
||||
|
||||
# Copy QT plugins.
|
||||
if [ -d "$QT_DIR/plugins" ]; then
|
||||
pushd "$QT_DIR/plugins" > /dev/null
|
||||
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
|
||||
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
|
||||
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
|
||||
done
|
||||
popd > /dev/null
|
||||
# Point plugins rpath to `$dest`.
|
||||
if [ -d "$dest/plugins" ]; then
|
||||
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
|
||||
fi
|
||||
fi
|
||||
|
||||
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
|
||||
|
||||
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
|
||||
}
|
||||
|
||||
# Check all binaries in the dest `$1`
|
||||
# and report if they're still missing dependencies or are static.
|
||||
check_runtime_dependencies() {
|
||||
package_dir="$1"
|
||||
missing=0
|
||||
# Iterate over all .so files.
|
||||
while IFS= read -r binary_file; do
|
||||
# Skip non-binaries.
|
||||
readelf -h "$binary_file" >/dev/null 2>&1 || continue
|
||||
# Report non-dynamic binaries.
|
||||
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
|
||||
echo "ldd failed for $binary_file"
|
||||
cat "$package_dir/.ldd.out"
|
||||
missing=1
|
||||
continue
|
||||
fi
|
||||
# Report missing dependencies.
|
||||
if grep -q "not found" "$package_dir/.ldd.out"; then
|
||||
echo "Missing runtime dependencies for $binary_file"
|
||||
grep "not found" "$package_dir/.ldd.out"
|
||||
missing=1
|
||||
fi
|
||||
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
|
||||
rm -f "$package_dir/.ldd.out"
|
||||
# TODO: should error?
|
||||
if [ "$missing" -ne 0 ]; then
|
||||
echo "Runtime dependency check found issues; continuing packaging."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Iterate over all built Python wrappers in `install/ifcopenshell/python-x.y.z`.
|
||||
# and zip them, bundling all dynamic libs from `lib`.
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
|
||||
py_version_major=python-${numbers}$postfix
|
||||
pushd . > /dev/null
|
||||
cd $py_version
|
||||
if [ ! -d ifcopenshell ]; then
|
||||
mkdir ../ifcopenshell_
|
||||
mv * ../ifcopenshell_
|
||||
mv ../ifcopenshell_ ifcopenshell
|
||||
fi
|
||||
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
|
||||
find ifcopenshell -name "*.pyc" -delete
|
||||
# TODO: packs qt libs also?
|
||||
stage_runtime_payload ifcopenshell
|
||||
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell
|
||||
mv *.zip ~/output
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
# Iterate over all executables in `install/ifcopenshell/bin` and zip them.
|
||||
# Each zip bundles dynamic libs from `lib` and also qt libs.
|
||||
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
|
||||
exe=`basename "$exe_path"`
|
||||
package_dir="$install_root/.package-${exe}"
|
||||
rm -rf "$package_dir"
|
||||
mkdir -p "$package_dir"
|
||||
cp "$exe_path" "$package_dir/"
|
||||
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
|
||||
stage_runtime_payload "$package_dir" 0
|
||||
stage_qt_runtime_payload "$exe_path" "$package_dir"
|
||||
if [ "$exe" = "BonsaiViewer" ]; then
|
||||
mkdir -p "$package_dir/connectors"
|
||||
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
|
||||
fi
|
||||
check_runtime_dependencies "$package_dir"
|
||||
pushd "$package_dir" > /dev/null
|
||||
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip" .
|
||||
popd > /dev/null
|
||||
rm -rf "$package_dir"
|
||||
done
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -122,7 +122,148 @@ jobs:
|
||||
- name: Package .zip archives
|
||||
shell: bash
|
||||
run: |
|
||||
uv run nix/package-zip-archives.py linuxarm64
|
||||
VERSION=v`cat VERSION`
|
||||
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
|
||||
# invokes `cargo build --release` and stages the binary +
|
||||
# connector.json into dist/autodesk/. Same on-disk shape as the
|
||||
# old PyInstaller flow so the symlink + zip steps below
|
||||
# continue to work unchanged.
|
||||
uv run src/bonsaiviewer-autodesk/packaging/build.py
|
||||
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
|
||||
test -d "$autodesk_connector_dir"
|
||||
|
||||
cd ./build/`uname`/*/install/ifcopenshell
|
||||
mkdir -p ~/output
|
||||
install_root="$PWD"
|
||||
QT6_VERSION="${QT6_VERSION:-6.8.3}"
|
||||
|
||||
if [ -z "${QT_DIR:-}" ]; then
|
||||
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
|
||||
if [ -d "$qt_candidate/lib" ]; then
|
||||
QT_DIR="$qt_candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
ensure_soname_links() {
|
||||
dest="$1"
|
||||
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
|
||||
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
|
||||
[ -n "$soname" ] || continue
|
||||
[ -e "$dest/$soname" ] && continue
|
||||
ln -s "$(basename "$shared_object")" "$dest/$soname"
|
||||
done
|
||||
}
|
||||
|
||||
stage_runtime_payload() {
|
||||
dest="$1"
|
||||
include_geometry_writers="${2:-1}"
|
||||
while IFS= read -r runtime_file; do
|
||||
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
|
||||
continue
|
||||
fi
|
||||
cp -P "$runtime_file" "$dest/"
|
||||
done < <(
|
||||
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
|
||||
[ -d "$runtime_dir" ] || continue
|
||||
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
|
||||
done
|
||||
)
|
||||
ensure_soname_links "$dest"
|
||||
}
|
||||
|
||||
stage_qt_runtime_payload() {
|
||||
exe_path="$1"
|
||||
dest="$2"
|
||||
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
|
||||
|
||||
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
|
||||
ensure_soname_links "$dest"
|
||||
|
||||
if [ -d "$QT_DIR/plugins" ]; then
|
||||
pushd "$QT_DIR/plugins" > /dev/null
|
||||
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
|
||||
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
|
||||
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
|
||||
done
|
||||
popd > /dev/null
|
||||
if [ -d "$dest/plugins" ]; then
|
||||
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
|
||||
fi
|
||||
fi
|
||||
|
||||
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
|
||||
|
||||
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
|
||||
}
|
||||
|
||||
check_runtime_dependencies() {
|
||||
package_dir="$1"
|
||||
missing=0
|
||||
while IFS= read -r binary_file; do
|
||||
readelf -h "$binary_file" >/dev/null 2>&1 || continue
|
||||
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
|
||||
echo "ldd failed for $binary_file"
|
||||
cat "$package_dir/.ldd.out"
|
||||
missing=1
|
||||
continue
|
||||
fi
|
||||
if grep -q "not found" "$package_dir/.ldd.out"; then
|
||||
echo "Missing runtime dependencies for $binary_file"
|
||||
grep "not found" "$package_dir/.ldd.out"
|
||||
missing=1
|
||||
fi
|
||||
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
|
||||
rm -f "$package_dir/.ldd.out"
|
||||
if [ "$missing" -ne 0 ]; then
|
||||
echo "Runtime dependency check found issues; continuing packaging."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
|
||||
py_version_major=python-${numbers}$postfix
|
||||
pushd . > /dev/null
|
||||
cd $py_version
|
||||
if [ ! -d ifcopenshell ]; then
|
||||
mkdir ../ifcopenshell_
|
||||
mv * ../ifcopenshell_
|
||||
mv ../ifcopenshell_ ifcopenshell
|
||||
fi
|
||||
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
|
||||
find ifcopenshell -name "*.pyc" -delete
|
||||
stage_runtime_payload ifcopenshell
|
||||
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell
|
||||
mv *.zip ~/output
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
|
||||
exe=`basename "$exe_path"`
|
||||
package_dir="$install_root/.package-${exe}"
|
||||
rm -rf "$package_dir"
|
||||
mkdir -p "$package_dir"
|
||||
cp "$exe_path" "$package_dir/"
|
||||
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
|
||||
stage_runtime_payload "$package_dir" 0
|
||||
stage_qt_runtime_payload "$exe_path" "$package_dir"
|
||||
if [ "$exe" = "BonsaiViewer" ]; then
|
||||
mkdir -p "$package_dir/connectors"
|
||||
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
|
||||
fi
|
||||
check_runtime_dependencies "$package_dir"
|
||||
pushd "$package_dir" > /dev/null
|
||||
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip" .
|
||||
popd > /dev/null
|
||||
rm -rf "$package_dir"
|
||||
done
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
max-size: 5000MB
|
||||
|
||||
- name: Set up Python for connector build
|
||||
uses: actions/setup-python@v7
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
- 'src/ifc5d/ifc5d/**'
|
||||
- 'src/ifccityjson/**'
|
||||
branches:
|
||||
- v0.9.0
|
||||
- v0.8.0
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -51,10 +51,19 @@ jobs:
|
||||
name: "Linux Build",
|
||||
short_name: linux,
|
||||
}
|
||||
- {
|
||||
name: "MacOS Build",
|
||||
short_name: macos,
|
||||
}
|
||||
- {
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1,
|
||||
}
|
||||
exclude:
|
||||
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
|
||||
- pyver: py313
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
@@ -119,7 +128,7 @@ jobs:
|
||||
blender --command extension install-file -r user_default -e $bonsai_zip
|
||||
blender --command extension list
|
||||
|
||||
git clone --branch ${{ github.ref_name }} --single-branch https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
|
||||
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
|
||||
|
||||
# Reregister Bonsai.
|
||||
# Note that running it in background might miss some errors
|
||||
|
||||
@@ -34,10 +34,19 @@ jobs:
|
||||
name: "Linux Build",
|
||||
short_name: linux,
|
||||
}
|
||||
- {
|
||||
name: "MacOS Build",
|
||||
short_name: macos,
|
||||
}
|
||||
- {
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1,
|
||||
}
|
||||
exclude:
|
||||
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
|
||||
- pyver: py313
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
|
||||
@@ -34,21 +34,21 @@ jobs:
|
||||
- name: Run conda cleaner
|
||||
run: |
|
||||
python - << EOF
|
||||
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from binstar_client.utils import get_server_api
|
||||
from binstar_client.errors import BinstarError
|
||||
|
||||
|
||||
# Configuration
|
||||
api_token = os.environ.get('ANACONDA_TOKEN')
|
||||
pkg_name = 'ifcopenshell'
|
||||
channel_name = 'ifcopenshell'
|
||||
|
||||
|
||||
# Authenticate with Anaconda
|
||||
aserver_api = get_server_api(token=api_token)
|
||||
|
||||
|
||||
|
||||
|
||||
# Get the list of packages in the channel
|
||||
def get_package(filter_package_name: str = None):
|
||||
try:
|
||||
@@ -59,12 +59,12 @@ jobs:
|
||||
print(f"No packages found for {filter_package_name}.")
|
||||
if len(user_packages) > 1:
|
||||
raise ValueError(f"Found {len(user_packages)} package for {filter_package_name}. Will only support 1 package.")
|
||||
|
||||
|
||||
return user_packages[0]
|
||||
except BinstarError as err:
|
||||
raise ValueError(f"Failed to fetch packages: {err}")
|
||||
|
||||
|
||||
|
||||
|
||||
# Delete a package version
|
||||
def delete_package(package_name, version):
|
||||
try:
|
||||
@@ -72,33 +72,33 @@ jobs:
|
||||
print(f"Deleted {package_name} version {version}")
|
||||
except BinstarError as err:
|
||||
print(f"Failed to delete {package_name} version {version}: {err}")
|
||||
|
||||
|
||||
|
||||
|
||||
# Main logic
|
||||
def main():
|
||||
package = get_package(pkg_name)
|
||||
if not package:
|
||||
print("No packages found.")
|
||||
return
|
||||
|
||||
|
||||
number_of_supported_versions = ${{ env.NUM_SUPPORTED_VERSIONS }}
|
||||
|
||||
|
||||
package_name = package['name']
|
||||
versions = package["versions"]
|
||||
if len(versions) <= number_of_supported_versions:
|
||||
print(f"Number of versions {len(versions)} is less than or equal to {number_of_supported_versions}.")
|
||||
return
|
||||
|
||||
|
||||
# sort the versions in descending order
|
||||
print(f"Before reversal: {versions=}")
|
||||
versions.reverse()
|
||||
print(f"After reversal: {versions=}")
|
||||
|
||||
|
||||
releases = versions[number_of_supported_versions:]
|
||||
|
||||
|
||||
for release in releases:
|
||||
delete_package(package_name, release)
|
||||
|
||||
|
||||
main()
|
||||
|
||||
|
||||
EOF
|
||||
|
||||
@@ -24,13 +24,13 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
|
||||
- name: Get current version
|
||||
id: version
|
||||
# Strip any trailing prerelease label and number; the dated alpha
|
||||
# suffix is added below.
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
id: verdate
|
||||
run: echo "verdate=${{ steps.version.outputs.version }}alpha${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
|
||||
test:
|
||||
name: ${{ matrix.platform.distver }}-${{ matrix.pyver.name }}
|
||||
needs: activate
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
|
||||
- name: set ARTIFACTS ENV vars
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
elif [[ "$RUNNER_OS" == "Linux" ]]; then
|
||||
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: ci-ifcopenshell-docker
|
||||
|
||||
on:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
@@ -37,14 +37,13 @@ jobs:
|
||||
name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
|
||||
-
|
||||
-
|
||||
name: Build ifcopenshell
|
||||
run: |
|
||||
mkdir build && cd build
|
||||
cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=$PWD/install/ \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DUSE_CCACHE=ON \
|
||||
-DCMAKE_PREFIX_PATH=/usr \
|
||||
-DCMAKE_SYSTEM_PREFIX_PATH=/usr \
|
||||
-DBUILD_PACKAGE=On \
|
||||
@@ -67,12 +66,12 @@ jobs:
|
||||
../cmake
|
||||
make -j $(nproc)
|
||||
make install
|
||||
-
|
||||
-
|
||||
name: Package
|
||||
run: |
|
||||
make package
|
||||
working-directory: build
|
||||
- name: Upload
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
# Artifact name
|
||||
@@ -89,8 +88,8 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Download
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v8.0.1
|
||||
with:
|
||||
# Artifact name
|
||||
@@ -101,17 +100,17 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Login to Dockerhub
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: aecgeeks
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
-
|
||||
-
|
||||
name: Build container image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
with:
|
||||
context: artifacts
|
||||
repository: aecgeeks/ifcopenshell
|
||||
# Since the dispatch is set to `tag`, `github.ref_name` should evaluate to the pushed tag
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
- '.github/workflows/ci-ifcsverchok-build.yml'
|
||||
- 'src/ifcsverchok/*'
|
||||
branches:
|
||||
- v0.9.0
|
||||
- v0.8.0
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
|
||||
@@ -3,13 +3,11 @@ name: ci-ifctester-org
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- v0.9.0
|
||||
paths:
|
||||
- src/ifctester/**
|
||||
|
||||
jobs:
|
||||
publish_ifctester_org:
|
||||
publish_website:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
${OCCT_CMAKE_DEPS} \
|
||||
libcgal-dev libeigen3-dev
|
||||
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
with:
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
run: |
|
||||
echo $Python3_ROOT_DIR
|
||||
echo ${{ env.pythonLocation }}
|
||||
|
||||
|
||||
mkdir build && cd build
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
@@ -185,7 +185,6 @@ jobs:
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
|
||||
-DUSE_MMAP=On \
|
||||
-DUSE_CCACHE=ON \
|
||||
-DBUILD_SHARED_LIBS=${{ matrix.build_shared_libs }} \
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
|
||||
-DGLTF_SUPPORT=On \
|
||||
@@ -224,7 +223,7 @@ jobs:
|
||||
cmake --build .
|
||||
./IfcOpenHouse && test -f IfcOpenHouse.ifc
|
||||
./IfcParseExamples IfcOpenHouse.ifc
|
||||
./IfcAdvancedHouse && test -f IfcAdvancedHouse.ifc
|
||||
./IfcAdvancedHouse && test -f IfcAdvancedHouse.ifc
|
||||
./IfcAlignment && test -f FHWA_Bridge_Geometry_Alignment_Example.ifc
|
||||
./IfcSimplifiedAlignment && test -f FHWA_Bridge_Geometry_Alignment_Example_Simplified.ifc
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
external_repository: IfcOpenShell/bonsaibim_org_docs_unstable # Target repository
|
||||
publish_branch: main # Branch to deploy to
|
||||
cname: docs-unstable.bonsaibim.org # Custom domain for unstable docs
|
||||
publish_dir: src/bonsai/docs/_build/html # Directory containing built docs
|
||||
publish_dir: src/bonsai/docs/_build/html # Directory containing built docs
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
|
||||
build:
|
||||
needs: activate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
libtbb-dev nlohmann-json3-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libcgal-dev opencollada-dev
|
||||
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
echo ::set-output name=deb::$( ls assets/*.deb | head -n 1 | xargs basename )
|
||||
working-directory: build
|
||||
env:
|
||||
CHANGELOG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CHANGELOG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Release
|
||||
id: release
|
||||
uses: actions/create-release@v1
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
upload_url: ${{ steps.release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: build/assets/${{ steps.package.outputs.tgz }}
|
||||
asset_name: ${{ steps.package.outputs.tgz }}
|
||||
asset_content_type: application/x-gzip
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
upload_url: ${{ steps.release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: build/assets/${{ steps.package.outputs.deb }}
|
||||
asset_name: ${{ steps.package.outputs.deb }}
|
||||
asset_content_type: application/vnd.debian.binary-package
|
||||
|
||||
@@ -17,3 +17,6 @@
|
||||
[submodule "src/svgfill/3rdparty/svgpp"]
|
||||
path = src/svgfill/3rdparty/svgpp
|
||||
url = https://github.com/svgpp/svgpp
|
||||
[submodule "src/ifcopenshell-python/test/IfcRelSpaceBoundary_TestFiles"]
|
||||
path = src/ifcopenshell-python/test/IfcRelSpaceBoundary_TestFiles
|
||||
url = https://github.com/CyrilWaechter/IfcRelSpaceBoundary_TestFiles
|
||||
|
||||
+20
-38
@@ -134,7 +134,6 @@ option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only ins
|
||||
option(USE_DEBUG_PYTHON "Use debug binaries when building Debug IfcPython on Windows." OFF)
|
||||
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
|
||||
option(VERSION_OVERRIDE "Use VERSION as the branch label when commit information is embedded" OFF)
|
||||
option(USE_CCACHE "Use ccache as a compiler launcher if it is found" OFF)
|
||||
|
||||
set(
|
||||
PYTHON_MODULE_INSTALL_DIR
|
||||
@@ -179,30 +178,26 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
|
||||
set(BUILD_IFCGEOM ON)
|
||||
endif()
|
||||
|
||||
if(USE_CCACHE)
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
message(STATUS "`USE_CCACHE` is enabled and `ccache` is found, using it as a compiler launcher.")
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
|
||||
if(MSVC)
|
||||
# By default Visual Studio generators will use /Zi which is not compatible
|
||||
# with ccache, so tell Visual Studio to use /Z7 instead.
|
||||
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
|
||||
# Not needed for Ninja.
|
||||
if(CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
file(COPY_FILE
|
||||
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
|
||||
ONLY_IF_DIFFERENT)
|
||||
set(CMAKE_VS_GLOBALS
|
||||
"CLToolExe=cl.exe"
|
||||
"CLToolPath=${CMAKE_BINARY_DIR}"
|
||||
"UseMultiToolTask=true"
|
||||
)
|
||||
endif()
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
message(STATUS "`ccache` is found, using it as a compiler launcher.")
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
|
||||
if(MSVC)
|
||||
# By default Visual Studio generators will use /Zi which is not compatible
|
||||
# with ccache, so tell Visual Studio to use /Z7 instead.
|
||||
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
|
||||
# Not needed for Ninja.
|
||||
if(CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
file(COPY_FILE
|
||||
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
|
||||
ONLY_IF_DIFFERENT)
|
||||
set(CMAKE_VS_GLOBALS
|
||||
"CLToolExe=cl.exe"
|
||||
"CLToolPath=${CMAKE_BINARY_DIR}"
|
||||
"UseMultiToolTask=true"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "ccache usage is disabled, set `USE_CCACHE=ON` to enable it.")
|
||||
endif()
|
||||
|
||||
if(MSVC AND MSVC_PARALLEL_BUILD)
|
||||
@@ -316,25 +311,12 @@ if (WITH_ROCKSDB)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB)
|
||||
# See https://github.com/facebook/rocksdb/issues/981.
|
||||
if(TARGET RocksDB::rocksdb)
|
||||
set(IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET RocksDB::rocksdb)
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb)
|
||||
elseif(TARGET RocksDB::rocksdb-shared)
|
||||
set(IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET RocksDB::rocksdb-shared)
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb-shared)
|
||||
else()
|
||||
message(FATAL_ERROR "RocksDB found but neither RocksDB::rocksdb nor RocksDB::rocksdb-shared target exists")
|
||||
endif()
|
||||
# Our win/build-deps.cmd builds RocksDB separately per Debug/Release config into the
|
||||
# same install prefix, so the imported target only ever has DEBUG and RELEASE listed in
|
||||
# IMPORTED_CONFIGURATIONS. On a multi-config generator (Visual Studio), CMake maps any
|
||||
# unmatched build config to the *first* entry of that list, which happens to be DEBUG
|
||||
# (RocksDBTargets-debug.cmake sorts before RocksDBTargets-release.cmake). Without an
|
||||
# explicit mapping, RelWithDebInfo and MinSizeRel builds would end up linking the
|
||||
# /MDd-flavored rocksdb_d.lib into an /MD (NDEBUG) binary, causing a CRT/runtime-library
|
||||
# mismatch that depends on nothing but that alphabetical ordering.
|
||||
set_target_properties(${IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET} PROPERTIES
|
||||
MAP_IMPORTED_CONFIG_RELWITHDEBINFO "RELWITHDEBINFO;RELEASE"
|
||||
MAP_IMPORTED_CONFIG_MINSIZEREL "MINSIZEREL;RELEASE"
|
||||
)
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE ${IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET})
|
||||
|
||||
if (WITH_ZSTD)
|
||||
# @todo do we actually need the zstd include dir or rather just pass
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# Design Spec: Space Regeneration with Sloped Roofs, Walls, and Slabs
|
||||
|
||||
## Goal
|
||||
|
||||
Extend `bonsai.core.spatial.generate_space` so it produces correct `IfcSpace`
|
||||
geometry for non-rectilinear envelopes:
|
||||
|
||||
- sloped roofs,
|
||||
- sloped slabs,
|
||||
- sloped walls,
|
||||
- curved walls.
|
||||
|
||||
The existing footprint-based `IfcExtrudedAreaSolid` path is preserved for
|
||||
ordinary vertical extrusions. A new hybrid path keeps the representation
|
||||
parametric when possible and falls back to an `IfcFacetedBrep` only when the
|
||||
boundary cannot be expressed as a clipped extrusion.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Existing footprint generation │
|
||||
│ (get_space_polygon_from_*_objects) │
|
||||
└──────────────┬────────────────────────────┘
|
||||
v
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Detect extrudability and bounding planes │
|
||||
│ (pure-Python util, Blender-independent) │
|
||||
└──────────────┬────────────────────────────┘
|
||||
v
|
||||
┌──────┴──────┐
|
||||
v v
|
||||
┌───────────────────┐ ┌───────────────────┐
|
||||
│ Extrusion + clips │ │ B-rep fallback │
|
||||
│ IfcExtrudedAreaSolid│ │ IfcFacetedBrep │
|
||||
│ + IfcBooleanClippingResult│ │ (or IfcPolygonalFaceSet) │
|
||||
└───────────────────┘ └───────────────────┘
|
||||
```
|
||||
|
||||
## Prior art
|
||||
|
||||
- **CBIP** (Lilis et al.): constructive solid geometry approach that builds
|
||||
space volumes as half-space intersections of bounding planes — the basis for
|
||||
the parametric clipping path.
|
||||
- **Fichter et al. 2021**: ray-tracing method for automatic boundary
|
||||
generation; motivates the use of `geom.tree.select_ray` for top/bottom plane
|
||||
detection.
|
||||
- **Lilis et al. 2021**: semi-automatic boundary recognition; informs the
|
||||
fallback to existing `boundary.auto_generate_boundaries` machinery.
|
||||
- **Ying & Lee 2019**: faceting of curved walls; motivates the B-rep fallback
|
||||
for curved-in-plan walls that cannot be represented as vertical extruded
|
||||
profiles.
|
||||
|
||||
## Detection criteria
|
||||
|
||||
Use the parametric `IfcExtrudedAreaSolid` + `IfcBooleanClippingResult` path
|
||||
when **all** are true:
|
||||
|
||||
1. Side walls are vertical extrusions (face normal is horizontal).
|
||||
Curved-in-plan walls are allowed; their footprint is polygonized or
|
||||
reconstructed as a curved profile.
|
||||
2. The roof/top boundary is piecewise-planar.
|
||||
3. The bottom slab/floor boundary is piecewise-planar.
|
||||
4. The footprint is a single closed outer region, possibly with inner closed
|
||||
regions for holes.
|
||||
5. The resulting half-space intersection is non-empty and produces a single
|
||||
solid.
|
||||
|
||||
Otherwise use the B-rep fallback.
|
||||
|
||||
## Parametric extrusion + clipping algorithm
|
||||
|
||||
1. **Build the profile**
|
||||
- Outer ring from the footprint polygon → `IfcArbitraryClosedProfileDef`.
|
||||
- Inner rings (holes, e.g., around columns) →
|
||||
`IfcArbitraryProfileDefWithVoids`.
|
||||
2. **Extrude**
|
||||
- Create `IfcExtrudedAreaSolid` along local +Z, with a height large enough
|
||||
to cover all bounding planes.
|
||||
3. **Find top planes**
|
||||
- Cast vertical rays upward from the footprint centroid and sample points
|
||||
using `ifcopenshell.geom.tree.select_ray`.
|
||||
- Check each hit face for planarity with
|
||||
`ifcopenshell.util.shape.dissolve_faces(..., merge_coplanar=True)`.
|
||||
- Group coplanar hits into distinct planes.
|
||||
4. **Find bottom planes**
|
||||
- Same as top, but downward.
|
||||
5. **Clip**
|
||||
- For each top plane: create `IfcHalfSpaceSolid` with normal pointing
|
||||
upward (removed side), apply via `ifcopenshell.api.geometry.clip_solid`.
|
||||
- For each bottom plane: create `IfcHalfSpaceSolid` with normal pointing
|
||||
downward, apply via `clip_solid`.
|
||||
6. **Output**
|
||||
- `IfcExtrudedAreaSolid` wrapped in a chain of `IfcBooleanClippingResult`.
|
||||
|
||||
## B-rep fallback algorithm
|
||||
|
||||
For non-extrudable cases (sloped walls, curved roofs, etc.):
|
||||
|
||||
1. **Seed space**
|
||||
- Create a temporary rough mesh (e.g., extruded footprint bounding box) as
|
||||
a placeholder.
|
||||
2. **Extract boundary faces**
|
||||
- Run `ifcopenshell.util.boundary.auto_generate_boundaries` against the
|
||||
seed to identify the faces of bounding elements that touch the space.
|
||||
- Convert each boundary polygon from face-local back to 3D world
|
||||
coordinates.
|
||||
3. **Build closed shell**
|
||||
- Collect the 3D boundary faces.
|
||||
- Add narrow gap-closing faces if `auto_generate_boundaries` leaves
|
||||
unmatched edges.
|
||||
- Triangulate and produce `IfcClosedShell` → `IfcFacetedBrep` (or
|
||||
`IfcPolygonalFaceSet` for IFC4+).
|
||||
4. **Clean up**
|
||||
- Assign the B-rep to the `IfcSpace` and remove the temporary seed
|
||||
geometry.
|
||||
|
||||
## Files to touch
|
||||
|
||||
- `src/ifcopenshell-python/ifcopenshell/util/space.py`
|
||||
- New: `detect_space_volume_strategy`
|
||||
- New: `build_extruded_clipped_space`
|
||||
- New: `build_brep_space`
|
||||
- New helpers for ray-cast plane detection and face planarity checks.
|
||||
- `src/bonsai/bonsai/tool/spatial.py`
|
||||
- Extend `set_space_representation_from_polygon` to dispatch to the new
|
||||
strategy.
|
||||
- Extend footprint/profile creation to support inner rings for holes.
|
||||
- `src/bonsai/bonsai/core/spatial.py`
|
||||
- `generate_space` calls the dispatcher.
|
||||
|
||||
## Testing
|
||||
|
||||
- Add unit tests in `src/ifcopenshell-python/test/util/test_space.py` for pure
|
||||
geometry helpers:
|
||||
- simple shed roof,
|
||||
- gable roof,
|
||||
- sloped slab,
|
||||
- L-shaped footprint with sloped roof,
|
||||
- curved wall.
|
||||
- Add Bonsai tests in `src/bonsai/test/tool/test_spatial.py` for end-to-end
|
||||
`generate_space` with non-rectilinear geometry.
|
||||
|
||||
## Error handling
|
||||
|
||||
- If detection fails or half-space clipping produces an invalid result, fall
|
||||
back to the B-rep path.
|
||||
- If the B-rep path also fails, return an error string and leave the existing
|
||||
space representation unchanged.
|
||||
|
||||
## Known limitations and non-goals
|
||||
|
||||
- **Curved (single/double-curvature) roofs and domes** are handled only via the
|
||||
B-rep fallback; they are not expressible as `IfcExtrudedAreaSolid` +
|
||||
`IfcBooleanClippingResult` in this design.
|
||||
- The B-rep fallback produces **non-parametric** geometry: the resulting
|
||||
`IfcFacetedBrep`/`IfcPolygonalFaceSet` cannot be re-edited parametrically by
|
||||
the user afterwards. This is an accepted trade-off; the parametric path is
|
||||
preferred whenever detection succeeds.
|
||||
- The B-rep fallback depends on `boundary.auto_generate_boundaries`, so it
|
||||
inherits its assumptions: bounding elements must be related to the space and
|
||||
the seed volume must intersect them. Gap-closing faces may produce
|
||||
non-manifold output for degenerate envelopes; we accept this for
|
||||
non-extrudable edge cases.
|
||||
- The parametric path requires a single closed outer footprint with optional
|
||||
inner holes. Multi-region disconnected footprints are not supported and fall
|
||||
back to B-rep.
|
||||
|
||||
+63
-243
@@ -32,7 +32,20 @@ Example usage:
|
||||
python build-all.py IfcParse IfcOpenShell-Python
|
||||
|
||||
|
||||
Run with --help to see available arguments.
|
||||
Available arguments:
|
||||
``-py-313`` - build for specific Python version
|
||||
(building for all supported Python version by default).
|
||||
``-occt-xxx`` - use a specific OCCT version (e.g. ``-occt-7.8.1``) instead of the 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
|
||||
``-build-examples`` - build IfcOpenShell examples
|
||||
``-lto`` - enable link-time optimization (adds ``-flto`` to compiler flags)
|
||||
``-v`` - enable verbose logs
|
||||
|
||||
|
||||
Used environment variables:
|
||||
@@ -61,8 +74,6 @@ Used environment variables:
|
||||
`ADD_COMMIT_SHA` and `VERSION_OVERRIDE` will be set to `ON` while configuring IfcOpenShell
|
||||
- ``BUILD_BONSAIVIEWER`` - enable building BonsaiViewer, `off` by default.
|
||||
- ``IFCOS_BUILD_PYTHON_WRAPPER`` - enable building the Python wrapper, `on` by default.
|
||||
- ``PYTHON_USER_SITE`` - install the Python wrapper into the user's site-packages directory
|
||||
instead of the interpreter's prefix, `off` by default.
|
||||
|
||||
# This script builds IfcOpenShell and its dependencies #
|
||||
# #
|
||||
@@ -107,9 +118,6 @@ Used environment variables:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import logging
|
||||
import multiprocessing
|
||||
@@ -121,13 +129,12 @@ import subprocess as sp
|
||||
import sys
|
||||
import sysconfig
|
||||
import tarfile
|
||||
import textwrap
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Generator, Sequence
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Literal, NamedTuple
|
||||
from typing import Literal
|
||||
from urllib.request import urlretrieve
|
||||
|
||||
from typing_extensions import assert_never
|
||||
@@ -155,9 +162,8 @@ ADD_COMMIT_SHA = is_on_off(os.getenv("ADD_COMMIT_SHA"), default=False)
|
||||
IFCOS_BUILD_PYTHON_WRAPPER = is_on_off(os.getenv("IFCOS_BUILD_PYTHON_WRAPPER"), default=True)
|
||||
BUILD_BONSAIVIEWER = is_on_off(os.getenv("BUILD_BONSAIVIEWER"), default=False)
|
||||
USE_OCCT = is_on_off(os.getenv("USE_OCCT"), default=True)
|
||||
PYTHON_USER_SITE = is_on_off(os.getenv("PYTHON_USER_SITE"), default=False)
|
||||
|
||||
PYTHON_VERSIONS = ["3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0", "3.15.0"]
|
||||
PYTHON_VERSIONS = ["3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
|
||||
JSON_VERSION = "3.11.3"
|
||||
OCE_VERSION = "0.18.3"
|
||||
OCCT_VERSION = "7.8.1"
|
||||
@@ -196,142 +202,10 @@ strip = "strip"
|
||||
xz = "xz" # Used implicitly for `tar -xf *.tar.xz`.
|
||||
brew = "brew"
|
||||
|
||||
|
||||
class Args(NamedTuple):
|
||||
explicit_targets: list[str]
|
||||
build_examples: bool
|
||||
diskcleanup: bool
|
||||
lto: bool
|
||||
verbose: bool
|
||||
shared: bool
|
||||
ifcopenshell_shared: bool
|
||||
occt_shared: bool
|
||||
mac_cross_compile_intel: bool
|
||||
wasm: bool
|
||||
|
||||
|
||||
class DynamicArgs(NamedTuple):
|
||||
without: set[str]
|
||||
py_versions: set[str]
|
||||
occt_version: str | None
|
||||
|
||||
@classmethod
|
||||
def from_unknown_flags(cls, unknown_flags: list[str], arg_parser: argparse.ArgumentParser) -> DynamicArgs:
|
||||
flags = set(s.lstrip("-") for s in unknown_flags if s.startswith("-"))
|
||||
|
||||
without: set[str] = set()
|
||||
py_versions: set[str] = set()
|
||||
occt_versions: set[str] = set()
|
||||
leftover: set[str] = set()
|
||||
|
||||
for f in flags:
|
||||
if f.startswith("without-"):
|
||||
without.add(f.removeprefix("without-").lower())
|
||||
elif f.startswith("py-"):
|
||||
py_versions.add(f.removeprefix("py-"))
|
||||
elif f.startswith("occt-"):
|
||||
occt_versions.add(f.removeprefix("occt-"))
|
||||
else:
|
||||
leftover.add(f)
|
||||
|
||||
if leftover:
|
||||
arg_parser.error(f"unrecognized arguments: {', '.join('-' + f for f in sorted(leftover))}")
|
||||
if len(occt_versions) > 1:
|
||||
arg_parser.error(f"more than one OCCT version provided: {', '.join(sorted(occt_versions))}")
|
||||
|
||||
occt_version = next(iter(occt_versions), None)
|
||||
return cls(without=without, py_versions=py_versions, occt_version=occt_version)
|
||||
|
||||
|
||||
def parse_args() -> tuple[Args, DynamicArgs]:
|
||||
arg_parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=textwrap.dedent("""\
|
||||
Additional dynamic -flags (not declared above):
|
||||
-py-313 build for specific Python version
|
||||
(building for all supported Python versions by default)
|
||||
-occt-xxx use a specific OCCT version (e.g. -occt-7.8.1) instead of the default
|
||||
-without-xxx do not build dependency `xxx` (e.g. --without-swig)"""),
|
||||
)
|
||||
arg_parser.add_argument("explicit_targets", nargs="*", help="Targets provided by CLI.")
|
||||
arg_parser.add_argument(
|
||||
"--build-examples",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Build IfcOpenShell examples.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"-diskcleanup",
|
||||
"--diskcleanup",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Clean up build directories after finishing building dependencies.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"-lto",
|
||||
"--lto",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Enable link-time optimization (adds -flto to compiler flags).",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Enable verbose logs.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"-shared",
|
||||
"--shared",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Build shared libraries. By default will build static.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"-ifcopenshell-shared",
|
||||
"--ifcopenshell-shared",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Build only IfcOpenShell's own libraries as shared (dependencies stay static). "
|
||||
"Redundant if -shared is also passed.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--occt-shared",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Build OCCT as shared. Redundant if -shared is also passed.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"-mac-cross-compile-intel",
|
||||
"--mac-cross-compile-intel",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Cross compile for Intel Mac on Apple Silicon host.",
|
||||
)
|
||||
arg_parser.add_argument("-wasm", "--wasm", action="store_true", default=False, help="Compile for wasm.")
|
||||
namespace, unknown_flags = arg_parser.parse_known_args()
|
||||
args = Args(
|
||||
explicit_targets=namespace.explicit_targets,
|
||||
build_examples=namespace.build_examples,
|
||||
diskcleanup=namespace.diskcleanup,
|
||||
lto=namespace.lto,
|
||||
verbose=namespace.verbose,
|
||||
shared=namespace.shared,
|
||||
ifcopenshell_shared=namespace.ifcopenshell_shared or namespace.shared,
|
||||
occt_shared=namespace.occt_shared or namespace.shared,
|
||||
mac_cross_compile_intel=namespace.mac_cross_compile_intel,
|
||||
wasm=namespace.wasm,
|
||||
)
|
||||
|
||||
dynamic_args = DynamicArgs.from_unknown_flags(unknown_flags, arg_parser)
|
||||
return args, dynamic_args
|
||||
|
||||
|
||||
ARGS, DYNAMIC_ARGS = parse_args()
|
||||
|
||||
explicit_targets: set[str] = set(ARGS.explicit_targets)
|
||||
explicit_targets = [s for s in sys.argv[1:] if not s.startswith("-")]
|
||||
"""Targets provided by CLI."""
|
||||
flags = set(s.lstrip("-") for s in sys.argv[1:] if s.startswith("-"))
|
||||
"""CLI flags."""
|
||||
|
||||
# Helper function for coloured printing
|
||||
|
||||
@@ -350,11 +224,17 @@ def cecho(message, color=NO_COLOR):
|
||||
logger.info(f"{color}{message}\033[0m")
|
||||
|
||||
|
||||
# Flags.
|
||||
BUILD_EXAMPLES = "build-examples" in flags
|
||||
DISK_CLEANUP = "diskcleanup" in flags
|
||||
LTO = "lto" in flags
|
||||
VERBOSE = "v" in flags
|
||||
|
||||
APPLE = platform.system() == "Darwin"
|
||||
MAC_CROSS_COMPILE_INTEL = ARGS.mac_cross_compile_intel
|
||||
MAC_CROSS_COMPILE_INTEL = "mac-cross-compile-intel" in flags
|
||||
assert platform.system() == "Darwin" or not MAC_CROSS_COMPILE_INTEL
|
||||
|
||||
WASM = ARGS.wasm
|
||||
WASM = "wasm" in flags
|
||||
"""Build WASM outside pyodide build environment."""
|
||||
WASM_CMAKE_IS_USING_INIT_VARS = False
|
||||
if WASM:
|
||||
@@ -500,12 +380,12 @@ dependency_tree: dict[str, tuple[str, ...]] = {
|
||||
def gather_dependencies(dep: str) -> Generator[str]:
|
||||
yield dep
|
||||
for d in dependency_tree[dep]:
|
||||
if d.lower() not in DYNAMIC_ARGS.without:
|
||||
if f"without-{d.lower()}" not in flags:
|
||||
for x in gather_dependencies(d):
|
||||
yield x
|
||||
|
||||
|
||||
if ARGS.verbose:
|
||||
if VERBOSE:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
ch.setFormatter(formatter)
|
||||
@@ -526,26 +406,29 @@ else:
|
||||
MAC_CROSS_COMPILE_INTEL_AUTOCONF_HOST_ARGS = []
|
||||
|
||||
OFF_ON = ["OFF", "ON"]
|
||||
BUILD_STATIC = not ARGS.shared
|
||||
BUILD_STATIC = "shared" not in flags
|
||||
"""Whether dependencies are built static."""
|
||||
IFCOPENSHELL_STATIC = BUILD_STATIC and "ifcopenshell-shared" not in flags
|
||||
"""Whether IfcOpenShell's own libraries are built static."""
|
||||
ENABLE_FLAG = "--enable-static" if BUILD_STATIC else "--enable-shared"
|
||||
DISABLE_FLAG = "--disable-shared" if BUILD_STATIC else "--disable-static"
|
||||
LINK_TYPE = "static" if BUILD_STATIC else "shared"
|
||||
LINK_TYPE_UCFIRST = LINK_TYPE.capitalize()
|
||||
LIBRARY_EXT = "a" if BUILD_STATIC else "so"
|
||||
PIC = "-fPIC" if BUILD_STATIC else ""
|
||||
|
||||
if DYNAMIC_ARGS.py_versions:
|
||||
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if "".join(pyv.split(".")[:2]) in DYNAMIC_ARGS.py_versions]
|
||||
if any(f.startswith("py-") for f in flags):
|
||||
PYTHON_VERSIONS = [pyv for pyv in PYTHON_VERSIONS if f"py-{''.join(pyv.split('.')[:2])}" in flags]
|
||||
|
||||
if DYNAMIC_ARGS.occt_version is not None:
|
||||
OCCT_VERSION = DYNAMIC_ARGS.occt_version
|
||||
if any(f.startswith("occt-") for f in flags):
|
||||
OCCT_VERSION = next(f.split("-", 1)[1] for f in flags if f.startswith("occt-"))
|
||||
|
||||
if explicit_targets:
|
||||
targets = {dep for target in explicit_targets for dep in gather_dependencies(target)}
|
||||
else:
|
||||
targets = set(dependency_tree.keys())
|
||||
|
||||
targets = set(t for t in targets if t.lower() not in DYNAMIC_ARGS.without)
|
||||
targets = set(t for t in targets if "without-%s" % t.lower() not in flags)
|
||||
if not explicit_targets and not BUILD_BONSAIVIEWER:
|
||||
targets.difference_update({"BonsaiViewer", "qt6"})
|
||||
if BUILD_BONSAIVIEWER:
|
||||
@@ -628,7 +511,7 @@ def restore_env(var_name: str, old_value: str | None) -> None:
|
||||
os.environ[var_name] = old_value
|
||||
|
||||
|
||||
def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False, env: dict[str, str] | None = None) -> str:
|
||||
def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False) -> str:
|
||||
"""
|
||||
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
|
||||
sets up logging `stderr` to `LOG_FILE` (in append mode) and returns stdout
|
||||
@@ -652,7 +535,7 @@ def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False, env
|
||||
# Ensure both live logs available in the log file
|
||||
# and the putput.
|
||||
with open(LOG_FILE, "a", encoding="utf-8") as log_file_handle:
|
||||
proc = sp.Popen(cmds, cwd=cwd, stdout=sp.PIPE, stderr=sp.PIPE, encoding="utf-8", env=env)
|
||||
proc = sp.Popen(cmds, cwd=cwd, stdout=sp.PIPE, stderr=sp.PIPE, encoding="utf-8")
|
||||
assert proc.stdout and proc.stderr
|
||||
|
||||
t_out = threading.Thread(target=stream_reader, args=(proc.stdout, stdout, log_file_handle))
|
||||
@@ -938,7 +821,7 @@ def build_dependency(
|
||||
)
|
||||
logger.info(f"\rInstalled {name} \n")
|
||||
|
||||
if ARGS.diskcleanup:
|
||||
if DISK_CLEANUP:
|
||||
shutil.rmtree(build_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@@ -1044,8 +927,6 @@ ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
|
||||
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CXXFLAGS_SHARED = CXXFLAGS_MINIMAL
|
||||
CFLAGS_SHARED = CFLAGS_MINIMAL
|
||||
if WASM:
|
||||
# WASM `SIDE_MODULE_` are absorbed by `emcmake` automatically.
|
||||
CXXFLAGS = CXXFLAGS_MINIMAL
|
||||
@@ -1055,19 +936,19 @@ elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev
|
||||
CXXFLAGS = f"{CXXFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS = f"{CFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden {ADDITIONAL_ARGS_STR}"
|
||||
else:
|
||||
CXXFLAGS = CXXFLAGS_SHARED
|
||||
CFLAGS = CFLAGS_SHARED
|
||||
CXXFLAGS = CXXFLAGS_MINIMAL
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
LDFLAGS = f"{LDFLAGS} -Wl,--gc-sections {ADDITIONAL_ARGS_STR}"
|
||||
else:
|
||||
if BUILD_STATIC:
|
||||
CXXFLAGS = f"{CXXFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS = f"{CFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
else:
|
||||
CXXFLAGS = CXXFLAGS_SHARED
|
||||
CFLAGS = CFLAGS_SHARED
|
||||
CXXFLAGS = CXXFLAGS_MINIMAL
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
LDFLAGS = f"{LDFLAGS} {ADDITIONAL_ARGS_STR}"
|
||||
|
||||
if ARGS.lto:
|
||||
if LTO:
|
||||
for f in compiler_flags:
|
||||
locals()[f] += f" -flto={IFCOS_NUM_BUILD_PROCS}"
|
||||
|
||||
@@ -1150,9 +1031,6 @@ if "swig" in targets:
|
||||
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")
|
||||
|
||||
@@ -1167,23 +1045,12 @@ if USE_OCCT and "occ" in targets:
|
||||
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=occt_name,
|
||||
name=f"occt-{OCCT_VERSION}",
|
||||
mode="cmake",
|
||||
build_tool_args=[
|
||||
f"-DINSTALL_DIR={OCCT_INSTALL_PATH}",
|
||||
f"-DBUILD_LIBRARY_TYPE={occt_link_type}",
|
||||
f"-DINSTALL_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}",
|
||||
f"-DBUILD_LIBRARY_TYPE={LINK_TYPE_UCFIRST}",
|
||||
f"-DBUILD_MODULE_Draw=0",
|
||||
f"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off",
|
||||
# Disable xlib explicitly, as it tries to use it on Desktop Ubuntu, adding unnecessary dependency.
|
||||
@@ -1202,11 +1069,6 @@ if USE_OCCT and "occ" in targets:
|
||||
patch=patches,
|
||||
revision="V" + OCCT_VERSION.replace(".", "_"),
|
||||
)
|
||||
|
||||
if ARGS.occt_shared:
|
||||
restore_env("CPPFLAGS", OLD_CPP_FLAGS)
|
||||
restore_env("CXXFLAGS", OLD_CXX_FLAGS)
|
||||
restore_env("CFLAGS", OLD_C_FLAGS)
|
||||
elif "occ" in targets:
|
||||
build_dependency(
|
||||
name=f"oce-{OCE_VERSION}",
|
||||
@@ -1285,7 +1147,13 @@ if "OpenCOLLADA" in targets:
|
||||
# OpenCOLLADAConfig.cmake.in hardcodes shared-lib targets on Unix regardless of
|
||||
# whether shared libs were actually built. We make it follow `USE_SHARED` instead.
|
||||
patches.append("./patches/opencollada/config_select_libs_by_use_shared.patch")
|
||||
patches.append("./patches/opencollada/remove_tr1.patch")
|
||||
|
||||
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
|
||||
# outside of the std:: namespace.
|
||||
patches.append("./patches/opencollada/remove_tr1.patch")
|
||||
|
||||
build_dependency(
|
||||
"OpenCOLLADA",
|
||||
@@ -1310,14 +1178,6 @@ if "OpenCOLLADA" in targets:
|
||||
revision=OPENCOLLADA_VERSION,
|
||||
)
|
||||
|
||||
|
||||
def python_consider_rc(python_version: str) -> str:
|
||||
# TODO: remove after Python 3.15 release.
|
||||
if python_version == "3.15.0":
|
||||
python_version += "rc1"
|
||||
return python_version
|
||||
|
||||
|
||||
if "python" in targets and not USE_CURRENT_PYTHON_VERSION and not WASM:
|
||||
# Python should not be built with -fvisibility=hidden, from experience that introduces segfaults
|
||||
OLD_CPP_FLAGS = os.environ["CPPFLAGS"]
|
||||
@@ -1346,16 +1206,13 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and not WASM:
|
||||
PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"])
|
||||
|
||||
for PYTHON_VERSION in PYTHON_VERSIONS:
|
||||
python_version_url = PYTHON_VERSION
|
||||
PYTHON_VERSION = python_consider_rc(PYTHON_VERSION)
|
||||
|
||||
# Don't fail silently on missing Python dependencies (e.g. openssl or zlib),
|
||||
# because later ifcopenshell-python build will fail too but in a more confusing way.
|
||||
build_dependency(
|
||||
f"python-{PYTHON_VERSION}",
|
||||
"autoconf",
|
||||
PYTHON_CONFIGURE_ARGS,
|
||||
f"http://www.python.org/ftp/python/{python_version_url}/",
|
||||
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
|
||||
f"Python-{PYTHON_VERSION}.tgz",
|
||||
)
|
||||
python_install = INSTALL_DIR / f"python-{PYTHON_VERSION}"
|
||||
@@ -1574,7 +1431,7 @@ if "qt6" in targets:
|
||||
cecho("Building IfcOpenShell:", GREEN)
|
||||
|
||||
IFCOS_DIR = os.path.join(DEPS_DIR, "build", "ifcopenshell")
|
||||
if not is_on_off(os.getenv("NO_CLEAN"), default=False):
|
||||
if os.environ.get("NO_CLEAN", "").lower() not in {"1", "on", "true"}:
|
||||
if os.path.exists(IFCOS_DIR):
|
||||
shutil.rmtree(IFCOS_DIR)
|
||||
os.makedirs(IFCOS_DIR, exist_ok=True)
|
||||
@@ -1584,8 +1441,8 @@ os.makedirs(ifcos_build_dir, exist_ok=True)
|
||||
|
||||
cmake_args = [
|
||||
"-DUSE_MMAP=OFF",
|
||||
f"-DBUILD_EXAMPLES={OFF_ON[ARGS.build_examples]}",
|
||||
"-DBUILD_SHARED_LIBS=" + OFF_ON[ARGS.ifcopenshell_shared],
|
||||
f"-DBUILD_EXAMPLES={OFF_ON[BUILD_EXAMPLES]}",
|
||||
"-DBUILD_SHARED_LIBS=" + OFF_ON[not IFCOPENSHELL_STATIC],
|
||||
"-DGLTF_SUPPORT=ON",
|
||||
"-DBoost_NO_BOOST_CMAKE=On",
|
||||
"-DCREATE_BUNDLE=On",
|
||||
@@ -1630,7 +1487,7 @@ if "cgal" in targets:
|
||||
cmake_args.append(f"-DCGAL_WITH_GMPXX=Off")
|
||||
|
||||
if "occ" in targets and USE_OCCT:
|
||||
cmake_args_prefix_path.append(OCCT_INSTALL_PATH)
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/occt-{OCCT_VERSION}")
|
||||
|
||||
elif "occ" in targets:
|
||||
# We don't support find_package for OCE.
|
||||
@@ -1695,7 +1552,6 @@ ifcos_build_args = [
|
||||
f"-DBUILD_CONVERT={OFF_ON['IfcConvert' in targets]}",
|
||||
f"-DBUILD_BONSAIVIEWER={OFF_ON['BonsaiViewer' in targets]}",
|
||||
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell",
|
||||
"-DUSE_CCACHE=ON",
|
||||
]
|
||||
|
||||
if not WASM and (
|
||||
@@ -1717,42 +1573,6 @@ if not WASM and (
|
||||
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=ifcos_build_dir)
|
||||
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=ifcos_build_dir)
|
||||
|
||||
def test_examples() -> None:
|
||||
cecho("Running examples...", GREEN)
|
||||
examples_bin_dir = Path(DEPS_DIR) / "install" / "ifcopenshell" / "bin"
|
||||
|
||||
examples_env = os.environ.copy()
|
||||
ld_library_paths = ["../lib"]
|
||||
if ARGS.occt_shared:
|
||||
ld_library_paths.append(f"{OCCT_INSTALL_PATH}/lib")
|
||||
examples_env["LD_LIBRARY_PATH"] = os.pathsep.join(ld_library_paths)
|
||||
|
||||
examples: dict[tuple[str, ...], str | None] = {
|
||||
("./IfcOpenHouse",): "IfcOpenHouse.ifc",
|
||||
("./IfcParseExamples", "IfcOpenHouse.ifc"): None,
|
||||
("./IfcAdvancedHouse",): "IfcAdvancedHouse.ifc",
|
||||
}
|
||||
# Only for ifc4x3 schema.
|
||||
if (examples_bin_dir / "IfcAlignment").is_file():
|
||||
examples[("./IfcAlignment",)] = "FHWA_Bridge_Geometry_Alignment_Example.ifc"
|
||||
examples[("./IfcSimplifiedAlignment",)] = "FHWA_Bridge_Geometry_Alignment_Example_Simplified.ifc"
|
||||
|
||||
produced_files: set[str] = set()
|
||||
try:
|
||||
for cmd, expected_file in examples.items():
|
||||
run(cmd, cwd=str(examples_bin_dir), env=examples_env)
|
||||
if expected_file is None:
|
||||
continue
|
||||
if not (examples_bin_dir / expected_file).is_file():
|
||||
raise RuntimeError(f"Example `{' '.join(cmd)}` did not produce expected file '{expected_file}'.")
|
||||
produced_files.add(expected_file)
|
||||
finally:
|
||||
for produced_file in produced_files:
|
||||
(examples_bin_dir / produced_file).unlink(missing_ok=True)
|
||||
|
||||
if ARGS.build_examples:
|
||||
test_examples()
|
||||
|
||||
if "IfcOpenShell-Python" in targets:
|
||||
wrapper_ldflags = ""
|
||||
if platform.system() == "Darwin":
|
||||
@@ -1804,7 +1624,8 @@ if "IfcOpenShell-Python" in targets:
|
||||
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
|
||||
f"-DPYTHON_INCLUDE_DIR={python_include}",
|
||||
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
|
||||
"-DUSERSPACE_PYTHON_PREFIX=" + OFF_ON[PYTHON_USER_SITE],
|
||||
"-DUSERSPACE_PYTHON_PREFIX="
|
||||
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
|
||||
],
|
||||
cmake_dir=CMAKE_DIR,
|
||||
cwd=ifcos_build_dir,
|
||||
@@ -1857,7 +1678,6 @@ if "IfcOpenShell-Python" in targets:
|
||||
compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable)
|
||||
else:
|
||||
for python_version in PYTHON_VERSIONS:
|
||||
python_version = python_consider_rc(python_version)
|
||||
python_path = INSTALL_DIR / f"python-{python_version}"
|
||||
module_dir = compile_python_wrapper(python_version, python_path=python_path)
|
||||
assert module_dir
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# ///
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Literal, NamedTuple
|
||||
|
||||
|
||||
class C:
|
||||
GREY = "\033[90m"
|
||||
YELLOW = "\033[33m"
|
||||
RED = "\033[31m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
COLORS = {
|
||||
logging.DEBUG: C.GREY,
|
||||
logging.WARNING: C.YELLOW,
|
||||
logging.ERROR: C.RED,
|
||||
}
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
color = self.COLORS.get(record.levelno, C.RESET)
|
||||
return f"{color}{super().format(record)}{C.RESET}"
|
||||
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(ColorFormatter("%(message)s"))
|
||||
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run(
|
||||
*cmd: str,
|
||||
cwd: Path | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
stderr: int | None = None,
|
||||
) -> str:
|
||||
logger.debug(f"$ {shlex.join(cmd)}")
|
||||
return subprocess.check_output(cmd, cwd=cwd, env=env, stderr=stderr, text=True)
|
||||
|
||||
|
||||
REPO_ROOT = Path(run("git", "-C", str(Path(__file__).parent), "rev-parse", "--show-toplevel").strip())
|
||||
VERSION = "v" + (REPO_ROOT / "VERSION").read_text().strip()
|
||||
|
||||
|
||||
def get_git_sha() -> str:
|
||||
sha = os.getenv("GITHUB_SHA") or run("git", "rev-parse", "HEAD", cwd=REPO_ROOT).strip()
|
||||
return sha[:7]
|
||||
|
||||
|
||||
def is_platform(name: Literal["MAC", "LINUX"]) -> bool:
|
||||
current = "MAC" if platform.system() == "Darwin" else "LINUX"
|
||||
return current == name
|
||||
|
||||
|
||||
def get_install_dir(arch_suffix: str) -> Path:
|
||||
if is_platform("MAC"):
|
||||
pattern = "Darwin/*/*/install"
|
||||
else:
|
||||
if "arm64" in arch_suffix:
|
||||
pattern = "Linux/aarch64/install"
|
||||
else:
|
||||
pattern = "Linux/x86_64/install"
|
||||
for data in (REPO_ROOT / "build").glob(pattern):
|
||||
return data
|
||||
raise Exception("No install dir found")
|
||||
|
||||
|
||||
def find_qt_dir(install_root: Path, qt6_version: str) -> Path | None:
|
||||
for qt_candidate in install_root.glob(f"qt6-{qt6_version}-*/{qt6_version}/*"):
|
||||
if (qt_candidate / "lib").is_dir():
|
||||
return qt_candidate
|
||||
return None
|
||||
|
||||
|
||||
def find_occt_dir(install_root: Path) -> Path:
|
||||
candidates = [candidate for candidate in install_root.glob("occt-shared-*") if candidate.is_dir()]
|
||||
if len(candidates) != 1:
|
||||
raise Exception(f"Expected exactly one OCCT shared candidate, found: {candidates}")
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def ensure_soname_links(paths: list[Path]) -> None:
|
||||
"""Ensure that all shared libraries in `paths` are present using their SONAMEs (at least as symlinks)."""
|
||||
for shared_object in paths:
|
||||
if not shared_object.is_file():
|
||||
continue
|
||||
try:
|
||||
readelf_output = run("readelf", "-d", str(shared_object))
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
match = re.search(r"\(SONAME\).*Library soname: \[(.*)\]", readelf_output)
|
||||
if not match:
|
||||
continue
|
||||
soname = match.group(1)
|
||||
soname_path = shared_object.parent / soname
|
||||
if soname_path.exists():
|
||||
continue
|
||||
soname_path.symlink_to(shared_object.name)
|
||||
|
||||
|
||||
def is_shared_library(path: Path) -> bool:
|
||||
name = path.name.lower()
|
||||
return name.endswith((".so", ".dylib", ".dll")) or ".so." in name
|
||||
|
||||
|
||||
def stage_runtime_payload(install_dir: Path, dest: Path, *, include_geometry_writers: bool = True) -> None:
|
||||
"""Copy all libs from `install_dir/{bin,lib,lib64}` into `dest`."""
|
||||
runtime_files = []
|
||||
for runtime_dir_name in ("bin", "lib", "lib64"):
|
||||
runtime_dir = install_dir / runtime_dir_name
|
||||
if not runtime_dir.is_dir():
|
||||
continue
|
||||
for runtime_file in runtime_dir.rglob("*"):
|
||||
if not (runtime_file.is_symlink() or runtime_file.is_file()):
|
||||
continue
|
||||
if not is_shared_library(runtime_file):
|
||||
continue
|
||||
if not include_geometry_writers and runtime_file.name.startswith("ifcopenshell.geometry.writer."):
|
||||
continue
|
||||
dest_file = dest / runtime_file.name
|
||||
shutil.copy(runtime_file, dest_file, follow_symlinks=False)
|
||||
runtime_files.append(dest_file)
|
||||
if not is_platform("MAC"):
|
||||
ensure_soname_links(runtime_files)
|
||||
|
||||
for lib_so in runtime_files:
|
||||
if lib_so.is_file():
|
||||
run("patchelf", "--set-rpath", "$ORIGIN", str(lib_so))
|
||||
|
||||
|
||||
def stage_qt_runtime_payload(exe_path: Path, dest: Path, qt_dir: Path | None) -> None:
|
||||
"""Copy QT libs/plugins from `qt_dir` next to `exe_path`, if it depends on QT."""
|
||||
|
||||
def is_so_file(path: Path) -> bool:
|
||||
return (path.is_file() or path.is_symlink()) and ".so" in path.name
|
||||
|
||||
if not qt_dir or not (qt_dir / "lib").is_dir():
|
||||
return
|
||||
|
||||
# Skip executables that don't depend on QT (don't have `libQt6` referenced).
|
||||
env = os.environ.copy()
|
||||
env["LD_LIBRARY_PATH"] = f"{qt_dir / 'lib'}:{env.get('LD_LIBRARY_PATH', '')}"
|
||||
try:
|
||||
ldd_output = run("ldd", str(exe_path), env=env)
|
||||
except subprocess.CalledProcessError:
|
||||
return
|
||||
if "libQt6" not in ldd_output:
|
||||
return
|
||||
|
||||
# Copy all QT libs to `dest`.
|
||||
qt_lib_files = []
|
||||
for lib_file in (qt_dir / "lib").iterdir():
|
||||
if is_so_file(lib_file):
|
||||
dest_file = dest / lib_file.name
|
||||
qt_lib_files.append(dest_file)
|
||||
# Currently we install some qt libs to `install/ifcopenshell/lib` too,
|
||||
# so there's a bit of overlap beteen stage_runtime and stage_qt_runtime,
|
||||
# hence the skip.
|
||||
if dest_file.exists():
|
||||
continue
|
||||
shutil.copy(lib_file, dest_file, follow_symlinks=False)
|
||||
ensure_soname_links(qt_lib_files)
|
||||
|
||||
# Copy QT plugins.
|
||||
plugins_dir = qt_dir / "plugins"
|
||||
if plugins_dir.is_dir():
|
||||
for plugin_file in plugins_dir.rglob("*"):
|
||||
if not is_so_file(plugin_file):
|
||||
continue
|
||||
dest_plugin_file = dest / "plugins" / plugin_file.relative_to(plugins_dir)
|
||||
dest_plugin_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(plugin_file, dest_plugin_file, follow_symlinks=False)
|
||||
|
||||
# Point plugins rpath to `dest`.
|
||||
dest_plugins_dir = dest / "plugins"
|
||||
if dest_plugins_dir.is_dir():
|
||||
for plugin_so in dest_plugins_dir.rglob("*.so*"):
|
||||
if plugin_so.is_file():
|
||||
run("patchelf", "--set-rpath", "$ORIGIN/../..:$ORIGIN", str(plugin_so))
|
||||
|
||||
# Non-recursive, set rpath only for top-level libs.
|
||||
for lib_so in qt_lib_files:
|
||||
if lib_so.is_file():
|
||||
run("patchelf", "--set-rpath", "$ORIGIN", str(lib_so))
|
||||
|
||||
qt_conf_path = dest / "qt.conf"
|
||||
qt_conf_path.write_text("[Paths]\nPrefix = .\n")
|
||||
|
||||
|
||||
KNOWN_EXCEPTIONS = frozenset(
|
||||
(
|
||||
# Optional Qt SQL driver plugins we don't ship the client libs for.
|
||||
"libqsqlpsql.so",
|
||||
"libqsqlmysql.so",
|
||||
"libqsqlmimer.so",
|
||||
"libqsqlodbc.so",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def check_runtime_dependencies(package_dir: Path) -> None:
|
||||
"""Check all binaries in `package_dir` and report if they're still missing dependencies or are static."""
|
||||
|
||||
def is_executable_or_so(path: Path) -> bool:
|
||||
name = path.name
|
||||
return os.access(path, os.X_OK) or name.endswith(".so") or ".so." in name
|
||||
|
||||
missing = False
|
||||
env = os.environ.copy()
|
||||
env.pop("LD_LIBRARY_PATH", None)
|
||||
|
||||
for binary_file in package_dir.rglob("*"):
|
||||
if not binary_file.is_file() or not is_executable_or_so(binary_file):
|
||||
continue
|
||||
|
||||
# Skip non-binaries.
|
||||
try:
|
||||
run("readelf", "-h", str(binary_file), stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
|
||||
try:
|
||||
ldd_output = run("ldd", str(binary_file), env=env, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"ldd failed for {binary_file}")
|
||||
logger.error(e.output)
|
||||
missing = True
|
||||
continue
|
||||
|
||||
if "not found" in ldd_output:
|
||||
is_known = binary_file.name in KNOWN_EXCEPTIONS
|
||||
log = logger.debug if is_known else logger.warning
|
||||
log(f"Missing runtime dependencies for {binary_file}")
|
||||
for line in ldd_output.splitlines():
|
||||
if "not found" in line:
|
||||
log(line)
|
||||
if not is_known:
|
||||
missing = True
|
||||
|
||||
# TODO: should error?
|
||||
if missing:
|
||||
logger.warning("Runtime dependency check found issues; continuing packaging.")
|
||||
|
||||
|
||||
def package_python_wrapper(
|
||||
py_dir: Path,
|
||||
ifcopenshell_install_dir: Path,
|
||||
github_sha: str,
|
||||
output_dir: Path,
|
||||
arch_suffix: str,
|
||||
occt_dir: Path | None,
|
||||
) -> None:
|
||||
logger.info(f"Packaging python wrapper '{py_dir.name}'")
|
||||
py_version = py_dir.name
|
||||
postfix = "" if py_version[-1].isdigit() else py_version[-1]
|
||||
# Match and convert `x.y` -> `xy`.
|
||||
version_match = re.search(r"[0-9]+\.[0-9]+", py_version)
|
||||
assert version_match
|
||||
numbers = "".join(version_match.group().split("."))
|
||||
py_version_major = f"python-{numbers}{postfix}"
|
||||
|
||||
package_dir = ifcopenshell_install_dir / f".package-{py_version_major}"
|
||||
if package_dir.exists():
|
||||
# Clean up previous local runs.
|
||||
shutil.rmtree(package_dir)
|
||||
package_dir.mkdir(parents=True)
|
||||
|
||||
ifcopenshell_dir = package_dir / "ifcopenshell"
|
||||
ifcopenshell_dir.mkdir()
|
||||
for item in py_dir.iterdir():
|
||||
dest = ifcopenshell_dir / item.name
|
||||
if item.is_dir():
|
||||
shutil.copytree(item, dest, symlinks=True)
|
||||
else:
|
||||
shutil.copy(item, dest, follow_symlinks=False)
|
||||
|
||||
if not is_platform("MAC"):
|
||||
for lib_so in ifcopenshell_dir.glob("*.so*"):
|
||||
if lib_so.is_file():
|
||||
run("patchelf", "--set-rpath", "$ORIGIN", str(lib_so))
|
||||
|
||||
# Cache from test run during build.
|
||||
pycache_dir = ifcopenshell_dir / "__pycache__"
|
||||
if pycache_dir.is_dir():
|
||||
shutil.rmtree(pycache_dir)
|
||||
for pyc_file in ifcopenshell_dir.rglob("*.pyc"):
|
||||
pyc_file.unlink()
|
||||
|
||||
# TODO: packs qt libs also?
|
||||
stage_runtime_payload(ifcopenshell_install_dir, ifcopenshell_dir)
|
||||
|
||||
if occt_dir:
|
||||
stage_runtime_payload(occt_dir, ifcopenshell_dir)
|
||||
|
||||
if not is_platform("MAC"):
|
||||
check_runtime_dependencies(ifcopenshell_dir)
|
||||
|
||||
zip_path = output_dir / f"ifcopenshell-{py_version_major}-{VERSION}-{github_sha}-{arch_suffix}.zip"
|
||||
run("zip", "-y", "-r", "-qq", "-1", str(zip_path), "ifcopenshell", cwd=package_dir)
|
||||
shutil.rmtree(package_dir)
|
||||
|
||||
|
||||
def is_packageable_executable(path: Path) -> bool:
|
||||
if not path.is_file() or not os.access(path, os.X_OK):
|
||||
return False
|
||||
return not (path.name.lower().endswith(".zip") or is_shared_library(path))
|
||||
|
||||
|
||||
def package_executable(
|
||||
exe_path: Path,
|
||||
ifcopenshell_install_dir: Path,
|
||||
github_sha: str,
|
||||
output_dir: Path,
|
||||
autodesk_connector_dir: Path,
|
||||
qt_dir: Path | None,
|
||||
occt_dir: Path | None,
|
||||
arch_suffix: str,
|
||||
) -> None:
|
||||
exe = exe_path.name
|
||||
logger.info(f"Packaging executable '{exe}'")
|
||||
package_dir = ifcopenshell_install_dir / f".package-{exe}"
|
||||
if package_dir.exists():
|
||||
# Clean up previous local runs.
|
||||
shutil.rmtree(package_dir)
|
||||
package_dir.mkdir(parents=True)
|
||||
|
||||
shutil.copy(exe_path, package_dir / exe)
|
||||
# TODO: kept `is_platform(MAC)` to retain original bash script behaviour,
|
||||
# but is this guard needed or it should be always False?
|
||||
stage_runtime_payload(ifcopenshell_install_dir, package_dir, include_geometry_writers=is_platform("MAC"))
|
||||
|
||||
if occt_dir:
|
||||
stage_runtime_payload(occt_dir, package_dir)
|
||||
|
||||
# On macOS, rpath is already set at build time via CMake's INSTALL_RPATH, and
|
||||
# QT apps are packaged as .app bundles (`package_app_bundle`) instead.
|
||||
if not is_platform("MAC"):
|
||||
run("patchelf", "--set-rpath", "$ORIGIN", str(package_dir / exe))
|
||||
stage_qt_runtime_payload(exe_path, package_dir, qt_dir)
|
||||
|
||||
if exe == "BonsaiViewer":
|
||||
connectors_dir = package_dir / "connectors"
|
||||
connectors_dir.mkdir()
|
||||
shutil.copytree(autodesk_connector_dir, connectors_dir / autodesk_connector_dir.name, symlinks=True)
|
||||
|
||||
check_runtime_dependencies(package_dir)
|
||||
|
||||
zip_path = output_dir / f"{exe}-{VERSION}-{github_sha}-{arch_suffix}.zip"
|
||||
run("zip", "-y", "-qq", "-r", str(zip_path), ".", cwd=package_dir)
|
||||
shutil.rmtree(package_dir)
|
||||
|
||||
|
||||
def package_app_bundle(
|
||||
app_path: Path,
|
||||
install_root: Path,
|
||||
github_sha: str,
|
||||
output_dir: Path,
|
||||
autodesk_connector_dir: Path,
|
||||
arch_suffix: str,
|
||||
) -> None:
|
||||
"""Zip a `.app` bundle (e.g. BonsaiViewer.app) living at the install-prefix root.
|
||||
|
||||
Their install rule uses `BUNDLE DESTINATION "."` - that's the layout Qt's
|
||||
macdeployqt expects. macdeployqt has already embedded the Qt frameworks
|
||||
inside each bundle during install/strip, so the only thing left to stage
|
||||
is the connector.
|
||||
"""
|
||||
app = app_path.stem
|
||||
logger.info(f"Packaging app bundle '{app}'")
|
||||
|
||||
if app == "BonsaiViewer":
|
||||
# ConnectorDiscovery looks in applicationDirPath()/connectors,
|
||||
# which for a bundle is Contents/MacOS.
|
||||
connectors_dir = app_path / "Contents" / "MacOS" / "connectors"
|
||||
connectors_dir.mkdir(parents=True)
|
||||
shutil.copytree(autodesk_connector_dir, connectors_dir / autodesk_connector_dir.name, symlinks=True)
|
||||
|
||||
zip_path = output_dir / f"{app}-{VERSION}-{github_sha}-{arch_suffix}.zip"
|
||||
run("zip", "-qq", "-r", str(zip_path), app_path.name, cwd=install_root)
|
||||
|
||||
|
||||
ARCH_SUFFIXES = ("linux64", "linuxarm64", "macosm164")
|
||||
LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR")
|
||||
|
||||
|
||||
class Args(NamedTuple):
|
||||
arch_suffix: str
|
||||
log_level: str
|
||||
occt_shared: bool
|
||||
|
||||
|
||||
ARGS: Args
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("arch_suffix", choices=ARCH_SUFFIXES, help="Zip filename suffix.")
|
||||
# TODO: relax default to INFO once things get more stable.
|
||||
parser.add_argument("--log-level", default="DEBUG", choices=LOG_LEVELS, help="Logging verbosity.")
|
||||
# TODO: add `--shared`.
|
||||
parser.add_argument("--occt-shared", action="store_true", help="OCCT was built as shared libraries.")
|
||||
args = parser.parse_args()
|
||||
|
||||
global ARGS
|
||||
ARGS = Args(arch_suffix=args.arch_suffix, log_level=args.log_level, occt_shared=args.occt_shared)
|
||||
logger.setLevel(ARGS.log_level)
|
||||
|
||||
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
|
||||
# invokes `cargo build --release` and stages the binary +
|
||||
# connector.json into dist/autodesk/. Same on-disk shape as the
|
||||
# old PyInstaller flow so the symlink + zip steps below
|
||||
# continue to work unchanged.
|
||||
run("uv", "run", str(REPO_ROOT / "src/bonsaiviewer-autodesk/packaging/build.py"))
|
||||
autodesk_connector_dir = REPO_ROOT / "src/bonsaiviewer-autodesk/dist/autodesk"
|
||||
assert autodesk_connector_dir.is_dir()
|
||||
|
||||
# Locate the ifcopenshell install dir and stage QT6 alongside the zip output.
|
||||
install_root = get_install_dir(ARGS.arch_suffix)
|
||||
ifcopenshell_install_dir = install_root / "ifcopenshell"
|
||||
|
||||
output_dir = Path.home() / "output"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
qt6_version = os.getenv("QT6_VERSION", "6.8.3")
|
||||
qt_dir_env = os.getenv("QT_DIR")
|
||||
qt_dir = Path(qt_dir_env) if qt_dir_env else find_qt_dir(install_root, qt6_version)
|
||||
|
||||
occt_dir = find_occt_dir(install_root) if ARGS.occt_shared else None
|
||||
|
||||
# Iterate over all built Python wrappers in `install/ifcopenshell/python-x.y.z`
|
||||
# and zip them, bundling all dynamic libs from `lib`.
|
||||
github_sha = get_git_sha()
|
||||
for py_dir in sorted(ifcopenshell_install_dir.glob("python-*")):
|
||||
package_python_wrapper(py_dir, ifcopenshell_install_dir, github_sha, output_dir, ARGS.arch_suffix, occt_dir)
|
||||
|
||||
# Iterate over all executables in `install/ifcopenshell/bin` and zip them.
|
||||
# Each zip bundles dynamic libs from `lib` and also qt libs.
|
||||
bin_dir = ifcopenshell_install_dir / "bin"
|
||||
for exe_path in sorted(bin_dir.iterdir()):
|
||||
if is_packageable_executable(exe_path):
|
||||
package_executable(
|
||||
exe_path,
|
||||
ifcopenshell_install_dir,
|
||||
github_sha,
|
||||
output_dir,
|
||||
autodesk_connector_dir,
|
||||
qt_dir,
|
||||
occt_dir,
|
||||
ARGS.arch_suffix,
|
||||
)
|
||||
|
||||
if is_platform("MAC"):
|
||||
for app_path in sorted(install_root.glob("*.app")):
|
||||
package_app_bundle(app_path, install_root, github_sha, output_dir, autodesk_connector_dir, ARGS.arch_suffix)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,13 +1,3 @@
|
||||
# Removing use of `tr1` namespace that might not be available on some systems.
|
||||
#
|
||||
# Current status on different systems:
|
||||
# - msvc - removed `tr1` namespace in 14.51 (`_MSC_VER == 1951`)
|
||||
# - gcc (with libstdc++) - currently neither deprecated nor removed, though there are plans to
|
||||
# - clang (with libc++) - never had it
|
||||
#
|
||||
# One of the hunks in the patch is patching `_MSC_VER == 1500`, so it's not stricly needed,
|
||||
# but kept it just so it will be easy to check the absense of any `tr1` use.
|
||||
|
||||
diff --git a/COLLADABaseUtils/include/COLLADABUhash_map.h b/COLLADABaseUtils/include/COLLADABUhash_map.h
|
||||
index 8ab0fb9b..12503bfb 100644
|
||||
--- a/COLLADABaseUtils/include/COLLADABUhash_map.h
|
||||
@@ -37,13 +27,11 @@ index 8ab0fb9b..12503bfb 100644
|
||||
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
|
||||
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
|
||||
- #define COLLADABU_HASH_SET std::tr1::unordered_set
|
||||
- #define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
|
||||
- #define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
+ #define COLLADABU_HASH_MAP std::unordered_map
|
||||
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
|
||||
+ #define COLLADABU_HASH_SET std::unordered_set
|
||||
+ #define COLLADABU_HASH_NAMESPACE_OPEN std
|
||||
+ #define COLLADABU_HASH_NAMESPACE_CLOSE
|
||||
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
#define COLLADABU_HASH_FUN hash
|
||||
@@ -107,12 +107,12 @@
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE
|
||||
@@ -57,13 +45,11 @@ index 8ab0fb9b..12503bfb 100644
|
||||
- #define COLLADABU_HASH_MAP std::tr1::unordered_map
|
||||
- #define COLLADABU_HASH_MULTIMAP std::tr1::unordered_multimap
|
||||
- #define COLLADABU_HASH_SET std::tr1::unordered_set
|
||||
- #define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
|
||||
- #define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
+ #define COLLADABU_HASH_MAP std::unordered_map
|
||||
+ #define COLLADABU_HASH_MULTIMAP std::unordered_multimap
|
||||
+ #define COLLADABU_HASH_SET std::unordered_set
|
||||
+ #define COLLADABU_HASH_NAMESPACE_OPEN std
|
||||
+ #define COLLADABU_HASH_NAMESPACE_CLOSE
|
||||
#define COLLADABU_HASH_NAMESPACE_OPEN std { namespace tr1
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
#define COLLADABU_HASH_FUN hash
|
||||
diff --git a/common/libBuffer/include/CommonFWriteBufferFlusher.h b/common/libBuffer/include/CommonFWriteBufferFlusher.h
|
||||
index c7af45b2..fac4f133 100644
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/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
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ source:
|
||||
|
||||
build:
|
||||
script: |
|
||||
BUILD_CFG=Release python nix/build-all.py -v --wasm
|
||||
BUILD_CFG=Release python nix/build-all.py -v --wasm --py313
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
|
||||
+5
-16
@@ -6,6 +6,11 @@ version = "0.0.0"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
include = '''
|
||||
src/.*.pyi?$
|
||||
|nix/.*.pyi?$
|
||||
|pyodide/.*.pyi?$
|
||||
'''
|
||||
extend-exclude = '''
|
||||
src/ifcopenshell-python/ifcopenshell/express/rules/*
|
||||
|src/ifcopenshell-python/ifcopenshell/express/express_parser.py
|
||||
@@ -14,15 +19,6 @@ extend-exclude = '''
|
||||
|src/ifc2ca/templates/*
|
||||
|src/svgfill
|
||||
|src/exterior-shell-extractor
|
||||
|choco/bonsai/tools/enable_blenderbim_addon.py
|
||||
|choco/bonsai/tools/disable_blenderbim_addon.py
|
||||
|docs/conf.py
|
||||
|docs/generate_docs.py
|
||||
|aws/lambda/example_handler/__init__.py
|
||||
|conda/update_version_init.py
|
||||
|test/bpy.py
|
||||
|test/tests.py
|
||||
|test/run.py
|
||||
'''
|
||||
|
||||
[tool.pyright]
|
||||
@@ -113,13 +109,10 @@ unresolved-attribute = "ignore"
|
||||
invalid-argument-type = "ignore"
|
||||
invalid-method-override = "ignore"
|
||||
invalid-assignment = "ignore"
|
||||
unsound-assignment = "ignore"
|
||||
invalid-parameter-default = "ignore"
|
||||
missing-override-decorator = "ignore"
|
||||
invalid-yield = "ignore"
|
||||
unsound-yield = "ignore"
|
||||
invalid-return-type = "ignore"
|
||||
unsound-return-statement = "ignore"
|
||||
non-callable-init-subclass = "ignore"
|
||||
not-iterable = "ignore"
|
||||
possibly-missing-attribute = "ignore"
|
||||
@@ -127,8 +120,6 @@ no-matching-overload = "ignore"
|
||||
not-subscriptable = "ignore"
|
||||
unsupported-dynamic-base = "ignore"
|
||||
unsupported-operator = "ignore"
|
||||
# `@persistent` is incorrectly annotated as `Any` in fake-bpy, needs to be resolved upstream.
|
||||
dynamic-function-decorator-return = "ignore"
|
||||
|
||||
[tool.ty.environment]
|
||||
extra-paths = [
|
||||
@@ -190,8 +181,6 @@ dev-setup.help = "Install repo packages in editable mode"
|
||||
|
||||
ruff = "ruff check"
|
||||
|
||||
check-whitespace = "uv run .github/scripts/check-whitespace.py"
|
||||
|
||||
black = "black ."
|
||||
|
||||
ty.sequence = ["ty-bonsai", "ty-ios"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
black==26.5.1
|
||||
ruff==0.16.4
|
||||
black==26.3.1
|
||||
ruff==0.16.0
|
||||
poethepoet
|
||||
ty==0.0.74
|
||||
ty==0.0.63
|
||||
gersemi==0.28.0
|
||||
|
||||
+23
-4
@@ -69,17 +69,32 @@ endif # def PYVERSION
|
||||
IFCMERGE_VERSION:=2026-04-07
|
||||
|
||||
ifdef PLATFORM
|
||||
SUPPORTED_PLATFORMS := linux macosm1 win
|
||||
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
||||
|
||||
ifeq ($(filter $(PLATFORM),$(SUPPORTED_PLATFORMS)),)
|
||||
$(error Unsupported PLATFORM=$(PLATFORM). Must be one of $(SUPPORTED_PLATFORMS))
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM),macos)
|
||||
ifeq ($(PYVERSION),py313)
|
||||
$(error Blender 5.1 with Python 3.13 doesn't support intel macOS.)
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), linux)
|
||||
PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
|
||||
BLENDER_PLATFORM:=linux-x64
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), macos)
|
||||
ifeq ($(PYVERSION), py311)
|
||||
PYPI_PLATFORM:=--platform macosx_10_10_x86_64
|
||||
else
|
||||
PYPI_PLATFORM:=--platform macosx_10_13_x86_64
|
||||
endif
|
||||
BLENDER_PLATFORM:=macos-x64
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), macosm1)
|
||||
PYPI_PLATFORM:=--platform macosx_11_0_arm64
|
||||
BLENDER_PLATFORM:=macos-arm64
|
||||
@@ -93,7 +108,7 @@ endif
|
||||
endif # def PLATFORM
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=ad113e1
|
||||
OLD:=3e7b739
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -179,8 +194,10 @@ endif
|
||||
# Provides networkx graph analysis for project dependency calculations
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download networkx --dest=./wheels
|
||||
# Required by IFCDiff
|
||||
# Pinned <9.1: deepdiff 9.1.0 adds the compiled dependency cachebox<6,>=5.2,
|
||||
# which this platformless download cannot provide for every target platform.
|
||||
# Pinned <9.1: deepdiff 9.1.0 adds cachebox<6,>=5.2 which only ships macOS x86_64
|
||||
# wheels for macosx_10_12+ and is incompatible with our macos py311 --platform
|
||||
# macosx_10_10_x86_64 target. Revisit once the macos py311 platform tag is bumped
|
||||
# to 10_13 (matching py312/py313).
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download "deepdiff<9.1" --dest=./wheels
|
||||
# Required by IFCCSV and ifcopenshell.util.selector
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lark --dest=./wheels
|
||||
@@ -198,6 +215,8 @@ endif
|
||||
# pyradiance is using different platform versions than defaults in our makefile.
|
||||
ifeq ($(PLATFORM), linux)
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform manylinux_2_28_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
else ifeq ($(PLATFORM), macos)
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform macosx_10_13_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
else
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
endif
|
||||
|
||||
@@ -59,6 +59,7 @@ from bonsai.bim.module.model.decorator import (
|
||||
)
|
||||
from bonsai.bim.module.model.wall import WallGizmoPreviewDecorator
|
||||
from bonsai.bim.module.nest.decorator import NestDecorator
|
||||
from bonsai.tool.spatial import install_geom_cache_handlers, uninstall_geom_cache_handlers
|
||||
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
global_subscription_owner = object()
|
||||
@@ -121,9 +122,25 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
|
||||
def active_object_callback():
|
||||
refresh_ui_data()
|
||||
update_bim_tool_props()
|
||||
update_spatial_tool_props()
|
||||
tool.Geometry.sync_item_positions()
|
||||
|
||||
|
||||
def update_spatial_tool_props():
|
||||
"""Sync ``BIMSpatialDecompositionProperties.space_height`` with the
|
||||
active object's height when it is an ``IfcSpace``, otherwise reset to
|
||||
the 3m default. Called from the msgbus active-object callback so Scene
|
||||
property writes happen outside ``draw()``."""
|
||||
obj = tool.Blender.get_active_object()
|
||||
props = tool.Spatial.get_spatial_props()
|
||||
if obj:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and element.is_a("IfcSpace"):
|
||||
props.space_height = obj.dimensions.z
|
||||
return
|
||||
props.space_height = 3
|
||||
|
||||
|
||||
def update_bim_tool_props():
|
||||
"""Selection-driven BIM Tool sync: re-target user-intent enums
|
||||
(ifc_class, relating_type_id) AND refresh header values
|
||||
@@ -528,6 +545,7 @@ def _install_viewport_overlays() -> None:
|
||||
ArrayPreviewDecorator.uninstall()
|
||||
ArraySelectionHighlightDecorator.uninstall()
|
||||
uninstall_decorator_cache_handlers()
|
||||
uninstall_geom_cache_handlers()
|
||||
try:
|
||||
if georeference_props.should_visualise:
|
||||
GeoreferenceDecorator.install(bpy.context)
|
||||
@@ -570,6 +588,7 @@ def _install_viewport_overlays() -> None:
|
||||
ArrayPreviewDecorator.install(bpy.context)
|
||||
finally:
|
||||
install_decorator_cache_handlers()
|
||||
install_geom_cache_handlers()
|
||||
|
||||
|
||||
@persistent
|
||||
|
||||
@@ -23,6 +23,7 @@ from . import operator, prop, ui
|
||||
classes = (
|
||||
operator.AddBoundary,
|
||||
operator.ColourByRelatedBuildingElement,
|
||||
operator.CopyBoundaryAttributeToSelection,
|
||||
operator.DecorateBoundaries,
|
||||
operator.DisableEditingBoundary,
|
||||
operator.DisableEditingBoundaryGeometry,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import logging
|
||||
import multiprocessing
|
||||
from math import acos, degrees, inf, pi, radians
|
||||
from math import inf, pi
|
||||
from typing import Optional, Union
|
||||
|
||||
import bmesh
|
||||
@@ -28,6 +28,7 @@ import ifcopenshell.api.boundary
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.ifcopenshell_wrapper as W
|
||||
import ifcopenshell.util.boundary
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.shape
|
||||
@@ -39,6 +40,7 @@ from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.bim.import_ifc as import_ifc
|
||||
import bonsai.core.attribute as core
|
||||
import bonsai.core.geometry
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
@@ -422,6 +424,32 @@ class EditBoundaryAttributes(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CopyBoundaryAttributeToSelection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.copy_boundary_attribute_to_selection"
|
||||
bl_label = "Copy Boundary Attribute To Selection"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
name: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = tool.Blender.get_active_object()
|
||||
assert obj
|
||||
bprops = tool.Boundary.get_object_boundary_props(obj)
|
||||
if self.name in EDITABLE_ATTRIBUTES:
|
||||
blender_prop = EDITABLE_ATTRIBUTES[self.name]
|
||||
blender_obj = getattr(bprops, blender_prop, None)
|
||||
value = tool.Ifc.get_entity(blender_obj) if blender_obj else None
|
||||
elif self.name == "PhysicalOrVirtualBoundary":
|
||||
value = bprops.physical_or_virtual
|
||||
elif self.name == "InternalOrExternalBoundary":
|
||||
value = bprops.internal_or_external
|
||||
else:
|
||||
return
|
||||
total = core.copy_attribute_to_selection(
|
||||
tool.Ifc, tool.Blender, tool.Root, tool.Spatial, name=self.name, value=value
|
||||
)
|
||||
self.report({"INFO"}, f"Attribute was successfully copied to {total} elements.")
|
||||
|
||||
|
||||
class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.update_boundary_geometry"
|
||||
bl_label = "Update Boundary Geometry"
|
||||
@@ -668,36 +696,30 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def auto_generate_boundaries(
|
||||
self, space: ifcopenshell.entity_instance, space_obj: bpy.types.Object
|
||||
) -> Union[str, list[ifcopenshell.entity_instance]]:
|
||||
"""
|
||||
:return: list of created boundaries or a string with error description.
|
||||
"""Generate boundaries by delegating to ifcopenshell.util.boundary.
|
||||
|
||||
This method handles Blender-specific preprocessing (flushing moved
|
||||
objects, building the geometry cache + spatial tree) then delegates
|
||||
the algorithm to the Blender-independent util module.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
props = tool.Model.get_model_props()
|
||||
boundaries: list[ifcopenshell.entity_instance] = []
|
||||
assert isinstance(space_obj.data, bpy.types.Mesh)
|
||||
|
||||
# Identify all potential building elements
|
||||
# TODO: don't select everything, use AABB culling in Blender
|
||||
building_elements = list(
|
||||
tool.Ifc.get().by_type("IfcWall")
|
||||
+ tool.Ifc.get().by_type("IfcSlab")
|
||||
+ tool.Ifc.get().by_type("IfcVirtualElement")
|
||||
)
|
||||
building_elements = []
|
||||
for ifc_class in ifcopenshell.util.boundary.BOUNDARY_ELEMENT_CLASSES:
|
||||
building_elements.extend(ifc_file.by_type(ifc_class))
|
||||
|
||||
# Flush moved objects to IFC
|
||||
for building_element in building_elements:
|
||||
if obj := tool.Ifc.get_object(building_element):
|
||||
if tool.Ifc.is_moved(obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
|
||||
if tool.Ifc.is_moved(space_obj):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=space_obj)
|
||||
|
||||
# Don't generate boundaries of building elements that we've already got bounaries for.
|
||||
for boundary in space.BoundedBy:
|
||||
if boundary.RelatedBuildingElement in building_elements:
|
||||
building_elements.remove(boundary.RelatedBuildingElement)
|
||||
|
||||
# Create tree of gross shapes of all potential related building elements
|
||||
# Build shapes dict with iterator (parallel, includes space + building elements)
|
||||
include = building_elements + [space]
|
||||
tree = ifcopenshell.geom.tree()
|
||||
shapes = {}
|
||||
@@ -712,189 +734,23 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
shapes[shape.id] = {
|
||||
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
|
||||
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
|
||||
"edges": ifcopenshell.util.shape.get_edges(shape.geometry),
|
||||
"matrix": ifcopenshell.util.shape.get_shape_matrix(shape),
|
||||
}
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
# Spatially query all potential boundary elements via a 100mm extension of the space
|
||||
building_elements = [e for e in tree.select(space, extend=0.1) if e != space]
|
||||
# Pass all building element shapes to the auto-generation function.
|
||||
# The function performs its own spatial filtering (coplanarity + overlap),
|
||||
# so tree-adjacency filtering is not needed here.
|
||||
filtered_shapes = {space.id(): shapes[space.id()]}
|
||||
for element in building_elements:
|
||||
if element.id() in shapes:
|
||||
filtered_shapes[element.id()] = shapes[element.id()]
|
||||
|
||||
if not building_elements:
|
||||
return "No building elements found to create boundaries."
|
||||
|
||||
# Create a dissolved bmesh for the space
|
||||
space_bm = bmesh.new()
|
||||
space_bm.from_mesh(space_obj.data)
|
||||
bmesh.ops.dissolve_limit(space_bm, angle_limit=pi * 2 / 360, verts=space_bm.verts[:], edges=space_bm.edges[:])
|
||||
|
||||
# Create dissolved bmeshes for all boundary elements
|
||||
building_element_bms = {}
|
||||
for building_element in building_elements:
|
||||
bm = bmesh.new()
|
||||
shape = shapes[building_element.id()]
|
||||
|
||||
for vert in shape["verts"]:
|
||||
bm.verts.new(Vector(vert))
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
for face in shape["faces"]:
|
||||
bm.faces.new([bm.verts[i] for i in face])
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.faces.ensure_lookup_table()
|
||||
bm.normal_update() # Needed so that dissolve_limit will work.
|
||||
bmesh.ops.dissolve_limit(bm, angle_limit=radians(1), verts=bm.verts[:], edges=bm.edges[:])
|
||||
bm.verts.ensure_lookup_table()
|
||||
bm.faces.ensure_lookup_table()
|
||||
building_element_bms[building_element.id()] = bm
|
||||
|
||||
# Compare space faces and building element faces to see if they relate to one another
|
||||
for space_face in space_bm.faces:
|
||||
space_face_normal = space_obj.matrix_world.to_3x3() @ space_face.normal
|
||||
space_face_vert = space_obj.matrix_world @ space_face.verts[0].co
|
||||
for building_element in building_elements:
|
||||
for face in building_element_bms[building_element.id()].faces:
|
||||
building_obj = tool.Ifc.get_object(building_element)
|
||||
face_normal = building_obj.matrix_world.to_3x3() @ face.normal
|
||||
angle = degrees(acos(max(min(space_face_normal.dot(face_normal), 1), -1)))
|
||||
if tool.Cad.is_x(angle, 180, tolerance=2):
|
||||
pass # Faces need to be parallel and have opposite normals to be related.
|
||||
elif building_element.is_a("IfcVirtualElement") and tool.Cad.is_x(angle, 0, tolerance=2):
|
||||
pass # Virtual elements only need to be parallel to be related, since they are planes.
|
||||
else:
|
||||
continue
|
||||
|
||||
# Both faces should be close to one another. Say within 50mm.
|
||||
space_vert = building_obj.matrix_world.inverted() @ space_face_vert
|
||||
dist = mathutils.geometry.distance_point_to_plane(space_vert, face.verts[0].co, face.normal)
|
||||
if abs(dist) > 0.05:
|
||||
continue
|
||||
|
||||
# Project the building element face onto the space face
|
||||
space_face_verts = [v.co.copy() for v in space_face.verts]
|
||||
space_face_matrix = self.get_face_matrix(*[v.copy() for v in space_face_verts[0:3]])
|
||||
space_face_matrix_i = space_face_matrix.inverted()
|
||||
|
||||
space_face_polygon = shapely.Polygon(
|
||||
[tuple((space_face_matrix_i @ v).xy) for v in space_face_verts]
|
||||
)
|
||||
|
||||
space_matrix_world_i = space_obj.matrix_world.inverted()
|
||||
face_verts = [space_matrix_world_i @ building_obj.matrix_world @ v.co.copy() for v in face.verts]
|
||||
face_polygon = shapely.Polygon([tuple((space_face_matrix_i @ v).xy) for v in face_verts])
|
||||
|
||||
gross_boundary_polygon = space_face_polygon.intersection(face_polygon)
|
||||
|
||||
if type(gross_boundary_polygon) == shapely.GeometryCollection:
|
||||
for geom in gross_boundary_polygon.geoms:
|
||||
if type(geom) == shapely.Polygon:
|
||||
gross_boundary_polygon = geom
|
||||
break
|
||||
|
||||
if (
|
||||
not (isinstance(gross_boundary_polygon, shapely.Polygon) and gross_boundary_polygon.is_valid)
|
||||
or gross_boundary_polygon.is_empty
|
||||
):
|
||||
continue
|
||||
|
||||
# The gross boundary polygon may not be a true gross boundary since it
|
||||
# may have openings already removed, such as in IFC4 Reference View. So
|
||||
# we cheat by using the exterior boundary to mean "gross".
|
||||
exterior_boundary_polygon = shapely.Polygon(gross_boundary_polygon.exterior.coords)
|
||||
|
||||
parent_boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
if building_element.is_a("IfcVirtualElement"):
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
else:
|
||||
parent_boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
parent_boundary.InternalOrExternalBoundary = "NOTDEFINED"
|
||||
if building_element.is_a("IfcWall"):
|
||||
is_external = ifcopenshell.util.element.get_pset(
|
||||
building_element, "Pset_WallCommon", "IsExternal"
|
||||
)
|
||||
if is_external is True:
|
||||
parent_boundary.InternalOrExternalBoundary = "EXTERNAL"
|
||||
elif is_external is False:
|
||||
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
|
||||
elif building_element.is_a("IfcSlab"):
|
||||
predefined_type = ifcopenshell.util.element.get_predefined_type(building_element)
|
||||
if predefined_type == "BASESLAB":
|
||||
parent_boundary.InternalOrExternalBoundary = "EXTERNAL_EARTH"
|
||||
else:
|
||||
is_external = ifcopenshell.util.element.get_pset(
|
||||
building_element, "Pset_SlabCommon", "IsExternal"
|
||||
)
|
||||
if is_external is True:
|
||||
parent_boundary.InternalOrExternalBoundary = "EXTERNAL"
|
||||
elif is_external is False:
|
||||
parent_boundary.InternalOrExternalBoundary = "INTERNAL"
|
||||
parent_boundary.RelatingSpace = space
|
||||
parent_boundary.RelatedBuildingElement = building_element
|
||||
parent_boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
|
||||
exterior_boundary_polygon, space_face_matrix
|
||||
)
|
||||
self.set_boundary_name(parent_boundary)
|
||||
boundaries.append(parent_boundary)
|
||||
|
||||
for rel in getattr(building_element, "HasOpenings", []):
|
||||
opening = rel.RelatedOpeningElement
|
||||
filling = opening.HasFillings[0].RelatedBuildingElement if opening.HasFillings else None
|
||||
|
||||
# Create shape of opening as a dissolved BMesh
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, opening)
|
||||
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
|
||||
opening_bm = bmesh.new()
|
||||
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
|
||||
for vert in verts:
|
||||
opening_bm.verts.new(Vector(vert))
|
||||
opening_bm.verts.ensure_lookup_table()
|
||||
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
|
||||
for face in faces:
|
||||
opening_bm.faces.new([opening_bm.verts[i] for i in face])
|
||||
opening_bm.verts.ensure_lookup_table()
|
||||
opening_bm.faces.ensure_lookup_table()
|
||||
opening_bm.normal_update() # Needed so that dissolve_limit will work.
|
||||
bmesh.ops.dissolve_limit(
|
||||
opening_bm, angle_limit=radians(1), verts=opening_bm.verts[:], edges=opening_bm.edges[:]
|
||||
)
|
||||
opening_bm.verts.ensure_lookup_table()
|
||||
opening_bm.faces.ensure_lookup_table()
|
||||
|
||||
# Get relevant faces of BMesh that can turn into boundaries
|
||||
opening_polygons = []
|
||||
for opening_face in opening_bm.faces:
|
||||
opening_face_normal = mat.to_3x3() @ opening_face.normal
|
||||
angle = degrees(acos(max(min(opening_face_normal.dot(face_normal), 1), -1)))
|
||||
if not tool.Cad.is_x(angle, 180, tolerance=2):
|
||||
continue # Any non-parallel faces are not relevant
|
||||
opening_face_verts = [space_matrix_world_i @ mat @ v.co.copy() for v in opening_face.verts]
|
||||
polygon = shapely.Polygon([tuple((space_face_matrix_i @ v).xy) for v in opening_face_verts])
|
||||
opening_polygons.append(polygon)
|
||||
|
||||
# Merge them all into a single opening polygon for our boundary
|
||||
opening_polygon = shapely.ops.unary_union(opening_polygons)
|
||||
|
||||
# Only openings that are projected onto our exterior boundary are relevant.
|
||||
if opening_polygon.intersection(exterior_boundary_polygon).area == 0:
|
||||
continue
|
||||
|
||||
boundary = ifcopenshell.api.root.create_entity(ifc_file, ifc_class=props.boundary_class)
|
||||
boundary.RelatingSpace = space
|
||||
boundary.RelatedBuildingElement = filling or opening
|
||||
boundary.ConnectionGeometry = self.create_connection_geometry_from_polygon(
|
||||
opening_polygon, space_face_matrix
|
||||
)
|
||||
if filling:
|
||||
boundary.PhysicalOrVirtualBoundary = "PHYSICAL"
|
||||
else:
|
||||
boundary.PhysicalOrVirtualBoundary = "VIRTUAL"
|
||||
boundary.InternalOrExternalBoundary = parent_boundary.InternalOrExternalBoundary
|
||||
if boundary.is_a() != "IfcRelSpaceBoundary":
|
||||
boundary.ParentBoundary = parent_boundary
|
||||
self.set_boundary_name(boundary)
|
||||
boundaries.append(boundary)
|
||||
|
||||
return boundaries
|
||||
return ifcopenshell.util.boundary.auto_generate_boundaries(
|
||||
ifc_file, space, filtered_shapes, props.boundary_class
|
||||
)
|
||||
|
||||
def create_element_boundary(
|
||||
self,
|
||||
|
||||
@@ -77,10 +77,14 @@ class BIM_PT_Boundary(Panel):
|
||||
self.draw_relation_editor(boundary, "RelatedBuildingElement", "related_building_element")
|
||||
self.draw_relation_editor(boundary, "ParentBoundary", "parent_boundary")
|
||||
self.draw_relation_editor(boundary, "CorrespondingBoundary", "corresponding_boundary")
|
||||
row = self.layout.row()
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, "physical_or_virtual")
|
||||
row = self.layout.row()
|
||||
op = row.operator("bim.copy_boundary_attribute_to_selection", text="", icon="COPYDOWN")
|
||||
op.name = "PhysicalOrVirtualBoundary"
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, "internal_or_external")
|
||||
op = row.operator("bim.copy_boundary_attribute_to_selection", text="", icon="COPYDOWN")
|
||||
op.name = "InternalOrExternalBoundary"
|
||||
else:
|
||||
row = self.layout.row()
|
||||
row.operator("bim.enable_editing_boundary", icon="GREASEPENCIL", text="Edit")
|
||||
@@ -125,6 +129,8 @@ class BIM_PT_Boundary(Panel):
|
||||
if hasattr(boundary, ifc_attribute):
|
||||
row = self.layout.row(align=True)
|
||||
row.prop(self.bprops, blender_property)
|
||||
op = row.operator("bim.copy_boundary_attribute_to_selection", text="", icon="COPYDOWN")
|
||||
op.name = ifc_attribute
|
||||
|
||||
|
||||
class BIM_PT_SpaceBoundaries(Panel):
|
||||
|
||||
@@ -50,13 +50,11 @@ from bonsai.bim.module.drawing.data import refresh as refresh_drawing_data
|
||||
from bonsai.bim.prop import Attribute, BIMFilterGroup
|
||||
|
||||
diagram_scales_enum = []
|
||||
diagram_scales_enum_system = None
|
||||
|
||||
|
||||
def purge():
|
||||
global diagram_scales_enum, diagram_scales_enum_system
|
||||
global diagram_scales_enum
|
||||
diagram_scales_enum = []
|
||||
diagram_scales_enum_system = None
|
||||
|
||||
|
||||
def update_target_view_doc(self: "DocProperties", context: bpy.types.Context) -> None:
|
||||
@@ -125,12 +123,14 @@ def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> No
|
||||
|
||||
|
||||
def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
global diagram_scales_enum, diagram_scales_enum_system
|
||||
global diagram_scales_enum
|
||||
assert context.scene
|
||||
system = context.scene.unit_settings.system
|
||||
if len(diagram_scales_enum) < 1 or diagram_scales_enum_system != system:
|
||||
diagram_scales_enum_system = system
|
||||
if system == "IMPERIAL":
|
||||
if (
|
||||
len(diagram_scales_enum) < 1
|
||||
or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13)
|
||||
or (context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31)
|
||||
):
|
||||
if context.scene.unit_settings.system == "IMPERIAL":
|
||||
diagram_scales_enum = [
|
||||
("CUSTOM", "Custom", ""),
|
||||
("1'=1'-0\"|1/1", "1'=1'-0\"", ""),
|
||||
@@ -144,21 +144,21 @@ def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context)
|
||||
('1/4"=1\'-0"|1/48', '1/4"=1\'-0"', ""),
|
||||
('3/16"=1\'-0"|1/64', '3/16"=1\'-0"', ""),
|
||||
('1/8"=1\'-0"|1/96', '1/8"=1\'-0"', ""),
|
||||
("1\"=10'|1/120", "1\"=10'", ""),
|
||||
('3/32"=1\'-0"|1/128', '3/32"=1\'-0"', ""),
|
||||
('1/16"=1\'-0"|1/192', '1/16"=1\'-0"', ""),
|
||||
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
|
||||
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
|
||||
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
|
||||
("1\"=10'|1/120", "1\"=10'", ""),
|
||||
("1\"=20'|1/240", "1\"=20'", ""),
|
||||
("1\"=30'|1/360", "1\"=30'", ""),
|
||||
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
|
||||
("1\"=40'|1/480", "1\"=40'", ""),
|
||||
("1\"=50'|1/600", "1\"=50'", ""),
|
||||
("1\"=60'|1/720", "1\"=60'", ""),
|
||||
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
|
||||
("1\"=70'|1/840", "1\"=70'", ""),
|
||||
("1\"=80'|1/960", "1\"=80'", ""),
|
||||
("1\"=90'|1/1080", "1\"=90'", ""),
|
||||
("1\"=100'|1/1200", "1\"=100'", ""),
|
||||
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
|
||||
("1\"=150'|1/1800", "1\"=150'", ""),
|
||||
("1\"=200'|1/2400", "1\"=200'", ""),
|
||||
("1\"=300'|1/3600", "1\"=300'", ""),
|
||||
|
||||
@@ -178,6 +178,7 @@ classes = (
|
||||
covering.RegenSelectedCoveringObject,
|
||||
space.ToggleSpaceVisibility,
|
||||
space.ToggleHideSpaces,
|
||||
space.ApplySpaceHeightToSelection,
|
||||
mep.FitFlowSegments,
|
||||
mep.RegenerateDistributionElement,
|
||||
prop.SnapMousePoint,
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.unit
|
||||
|
||||
import bonsai.core.geometry as core_geometry
|
||||
import bonsai.core.spatial as core
|
||||
import bonsai.tool as tool
|
||||
|
||||
@@ -115,3 +117,47 @@ class ToggleHideSpaces(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
core.toggle_hide_spaces(tool.Ifc, tool.Spatial)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ApplySpaceHeightToSelection(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.apply_space_height_to_selection"
|
||||
bl_label = "Apply Space Height To Selection"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = "Apply the space height value to all selected spaces without regenerating their footprint"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
selected_spaces = [
|
||||
obj
|
||||
for obj in context.selected_objects
|
||||
if (element := tool.Ifc.get_entity(obj)) and element.is_a("IfcSpace")
|
||||
]
|
||||
if not selected_spaces:
|
||||
cls.poll_message_set("No spaces selected.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
ifc_file = tool.Ifc.get()
|
||||
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
depth_ifc = tool.Spatial.get_spatial_props().space_height / si_conversion
|
||||
total = 0
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcSpace"):
|
||||
continue
|
||||
body = tool.Geometry.get_body_representation(element)
|
||||
if not body:
|
||||
continue
|
||||
extrusion = tool.Model.get_extrusion(body)
|
||||
if not extrusion:
|
||||
continue
|
||||
extrusion.Depth = depth_ifc
|
||||
core_geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=body,
|
||||
)
|
||||
total += 1
|
||||
self.report({"INFO"}, f"Height applied to {total} spaces.")
|
||||
|
||||
@@ -24,6 +24,7 @@ from bpy.props import (
|
||||
BoolProperty,
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
FloatProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
@@ -277,6 +278,17 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
|
||||
should_include_children: BoolProperty(
|
||||
name="Should Include Children", default=True, update=update_should_include_children
|
||||
)
|
||||
space_height: FloatProperty(
|
||||
name="Space Height",
|
||||
default=3,
|
||||
subtype="DISTANCE",
|
||||
description="Space height in meters. Auto-detected on generation unless forced. Used as fallback.",
|
||||
)
|
||||
force_space_height: BoolProperty(
|
||||
name="Force Height",
|
||||
default=False,
|
||||
description="If enabled, uses the height value directly and skips auto-detection",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_locked: bool
|
||||
@@ -294,6 +306,8 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
|
||||
subelement_class: str
|
||||
default_container: int
|
||||
should_include_children: bool
|
||||
space_height: float
|
||||
force_space_height: bool
|
||||
|
||||
@property
|
||||
def active_container(self) -> Union[BIMContainer, None]:
|
||||
|
||||
@@ -83,9 +83,14 @@ class SpatialToolUI:
|
||||
|
||||
@classmethod
|
||||
def draw_default_interface(cls, context):
|
||||
spatial_props = tool.Spatial.get_spatial_props()
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=cls.model_props, property="rl3", text="RL")
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(data=spatial_props, property="space_height", text="Height")
|
||||
row.prop(data=spatial_props, property="force_space_height", text="", icon="PINNED")
|
||||
row.operator("bim.apply_space_height_to_selection", text="", icon="COPYDOWN")
|
||||
row = cls.layout.row(align=True)
|
||||
op_name = lambda op: op.get_rna_type().name
|
||||
if AuthoringData.data["active_class"] == "IfcWall" and context.selected_objects:
|
||||
add_layout_hotkey(
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -31,7 +31,7 @@ def copy_attribute_to_selection(
|
||||
root: type[tool.Root],
|
||||
spatial: type[tool.Spatial],
|
||||
name: str,
|
||||
value: Union[str, None],
|
||||
value: Any,
|
||||
) -> int:
|
||||
total_changed = 0
|
||||
has_edited_spatial_name = False
|
||||
|
||||
@@ -46,7 +46,7 @@ def add_instance_flooring_covering_from_cursor(
|
||||
else:
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
|
||||
|
||||
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
space_polygon, _ = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
@@ -81,7 +81,7 @@ def add_instance_ceiling_covering_from_cursor(
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
|
||||
ceiling_height = covering.get_z_from_ceiling_height()
|
||||
|
||||
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
space_polygon, _ = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
@@ -106,7 +106,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
|
||||
else:
|
||||
assert False, "Object has to be active and selected."
|
||||
|
||||
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
space_polygon, _ = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
|
||||
if isinstance(space_polygon, str):
|
||||
return
|
||||
|
||||
@@ -20,9 +20,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
import ifcopenshell
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
|
||||
import bonsai.tool as tool
|
||||
|
||||
@@ -186,9 +187,6 @@ def generate_space(
|
||||
"""
|
||||
:return: None if successful, error message string if not.
|
||||
"""
|
||||
if not root.get_default_container():
|
||||
raise SpaceGenerationError("Please set a default container to create the space in.")
|
||||
|
||||
active_obj = spatial.get_active_obj()
|
||||
selected_objects = spatial.get_selected_objects()
|
||||
element = None
|
||||
@@ -206,7 +204,15 @@ def generate_space(
|
||||
else:
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
|
||||
|
||||
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
if element and element.is_a("IfcSpace"):
|
||||
z = active_obj.location.z
|
||||
container = ifcopenshell.util.element.get_parent(element) or root.get_default_container()
|
||||
else:
|
||||
container = root.get_default_container()
|
||||
if not container:
|
||||
raise SpaceGenerationError("Please set a default container to create the space in.")
|
||||
|
||||
space_polygon, bounding_walls = spatial.get_space_polygon_from_context_visible_objects(x, y, container=container)
|
||||
|
||||
if isinstance(space_polygon, str):
|
||||
if space_polygon == "NO POLYGONS FOUND":
|
||||
@@ -220,8 +226,25 @@ def generate_space(
|
||||
else:
|
||||
assert space_polygon
|
||||
|
||||
props = spatial.get_spatial_props()
|
||||
if props.force_space_height:
|
||||
h = props.space_height
|
||||
else:
|
||||
auto_h = spatial.get_auto_space_height(space_polygon, z, bounding_walls)
|
||||
if auto_h is not None and auto_h > 0:
|
||||
h = auto_h
|
||||
|
||||
if element and element.is_a("IfcSpace"):
|
||||
spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True)
|
||||
assert active_obj
|
||||
spatial.set_space_representation_from_polygon(
|
||||
active_obj,
|
||||
element,
|
||||
space_polygon,
|
||||
h,
|
||||
polygon_is_si=True,
|
||||
bounding_walls=bounding_walls,
|
||||
container=container,
|
||||
)
|
||||
else:
|
||||
if relating_type:
|
||||
name = model.generate_occurrence_name(relating_type, "IfcSpace")
|
||||
@@ -234,7 +257,9 @@ def generate_space(
|
||||
spatial.assign_ifcspace_class_to_obj(obj)
|
||||
|
||||
element = ifc.get_entity(obj)
|
||||
spatial.set_space_representation_from_polygon(obj, element, space_polygon, h, polygon_is_si=True)
|
||||
spatial.set_space_representation_from_polygon(
|
||||
obj, element, space_polygon, h, polygon_is_si=True, bounding_walls=bounding_walls, container=container
|
||||
)
|
||||
|
||||
if relating_type:
|
||||
spatial.assign_relating_type_to_element(ifc, type, element, relating_type)
|
||||
@@ -248,11 +273,25 @@ def generate_spaces_from_walls(
|
||||
z = spatial.get_active_obj_z()
|
||||
h = spatial.get_active_obj_height()
|
||||
|
||||
bounding_walls = [
|
||||
element
|
||||
for obj in spatial.get_selected_objects()
|
||||
if (element := ifc.get_entity(obj)) and element.is_a("IfcWall")
|
||||
]
|
||||
|
||||
union = spatial.get_union_shape_from_selected_objects()
|
||||
|
||||
props = spatial.get_spatial_props()
|
||||
for i, linear_ring in enumerate(union.interiors):
|
||||
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
|
||||
|
||||
if props.force_space_height:
|
||||
h = props.space_height
|
||||
else:
|
||||
auto_h = spatial.get_auto_space_height(poly, z, bounding_walls)
|
||||
if auto_h is not None and auto_h > 0:
|
||||
h = auto_h
|
||||
|
||||
name = "Space" + str(i)
|
||||
|
||||
obj = spatial.create_object(name)
|
||||
|
||||
@@ -331,10 +331,6 @@ class Material(bonsai.core.tool.Material):
|
||||
|
||||
@classmethod
|
||||
def get_style(cls, material: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if not material.is_a("IfcMaterial"):
|
||||
# material may also be an IfcMaterialConstituentSet / IfcMaterialLayerSet /
|
||||
# IfcMaterialProfileSet / IfcMaterialList, none of which have HasRepresentation.
|
||||
return None
|
||||
for material_representation in material.HasRepresentation:
|
||||
for representation in material_representation.Representations:
|
||||
for item in representation.Items:
|
||||
|
||||
@@ -618,7 +618,7 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices) - 1)])
|
||||
if is_closed:
|
||||
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop
|
||||
cls.edges.append((len(cls.vertices) - 1, offset)) # Close the loop
|
||||
|
||||
elif curve.is_a("IfcCompositeCurve"):
|
||||
# This is a first pass incomplete implementation only for simple polylines, and misses many details.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import multiprocessing
|
||||
from collections import defaultdict
|
||||
from collections.abc import Generator, Iterable
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||
@@ -34,11 +35,14 @@ import ifcopenshell.util.classification
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.space
|
||||
import ifcopenshell.util.type
|
||||
import ifcopenshell.util.unit
|
||||
import numpy as np
|
||||
import shapely
|
||||
import shapely.affinity
|
||||
import shapely.ops
|
||||
from mathutils import Matrix, Vector
|
||||
from natsort import natsorted
|
||||
@@ -58,8 +62,52 @@ if TYPE_CHECKING:
|
||||
BIMSpatialDecompositionProperties,
|
||||
)
|
||||
|
||||
_GEOM_CACHE_TOKEN = 0
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def _bump_geom_cache_token(*args) -> None:
|
||||
global _GEOM_CACHE_TOKEN
|
||||
if len(args) >= 2:
|
||||
depsgraph = args[1]
|
||||
if depsgraph is not None and hasattr(depsgraph, "updates"):
|
||||
if not any(
|
||||
(getattr(u, "is_updated_geometry", False) or getattr(u, "is_updated_transform", False))
|
||||
and hasattr(u, "id")
|
||||
and isinstance(u.id, bpy.types.Object)
|
||||
for u in depsgraph.updates
|
||||
):
|
||||
return
|
||||
_GEOM_CACHE_TOKEN += 1
|
||||
|
||||
|
||||
def install_geom_cache_handlers() -> None:
|
||||
for hook in (
|
||||
bpy.app.handlers.depsgraph_update_post,
|
||||
bpy.app.handlers.undo_post,
|
||||
bpy.app.handlers.redo_post,
|
||||
bpy.app.handlers.load_post,
|
||||
):
|
||||
if _bump_geom_cache_token not in hook:
|
||||
hook.append(_bump_geom_cache_token)
|
||||
|
||||
|
||||
def uninstall_geom_cache_handlers() -> None:
|
||||
for hook in (
|
||||
bpy.app.handlers.depsgraph_update_post,
|
||||
bpy.app.handlers.undo_post,
|
||||
bpy.app.handlers.redo_post,
|
||||
bpy.app.handlers.load_post,
|
||||
):
|
||||
try:
|
||||
hook.remove(_bump_geom_cache_token)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class Spatial(bonsai.core.tool.Spatial):
|
||||
_geom_cache: dict = {}
|
||||
|
||||
@classmethod
|
||||
def get_spatial_props(cls) -> BIMSpatialDecompositionProperties:
|
||||
return bpy.context.scene.BIMSpatialDecompositionProperties
|
||||
@@ -755,29 +803,233 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
|
||||
# HERE STARTS SPATIAL TOOL
|
||||
|
||||
@classmethod
|
||||
def get_or_build_geom_cache(cls) -> dict:
|
||||
"""Build or return a cached dict of IFC element shapes for space generation.
|
||||
|
||||
The cache is keyed on ``_GEOM_CACHE_TOKEN`` which is bumped by a
|
||||
``depsgraph_update_post`` handler when any Object geometry or transform
|
||||
changes, and on undo/redo/load. This means the cache survives space
|
||||
generations (which don't change Object geometry) but is correctly
|
||||
invalidated when a user moves or edits a wall, slab, etc.
|
||||
|
||||
:return: ``{"shapes": {id: {"verts": ndarray, "faces": ndarray, "bottom_z": float, "top_z": float}}, "token": int}``
|
||||
"""
|
||||
global _GEOM_CACHE_TOKEN
|
||||
cached = cls._geom_cache.get("current")
|
||||
if cached and cached["token"] == _GEOM_CACHE_TOKEN:
|
||||
return cached
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
include = []
|
||||
for ifc_class in ifcopenshell.util.space.BOUNDING_CLASSES + ifcopenshell.util.space.HEIGHT_DETECTION_CLASSES:
|
||||
include.extend(ifc_file.by_type(ifc_class))
|
||||
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("disable-opening-subtractions", True)
|
||||
settings.set("use-world-coords", True)
|
||||
|
||||
shapes = {}
|
||||
iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count(), include=include)
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
verts = ifcopenshell.util.shape.get_shape_vertices(shape, shape.geometry)
|
||||
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
|
||||
zs = verts[:, 2]
|
||||
shapes[shape.id] = {
|
||||
"verts": verts,
|
||||
"faces": faces,
|
||||
"bottom_z": float(zs.min()),
|
||||
"top_z": float(zs.max()),
|
||||
}
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
cache = {"shapes": shapes, "token": _GEOM_CACHE_TOKEN}
|
||||
cls._geom_cache["current"] = cache
|
||||
return cache
|
||||
|
||||
@classmethod
|
||||
def is_bounding_class(cls, visible_element: ifcopenshell.entity_instance) -> bool:
|
||||
for ifc_class in ["IfcWall", "IfcColumn", "IfcMember", "IfcVirtualElement", "IfcPlate"]:
|
||||
for ifc_class in ifcopenshell.util.space.BOUNDING_CLASSES:
|
||||
if visible_element.is_a(ifc_class):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_boundary_lines_from_ifc_elements(
|
||||
cls,
|
||||
cut_z: float,
|
||||
) -> tuple[list[shapely.LineString], list[ifcopenshell.entity_instance]]:
|
||||
"""Generate boundary lines by bisecting IFC element geometry with a horizontal plane.
|
||||
|
||||
Uses the class-level geometry cache (parallel iterator) instead of
|
||||
iterating Blender visible objects. Works without any Blender objects
|
||||
being loaded.
|
||||
|
||||
:param cut_z: Z elevation of the cutting plane in world coordinates.
|
||||
:return: (boundary_lines, bounding_elements)
|
||||
"""
|
||||
cache = cls.get_or_build_geom_cache()
|
||||
return ifcopenshell.util.space.get_boundary_lines(tool.Ifc.get(), cache["shapes"], cut_z)
|
||||
|
||||
@classmethod
|
||||
def get_space_polygon_from_context_visible_objects(
|
||||
cls, x: float, y: float
|
||||
) -> Union[shapely.Polygon, Literal["NO POLYGONS FOUND", "NO POLYGON FOR POINT"]]:
|
||||
boundary_lines = cls.get_boundary_lines_from_context_visible_objects()
|
||||
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
|
||||
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
|
||||
if not closed_polygons:
|
||||
return "NO POLYGONS FOUND"
|
||||
space_polygon = None
|
||||
for polygon in closed_polygons.geoms:
|
||||
if shapely.contains_xy(polygon, x, y):
|
||||
space_polygon = shapely.force_3d(polygon)
|
||||
if space_polygon is None:
|
||||
return "NO POLYGON FOR POINT"
|
||||
return space_polygon
|
||||
cls, x: float, y: float, container: Optional[ifcopenshell.entity_instance] = None
|
||||
) -> tuple[
|
||||
Union[shapely.Polygon, Literal["NO POLYGONS FOUND", "NO POLYGON FOR POINT"]],
|
||||
list[ifcopenshell.entity_instance],
|
||||
]:
|
||||
props = tool.Model.get_model_props()
|
||||
calculation_rl = props.rl3
|
||||
if container is None:
|
||||
container = tool.Root.get_default_container()
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
cut_z = container_obj.matrix_world.translation.z + calculation_rl
|
||||
|
||||
# Commit any moved visible bounding objects before reading IFC geometry,
|
||||
# so the IFC-based cache uses the current Blender positions.
|
||||
# Walls/roofs/slabs that affect the space footprint or height must be
|
||||
# committed before the cache is rebuilt; otherwise the IFC geometry read by
|
||||
# the iterator will be stale and a moved roof/slab will not be picked up.
|
||||
affected_classes = ifcopenshell.util.space.BOUNDING_CLASSES + ifcopenshell.util.space.HEIGHT_DETECTION_CLASSES
|
||||
for obj in bpy.context.visible_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element is None or not any(element.is_a(c) for c in affected_classes):
|
||||
continue
|
||||
tool.Geometry.commit_placement_if_moved(obj)
|
||||
cls._geom_cache.clear()
|
||||
|
||||
boundary_lines, bounding_elements = cls.get_boundary_lines_from_ifc_elements(cut_z)
|
||||
polygon, _ = ifcopenshell.util.space.get_space_polygon(boundary_lines, x, y)
|
||||
if isinstance(polygon, str):
|
||||
return polygon, []
|
||||
return polygon, bounding_elements
|
||||
|
||||
@classmethod
|
||||
def get_auto_space_height(
|
||||
cls,
|
||||
space_polygon: shapely.Polygon,
|
||||
base_z: float,
|
||||
bounding_walls: list[ifcopenshell.entity_instance],
|
||||
) -> Optional[float]:
|
||||
"""Auto-detect space height from elements above using IFC geometry.
|
||||
|
||||
Delegates to :func:`ifcopenshell.util.space.get_auto_space_height`.
|
||||
|
||||
:param space_polygon: The space footprint polygon in world XY.
|
||||
:param base_z: The space's base Z in world coordinates.
|
||||
:param bounding_walls: List of IFC wall elements bounding the space.
|
||||
:return: Detected height in SI (meters), or None if nothing found.
|
||||
"""
|
||||
cache = cls.get_or_build_geom_cache()
|
||||
return ifcopenshell.util.space.get_auto_space_height(
|
||||
tool.Ifc.get(), cache["shapes"], space_polygon, base_z, bounding_walls
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_space_volume_strategy(
|
||||
cls,
|
||||
space_polygon: shapely.Polygon,
|
||||
base_z: float,
|
||||
bounding_walls: list[ifcopenshell.entity_instance],
|
||||
container: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> tuple[str, Optional[list], Optional[list]]:
|
||||
"""Decide how to build the space volume (clipped extrusion or B-rep).
|
||||
|
||||
Rays are cast from the RL cut elevation (``container_z + props.rl3``), the
|
||||
same level at which the space footprint polygon was found.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
cache = cls.get_or_build_geom_cache()
|
||||
start_z = None
|
||||
if container is None:
|
||||
container = tool.Root.get_default_container()
|
||||
if container is not None:
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
props = tool.Model.get_model_props()
|
||||
start_z = container_obj.matrix_world.translation.z + props.rl3
|
||||
tree = ifcopenshell.geom.tree(ifc_file)
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("disable-opening-subtractions", True)
|
||||
settings.set("use-world-coords", True)
|
||||
tree.add_file(ifc_file, settings)
|
||||
return ifcopenshell.util.space.detect_space_volume_strategy(
|
||||
ifc_file, cache["shapes"], tree, space_polygon, base_z, bounding_walls, start_z=start_z
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_or_create_body_context(cls, ifc_file: ifcopenshell.file) -> ifcopenshell.entity_instance:
|
||||
"""Return the Model/Body/MODEL_VIEW context, creating one if absent."""
|
||||
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
if context is not None:
|
||||
return context
|
||||
# Some subcontexts may not expose the inherited ContextType value, so also
|
||||
# search by ContextIdentifier/TargetView directly.
|
||||
for ctx in ifc_file.by_type("IfcGeometricRepresentationSubContext"):
|
||||
if ctx.ContextIdentifier == "Body" and getattr(ctx, "TargetView", None) == "MODEL_VIEW":
|
||||
return ctx
|
||||
# Create a minimal context if none exists.
|
||||
model_context = ifcopenshell.util.representation.get_context(ifc_file, "Model")
|
||||
if model_context is None:
|
||||
model_context = ifc_file.createIfcGeometricRepresentationContext(
|
||||
ContextType="Model",
|
||||
CoordinateSpaceDimension=3,
|
||||
Precision=1e-5,
|
||||
WorldCoordinateSystem=ifc_file.createIfcAxis2Placement3D(
|
||||
ifc_file.createIfcCartesianPoint([0.0, 0.0, 0.0])
|
||||
),
|
||||
TrueNorth=ifc_file.createIfcDirection([0.0, 1.0, 0.0]),
|
||||
)
|
||||
return ifc_file.createIfcGeometricRepresentationSubContext(
|
||||
ParentContext=model_context,
|
||||
ContextIdentifier="Body",
|
||||
TargetView="MODEL_VIEW",
|
||||
ContextType="Model",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _remove_existing_body_representations(
|
||||
cls, element: ifcopenshell.entity_instance
|
||||
) -> Optional[ifcopenshell.entity_instance]:
|
||||
"""Remove every existing Body representation from an element.
|
||||
|
||||
Returns the context of the first removed representation, or None.
|
||||
"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
if element.Representation is None:
|
||||
return None
|
||||
body_reps = [r for r in element.Representation.Representations if r.RepresentationIdentifier == "Body"]
|
||||
context = None
|
||||
for rep in body_reps:
|
||||
context = rep.ContextOfItems
|
||||
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=rep)
|
||||
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=rep)
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def set_brep_representation_from_mesh(
|
||||
cls,
|
||||
obj: bpy.types.Object,
|
||||
element: ifcopenshell.entity_instance,
|
||||
item: ifcopenshell.entity_instance,
|
||||
) -> None:
|
||||
"""Assign a representation item (clipped solid or B-rep) to the element."""
|
||||
ifc_file = tool.Ifc.get()
|
||||
context = cls._remove_existing_body_representations(element)
|
||||
if context is None:
|
||||
context = cls._get_or_create_body_context(ifc_file)
|
||||
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
|
||||
new_body = builder.get_representation(context, item)
|
||||
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_body,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def debug_shape(cls, foo: shapely.Polygon) -> None:
|
||||
@@ -810,7 +1062,9 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
@classmethod
|
||||
def get_boundary_lines_from_context_visible_objects(cls) -> list[shapely.LineString]:
|
||||
def get_boundary_lines_from_context_visible_objects(
|
||||
cls,
|
||||
) -> tuple[list[shapely.LineString], list[ifcopenshell.entity_instance]]:
|
||||
props = tool.Model.get_model_props()
|
||||
calculation_rl = props.rl3
|
||||
container = tool.Root.get_default_container()
|
||||
@@ -818,6 +1072,7 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
cut_point = container_obj.matrix_world.translation.copy() + Vector((0, 0, calculation_rl))
|
||||
cut_normal = Vector((0, 0, 1))
|
||||
boundary_lines = []
|
||||
bounding_elements = []
|
||||
|
||||
for obj in bpy.context.visible_objects:
|
||||
visible_element = tool.Ifc.get_entity(obj)
|
||||
@@ -831,6 +1086,7 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
):
|
||||
continue
|
||||
|
||||
bounding_elements.append(visible_element)
|
||||
old_mesh = obj.data
|
||||
assert isinstance(old_mesh, bpy.types.Mesh)
|
||||
if visible_element.HasOpenings:
|
||||
@@ -870,7 +1126,7 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
start, end = tool.Drawing.extend_line(start, end, 0.05)
|
||||
boundary_lines.append(shapely.LineString([start, end]))
|
||||
|
||||
return boundary_lines
|
||||
return boundary_lines, bounding_elements
|
||||
|
||||
@classmethod
|
||||
def get_gross_mesh_from_element(cls, visible_element: ifcopenshell.entity_instance) -> bpy.types.Mesh:
|
||||
@@ -1086,13 +1342,9 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
curve = builder.polyline(coords_2d, closed=True)
|
||||
item = builder.extrude(curve, magnitude=depth_ifc)
|
||||
|
||||
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
if old_body:
|
||||
context = old_body.ContextOfItems
|
||||
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=old_body)
|
||||
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body)
|
||||
else:
|
||||
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
|
||||
context = cls._remove_existing_body_representations(element)
|
||||
if context is None:
|
||||
context = cls._get_or_create_body_context(ifc_file)
|
||||
|
||||
new_body = builder.get_representation(context, item)
|
||||
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body)
|
||||
@@ -1111,13 +1363,104 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
poly: Polygon,
|
||||
h: float,
|
||||
polygon_is_si: bool = True,
|
||||
bounding_walls: Optional[list[ifcopenshell.entity_instance]] = None,
|
||||
container: Optional[ifcopenshell.entity_instance] = None,
|
||||
) -> None:
|
||||
"""Create or replace the IFC body representation of a space from a polygon.
|
||||
|
||||
:param h: The height in SI (meters).
|
||||
"""
|
||||
# Remove collinear points introduced by the mesh bisection so the
|
||||
# footprint polygon has a minimal vertex count.
|
||||
poly = poly.simplify(0, preserve_topology=True)
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
ifc_file = tool.Ifc.get()
|
||||
x, y, z = obj.matrix_world.translation
|
||||
origin = obj.matrix_world.translation # Blender SI
|
||||
# The space builders expect base_z and polygon in SI (world) units.
|
||||
base_z = z
|
||||
poly_si = poly if polygon_is_si else shapely.affinity.scale(poly, unit_scale, unit_scale, origin=(0, 0))
|
||||
|
||||
# Ensure the IFC entity has an ObjectPlacement matching the Blender object,
|
||||
# so the generated representation is in the correct local coordinate system.
|
||||
bpy.context.view_layer.update()
|
||||
matrix = np.array(obj.matrix_world)
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
ifc_file,
|
||||
product=element,
|
||||
matrix=matrix,
|
||||
is_si=True,
|
||||
)
|
||||
|
||||
for b in list(element.BoundedBy or []):
|
||||
ifcopenshell.api.boundary.remove_boundary(ifc_file, b)
|
||||
|
||||
cls._remove_existing_body_representations(element)
|
||||
|
||||
if cls.get_spatial_props().force_space_height:
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
return
|
||||
if bounding_walls is None:
|
||||
bounding_walls = []
|
||||
if container is None:
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container is not None:
|
||||
for wall in ifc_file.by_type("IfcWall"):
|
||||
if wall in ifcopenshell.util.element.get_decomposition(container):
|
||||
bounding_walls.append(wall)
|
||||
|
||||
# Detect planes in world SI (same coordinate system as the geom cache).
|
||||
strategy, top_planes, bottom_planes = cls.get_space_volume_strategy(poly_si, base_z, bounding_walls, container)
|
||||
|
||||
# Build the geometry in the space's local coordinate system so the IFC
|
||||
# representation is relative to the object's ObjectPlacement.
|
||||
# Use the full inverse of the object's placement matrix so rotated spaces
|
||||
# keep the correct footprint orientation.
|
||||
matrix_inv = np.array(obj.matrix_world.inverted())
|
||||
# shapely.affine_transform expects [a, b, d, e, xoff, yoff]
|
||||
# where x' = a*x + b*y + xoff, y' = d*x + e*y + yoff.
|
||||
affine_params = [
|
||||
matrix_inv[0, 0],
|
||||
matrix_inv[0, 1],
|
||||
matrix_inv[1, 0],
|
||||
matrix_inv[1, 1],
|
||||
matrix_inv[0, 3],
|
||||
matrix_inv[1, 3],
|
||||
]
|
||||
local_poly_si = shapely.affinity.affine_transform(poly_si, affine_params)
|
||||
local_base_z = base_z - origin.z
|
||||
|
||||
def localize_plane(plane):
|
||||
point, normal = plane
|
||||
local_point = matrix_inv @ np.array([*point, 1.0])
|
||||
rotation_inv = matrix_inv[:3, :3]
|
||||
local_normal = rotation_inv @ np.array(normal)
|
||||
local_normal = local_normal / np.linalg.norm(local_normal)
|
||||
return (local_point[:3], local_normal)
|
||||
|
||||
local_top_planes = [localize_plane(p) for p in (top_planes or [])]
|
||||
local_bottom_planes = [localize_plane(p) for p in (bottom_planes or [])]
|
||||
|
||||
if strategy == "EXTRUDE_CLIP" and top_planes:
|
||||
item = ifcopenshell.util.space.build_extruded_clipped_space(
|
||||
ifc_file, local_poly_si, local_base_z, local_top_planes, local_bottom_planes
|
||||
)
|
||||
cls.set_brep_representation_from_mesh(obj, element, item)
|
||||
else:
|
||||
shapes = cls.get_or_build_geom_cache()["shapes"]
|
||||
local_shapes = {}
|
||||
for shape_id, shape_data in shapes.items():
|
||||
local_shape_data = dict(shape_data)
|
||||
local_shape_data["top_z"] = shape_data["top_z"] - origin.z
|
||||
local_shape_data["bottom_z"] = shape_data["bottom_z"] - origin.z
|
||||
local_shapes[shape_id] = local_shape_data
|
||||
item = ifcopenshell.util.space.build_brep_space(
|
||||
ifc_file, element, local_shapes, local_poly_si, local_base_z
|
||||
)
|
||||
if item is None:
|
||||
cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
|
||||
else:
|
||||
cls.set_brep_representation_from_mesh(obj, element, item)
|
||||
|
||||
@classmethod
|
||||
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
|
||||
|
||||
@@ -245,16 +245,9 @@ class Wall(bonsai.core.tool.Wall):
|
||||
@classmethod
|
||||
def iter_wall_slab_connections(cls, wall: ifcopenshell.entity_instance):
|
||||
"""Yield ``(slab, rel)`` tuples for every ``IfcRelConnectsElements(TOP)``
|
||||
connecting a slab to this wall — the rel kind ``extend_walls_to_underside``
|
||||
creates. Walks ``wall.ConnectedFrom`` because the slab is the relating
|
||||
side of the TOP rel."""
|
||||
for rel in getattr(wall, "ConnectedFrom", []) or ():
|
||||
if not rel.is_a("IfcRelConnectsElements") or rel.Description != "TOP":
|
||||
continue
|
||||
slab = rel.RelatingElement
|
||||
if slab is None:
|
||||
continue
|
||||
yield slab, rel
|
||||
connecting a slab to this wall. Delegates to
|
||||
:func:`ifcopenshell.util.element.iter_top_connections`."""
|
||||
yield from ifcopenshell.util.element.iter_top_connections(wall)
|
||||
|
||||
@classmethod
|
||||
def iter_slab_wall_connections(cls, slab: ifcopenshell.entity_instance):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pytest
|
||||
pytest-blender
|
||||
pytest-bdd
|
||||
fake-bpy-module-latest
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of Bonsai.
|
||||
#
|
||||
# Bonsai is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Bonsai is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
import pytest
|
||||
|
||||
import bonsai
|
||||
import bonsai.core.covering as subject
|
||||
import bonsai.core.tool
|
||||
from test.core.bootstrap import Prophecy, ifc, root, spatial
|
||||
|
||||
# NOTE: The Prophecy mocking framework serialises call arguments as JSON,
|
||||
# which means shapely geometry objects cannot be passed through mocked
|
||||
# calls. We use the plain integer 42 as a serialisable stand-in for the
|
||||
# polygon return value; the test verifies the unpack behaviour (that the
|
||||
# polygon-like scalar 42 reaches set_covering_representation_from_polygon
|
||||
# instead of the tuple (42, []) which old code would have passed).
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def covering():
|
||||
prophet = Prophecy(bonsai.core.tool.Covering)
|
||||
yield prophet
|
||||
prophet.verify()
|
||||
|
||||
|
||||
class TestAddInstanceFlooringCoveringFromCursor:
|
||||
def test_run(self, ifc, root, spatial):
|
||||
root.get_default_container().should_be_called().will_return("container")
|
||||
spatial.get_active_obj().should_be_called().will_return(None)
|
||||
spatial.get_selected_objects().should_be_called().will_return([])
|
||||
spatial.get_relating_type_id().should_be_called().will_return(0)
|
||||
spatial.get_x_y_z_h_mat_from_cursor().should_be_called().will_return((0, 0, 0, 3, None))
|
||||
|
||||
spatial.get_space_polygon_from_context_visible_objects(0, 0).should_be_called().will_return((42, []))
|
||||
spatial.create_object("Covering").should_be_called().will_return("mock_obj")
|
||||
spatial.set_obj_origin_to_cursor_position_and_zero_elevation("mock_obj").should_be_called()
|
||||
spatial.translate_obj_to_z_location("mock_obj", 0).should_be_called()
|
||||
spatial.assign_type_to_obj("mock_obj").should_be_called()
|
||||
spatial.set_covering_representation_from_polygon("mock_obj", 42, polygon_is_si=True).should_be_called()
|
||||
|
||||
subject.add_instance_flooring_covering_from_cursor(ifc, root, spatial)
|
||||
|
||||
def test_raises_when_no_default_container(self, ifc, root, spatial):
|
||||
root.get_default_container().should_be_called().will_return(None)
|
||||
with pytest.raises(subject.NoDefaultContainer):
|
||||
subject.add_instance_flooring_covering_from_cursor(ifc, root, spatial)
|
||||
|
||||
|
||||
class TestAddInstanceCeilingCoveringFromCursor:
|
||||
def test_run(self, ifc, root, covering, spatial):
|
||||
root.get_default_container().should_be_called().will_return("container")
|
||||
spatial.get_active_obj().should_be_called().will_return(None)
|
||||
spatial.get_selected_objects().should_be_called().will_return([])
|
||||
spatial.get_relating_type_id().should_be_called().will_return(0)
|
||||
covering.get_z_from_ceiling_height().should_be_called().will_return(3.0)
|
||||
spatial.get_x_y_z_h_mat_from_cursor().should_be_called().will_return((0, 0, 0, 3, None))
|
||||
|
||||
spatial.get_space_polygon_from_context_visible_objects(0, 0).should_be_called().will_return((42, []))
|
||||
spatial.create_object("Covering").should_be_called().will_return("mock_obj")
|
||||
spatial.set_obj_origin_to_cursor_position_and_zero_elevation("mock_obj").should_be_called()
|
||||
spatial.translate_obj_to_z_location("mock_obj", 3.0).should_be_called()
|
||||
spatial.assign_type_to_obj("mock_obj").should_be_called()
|
||||
spatial.set_covering_representation_from_polygon("mock_obj", 42, polygon_is_si=True).should_be_called()
|
||||
|
||||
subject.add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial)
|
||||
|
||||
def test_raises_when_no_default_container(self, ifc, root, covering, spatial):
|
||||
root.get_default_container().should_be_called().will_return(None)
|
||||
with pytest.raises(subject.NoDefaultContainer):
|
||||
subject.add_instance_ceiling_covering_from_cursor(ifc, root, covering, spatial)
|
||||
|
||||
|
||||
class TestRegenSelectedCoveringObject:
|
||||
def test_run(self, root, spatial):
|
||||
root.get_default_container().should_be_called().will_return("container")
|
||||
spatial.get_active_obj().should_be_called().will_return("active")
|
||||
spatial.get_selected_objects().should_be_called().will_return(["active"])
|
||||
spatial.get_x_y_z_h_mat_from_obj("active").should_be_called().will_return((2, 3, 1, 3, None))
|
||||
|
||||
spatial.get_space_polygon_from_context_visible_objects(2, 3).should_be_called().will_return((42, []))
|
||||
spatial.set_covering_representation_from_polygon("active", 42, polygon_is_si=True).should_be_called()
|
||||
|
||||
subject.regen_selected_covering_object(root, spatial)
|
||||
|
||||
def test_raises_when_no_default_container(self, root, spatial):
|
||||
root.get_default_container().should_be_called().will_return(None)
|
||||
with pytest.raises(subject.NoDefaultContainer):
|
||||
subject.regen_selected_covering_object(root, spatial)
|
||||
|
||||
def test_raises_when_no_active_selected(self, root, spatial):
|
||||
root.get_default_container().should_be_called().will_return("container")
|
||||
spatial.get_active_obj().should_be_called().will_return(None)
|
||||
spatial.get_selected_objects().should_be_called().will_return([])
|
||||
with pytest.raises(AssertionError):
|
||||
subject.regen_selected_covering_object(root, spatial)
|
||||
@@ -32,6 +32,7 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.shape_builder
|
||||
import numpy as np
|
||||
from ifcopenshell.util.shape_builder import ShapeBuilder, V
|
||||
from mathutils import Matrix
|
||||
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
@@ -1044,3 +1045,41 @@ class TestGetSiblingOccurrenceCount(NewFile):
|
||||
ifcopenshell.api.type.assign_type(ifc, related_objects=occurrences, relating_type=wall_type)
|
||||
|
||||
assert subject.get_sibling_occurrence_count(wall_type) == 2
|
||||
|
||||
|
||||
class TestConvertCurveToMesh(NewFile):
|
||||
def test_closed_polyline_converts_to_closed_loop(self):
|
||||
"""A closed IfcPolyline must produce the full edge loop.
|
||||
|
||||
Before the fix (cls.edges[-1] = … overwrite) the closing edge
|
||||
replaced the real last segment, leaving every loop open by one
|
||||
edge — e.g. a quad got only 3 edges.
|
||||
"""
|
||||
ifc = ifcopenshell.file()
|
||||
|
||||
# Closed quad: 4 unique points + closing repeat = 5 points
|
||||
p0 = ifc.createIfcCartesianPoint((0.0, 0.0))
|
||||
p1 = ifc.createIfcCartesianPoint((1.0, 0.0))
|
||||
p2 = ifc.createIfcCartesianPoint((1.0, 1.0))
|
||||
p3 = ifc.createIfcCartesianPoint((0.0, 1.0))
|
||||
polyline = ifc.createIfcPolyline((p0, p1, p2, p3, p0))
|
||||
|
||||
subject.vertices = []
|
||||
subject.edges = []
|
||||
subject.arcs = []
|
||||
subject.circles = []
|
||||
subject.unit_scale = 1.0
|
||||
|
||||
subject.convert_curve_to_mesh(None, Matrix(), polyline)
|
||||
|
||||
assert len(subject.vertices) == 4, f"Expected 4 vertices, got {len(subject.vertices)}"
|
||||
assert len(subject.edges) == 4, f"Expected 4 edges, got {len(subject.edges)}"
|
||||
|
||||
# Every vertex must appear in exactly 2 edges (closed loop)
|
||||
from collections import defaultdict
|
||||
counts = defaultdict(int)
|
||||
for e in subject.edges:
|
||||
counts[e[0]] += 1
|
||||
counts[e[1]] += 1
|
||||
for v_idx, cnt in counts.items():
|
||||
assert cnt == 2, f"Vertex {v_idx} has {cnt} incident edges (expected 2)"
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
@@ -24,12 +26,16 @@ import ifcopenshell.api.feature
|
||||
import ifcopenshell.api.nest
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.util.representation
|
||||
import numpy as np
|
||||
from mathutils import Matrix
|
||||
import pytest
|
||||
import shapely
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.core.tool
|
||||
import bonsai.tool as tool
|
||||
from bonsai.tool.spatial import Spatial as subject
|
||||
from bonsai.tool.spatial import _bump_geom_cache_token
|
||||
from test.bim.bootstrap import NewFile
|
||||
|
||||
|
||||
@@ -258,17 +264,59 @@ class TestSelectProducts(NewFile):
|
||||
assert obj in bpy.context.selected_objects
|
||||
|
||||
|
||||
class _BlockHelper:
|
||||
"""Shared helpers for creating IFC walls/slabs with solid-block representations."""
|
||||
|
||||
@staticmethod
|
||||
def create_wall(ifc, height=10.0):
|
||||
"""Create an IFC wall with a 10x10x{height} block representation from z=0."""
|
||||
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
|
||||
wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
|
||||
placement_2d = ifc.createIfcAxis2Placement2D(ifc.createIfcCartesianPoint([0.0, 0.0]))
|
||||
profile = ifc.createIfcRectangleProfileDef("AREA", None, placement_2d, 10.0, 10.0)
|
||||
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([0.0, 0.0, 0.0]))
|
||||
extrusion = ifc.createIfcExtrudedAreaSolid(
|
||||
profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), height
|
||||
)
|
||||
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [extrusion])
|
||||
wall.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
|
||||
return wall, extrusion
|
||||
|
||||
@staticmethod
|
||||
def create_thin_wall(ifc, cx, cy, width, depth, height=10.0):
|
||||
"""Create an IFC wall with a thin block representation centered at (cx, cy)."""
|
||||
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
|
||||
wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
|
||||
placement_2d = ifc.createIfcAxis2Placement2D(ifc.createIfcCartesianPoint([0.0, 0.0]))
|
||||
profile = ifc.createIfcRectangleProfileDef("AREA", None, placement_2d, width, depth)
|
||||
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([cx, cy, 0.0]))
|
||||
extrusion = ifc.createIfcExtrudedAreaSolid(
|
||||
profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), height
|
||||
)
|
||||
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [extrusion])
|
||||
wall.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
|
||||
return wall
|
||||
|
||||
@staticmethod
|
||||
def create_slab(ifc, z=4.0):
|
||||
"""Create an IfcSlab with a 12x12x1.0 block representation at bottom_z={z}."""
|
||||
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
|
||||
slab = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSlab")
|
||||
placement_2d = ifc.createIfcAxis2Placement2D(ifc.createIfcCartesianPoint([0.0, 0.0]))
|
||||
profile = ifc.createIfcRectangleProfileDef("AREA", None, placement_2d, 12.0, 12.0)
|
||||
placement_3d = ifc.createIfcAxis2Placement3D(ifc.createIfcCartesianPoint([0.0, 0.0, z]))
|
||||
extrusion = ifc.createIfcExtrudedAreaSolid(profile, placement_3d, ifc.createIfcDirection([0.0, 0.0, 1.0]), 1.0)
|
||||
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [extrusion])
|
||||
slab.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
|
||||
|
||||
|
||||
class TestGenerateSpace(NewFile):
|
||||
def test_generate_space_at_cursor(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
scene = bpy.context.scene
|
||||
product = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
|
||||
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
||||
obj = bpy.data.objects["Cube"]
|
||||
scene.collection.objects.link(obj)
|
||||
tool.Ifc.link(product, obj)
|
||||
scene.cursor.location = (0, 0, 0)
|
||||
# The wall block spans z=0..10, bisects to a 10x10 polygon at cut_z.
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
@@ -292,13 +340,8 @@ class TestGenerateSpace(NewFile):
|
||||
def test_regenerate_space_preserves_z_location(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
scene = bpy.context.scene
|
||||
product = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall")
|
||||
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
|
||||
obj = bpy.data.objects["Cube"]
|
||||
scene.collection.objects.link(obj)
|
||||
tool.Ifc.link(product, obj)
|
||||
scene.cursor.location = (0, 0, 0)
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
@@ -307,8 +350,495 @@ class TestGenerateSpace(NewFile):
|
||||
|
||||
bpy.context.view_layer.objects.active = space
|
||||
space.select_set(True)
|
||||
obj.select_set(False)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
|
||||
assert np.isclose(space.location.z, 5), f"Expected z=5, got {space.location.z}"
|
||||
|
||||
def test_auto_space_height_from_slab_above(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
_BlockHelper.create_slab(ifc, z=4.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
assert np.isclose(space.dimensions.z, 4, atol=0.1), f"Expected height ~4, got {space.dimensions.z}"
|
||||
|
||||
def test_forced_space_height(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
spatial_props = tool.Spatial.get_spatial_props()
|
||||
spatial_props.force_space_height = True
|
||||
spatial_props.space_height = 5
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
assert np.isclose(space.dimensions.z, 5, atol=0.1), f"Expected height 5, got {space.dimensions.z}"
|
||||
|
||||
def test_auto_space_height_fallback_no_slab(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
spatial_props = tool.Spatial.get_spatial_props()
|
||||
spatial_props.force_space_height = False
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
assert space.dimensions.z > 0, f"Expected positive height, got {space.dimensions.z}"
|
||||
|
||||
def test_apply_space_height_to_selection(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
|
||||
spatial_props = tool.Spatial.get_spatial_props()
|
||||
spatial_props.space_height = 6
|
||||
bpy.context.view_layer.objects.active = space
|
||||
space.hide_viewport = False
|
||||
space.select_set(True)
|
||||
|
||||
bpy.ops.bim.apply_space_height_to_selection()
|
||||
bpy.context.view_layer.update()
|
||||
assert np.isclose(space.dimensions.z, 6, atol=0.1), f"Expected height 6, got {space.dimensions.z}"
|
||||
|
||||
def test_cache_survives_second_generation(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space1 = bpy.data.objects["IfcSpace/Space"]
|
||||
height1 = space1.dimensions.z
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space2 = bpy.data.objects["IfcSpace/Space"]
|
||||
height2 = space2.dimensions.z
|
||||
|
||||
assert np.isclose(height1, height2, atol=0.1), f"Cache changed height: {height1} vs {height2}"
|
||||
|
||||
def test_regenerate_after_wall_height_change(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
wall, extrusion = _BlockHelper.create_wall(ifc, height=10.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
original_height = space.dimensions.z
|
||||
|
||||
# Modify the IFC representation to change the wall height.
|
||||
extrusion.Depth = 15.0
|
||||
_bump_geom_cache_token()
|
||||
|
||||
bpy.context.view_layer.objects.active = space
|
||||
space.select_set(True)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
new_height = space.dimensions.z
|
||||
assert new_height != original_height or new_height > 0
|
||||
|
||||
def test_regenerate_space_from_centered_cube_representation(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
scene = bpy.context.scene
|
||||
scene.cursor.location = (0, 0, 0)
|
||||
|
||||
# Create a space with a unit cube PolygonalFaceSet centered at local origin.
|
||||
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
|
||||
points = ifc.createIfcCartesianPointList3D(
|
||||
[
|
||||
(-0.5, -0.5, -0.5),
|
||||
(-0.5, -0.5, 0.5),
|
||||
(-0.5, 0.5, -0.5),
|
||||
(-0.5, 0.5, 0.5),
|
||||
(0.5, -0.5, -0.5),
|
||||
(0.5, -0.5, 0.5),
|
||||
(0.5, 0.5, -0.5),
|
||||
(0.5, 0.5, 0.5),
|
||||
]
|
||||
)
|
||||
faces = [
|
||||
ifc.createIfcIndexedPolygonalFace([1, 2, 4, 3]),
|
||||
ifc.createIfcIndexedPolygonalFace([3, 4, 8, 7]),
|
||||
ifc.createIfcIndexedPolygonalFace([7, 8, 6, 5]),
|
||||
ifc.createIfcIndexedPolygonalFace([5, 6, 2, 1]),
|
||||
ifc.createIfcIndexedPolygonalFace([3, 7, 5, 1]),
|
||||
ifc.createIfcIndexedPolygonalFace([8, 4, 2, 6]),
|
||||
]
|
||||
face_set = ifc.createIfcPolygonalFaceSet(points, True, faces)
|
||||
shape_rep = ifc.createIfcShapeRepresentation(ctx, "Body", "Tessellation", [face_set])
|
||||
|
||||
space_element = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSpace")
|
||||
space_element.Representation = ifc.createIfcProductDefinitionShape(None, None, [shape_rep])
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 5))
|
||||
obj = bpy.data.objects["Cube"]
|
||||
scene.collection.objects.link(obj)
|
||||
tool.Ifc.link(space_element, obj)
|
||||
bpy.context.view_layer.update()
|
||||
obj.name = "MySpace"
|
||||
|
||||
# Check the cube's world bottom Z before regeneration.
|
||||
bottom_z = (obj.matrix_world @ Vector(obj.bound_box[0])).z
|
||||
assert np.isclose(bottom_z, 4.5), f"Expected bottom_z=4.5, got {bottom_z}"
|
||||
|
||||
# Regenerate the space.
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
bpy.ops.bim.generate_space()
|
||||
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
world_verts = [obj.matrix_world @ v.co for v in mesh.vertices]
|
||||
world_zs = [v.z for v in world_verts]
|
||||
assert min(world_zs) >= -0.1, f"Expected space world bottom near z>=0, got {min(world_zs)}"
|
||||
assert max(world_zs) > 0, f"Expected space to have positive height, got {max(world_zs)}"
|
||||
assert np.isclose(obj.location.z, 5.0, atol=0.01), f"Expected location.z=5.0, got {obj.location.z}"
|
||||
|
||||
|
||||
class TestGenerateSpaceSlopedRoof(NewFile):
|
||||
def _create_shed_roof(self, ifc, z=4.0, rise=3.0):
|
||||
"""Create an IfcRoof whose underside is a sloped plane across the footprint.
|
||||
|
||||
Triangular prism: vertical profile (in the y-z plane) extruded along +x.
|
||||
Profile points (u, v) with placement loc=(-5, 0, z), axis=(1,0,0),
|
||||
ref=(0,0,1). The local frame maps u to world +z (u=0 -> z, u=rise ->
|
||||
z+rise) and v to world -y (v=-5 -> y=+5, v=+5 -> y=-5):
|
||||
(0,-5) -> world (-5, +5, z) eave (low) at north
|
||||
(rise,-5) -> world (-5, +5, z+rise) vertical edge
|
||||
(rise,5) -> world (-5, -5, z+rise) ridge at south
|
||||
The underside is the sloped face from (y=+5, z) to (y=-5, z+rise).
|
||||
ExtrudedDirection (0,0,1) is local, mapping to world +x; depth 10 spans
|
||||
x in [-5, 5].
|
||||
"""
|
||||
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
|
||||
roof = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcRoof")
|
||||
pts = [
|
||||
ifc.createIfcCartesianPoint((0.0, -5.0)),
|
||||
ifc.createIfcCartesianPoint((float(rise), -5.0)),
|
||||
ifc.createIfcCartesianPoint((float(rise), 5.0)),
|
||||
]
|
||||
polyline = ifc.createIfcPolyline(pts)
|
||||
profile = ifc.createIfcArbitraryClosedProfileDef(ProfileType="CURVE", OuterCurve=polyline)
|
||||
placement = ifc.createIfcAxis2Placement3D(
|
||||
ifc.createIfcCartesianPoint((-5.0, 0.0, z)),
|
||||
ifc.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
ifc.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
)
|
||||
extrude_dir = ifc.createIfcDirection((0.0, 0.0, 1.0))
|
||||
solid = ifc.createIfcExtrudedAreaSolid(profile, placement, extrude_dir, 10.0)
|
||||
rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [solid])
|
||||
ifcopenshell.api.geometry.assign_representation(ifc, product=roof, representation=rep)
|
||||
return roof
|
||||
|
||||
def test_generate_space_under_shed_roof(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_thin_wall(ifc, 0.0, 4.8, 10.0, 0.4)
|
||||
_BlockHelper.create_thin_wall(ifc, 0.0, -4.8, 10.0, 0.4)
|
||||
_BlockHelper.create_thin_wall(ifc, 4.8, 0.0, 0.4, 10.0)
|
||||
_BlockHelper.create_thin_wall(ifc, -4.8, 0.0, 0.4, 10.0)
|
||||
self._create_shed_roof(ifc, z=4.0, rise=3.0)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
mesh = space.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
verts = np.array([v.co for v in mesh.vertices])
|
||||
min_z = verts[:, 2].min()
|
||||
max_z = verts[:, 2].max()
|
||||
assert min_z >= -0.1
|
||||
assert max_z > 0
|
||||
top_z_north = max([v[2] for v in verts if v[1] > 1])
|
||||
top_z_south = max([v[2] for v in verts if v[1] < -1])
|
||||
assert abs(top_z_north - top_z_south) > 0.05, f"Top should slope along y: {top_z_north} vs {top_z_south}"
|
||||
|
||||
|
||||
class TestSpaceVolumeStrategy(NewFile):
|
||||
def test_vertical_box_returns_extrude_clip(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
_BlockHelper.create_wall(ifc, height=10.0)
|
||||
_BlockHelper.create_slab(ifc, z=4.0)
|
||||
space_polygon = shapely.box(-5, -5, 5, 5)
|
||||
strategy, top, bottom = subject.get_space_volume_strategy(space_polygon, 0.0, [ifc.by_type("IfcWall")[0]])
|
||||
assert strategy == "EXTRUDE_CLIP"
|
||||
assert len(top) == 1
|
||||
assert len(bottom) == 0
|
||||
|
||||
@staticmethod
|
||||
def _create_sloped_slab(ifc, z=4.0, rise=3.0):
|
||||
"""Create an IfcSlab whose underside is a sloped plane across the footprint."""
|
||||
ctx = ifcopenshell.util.representation.get_context(ifc, "Model", "Body", "MODEL_VIEW")
|
||||
slab = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcSlab")
|
||||
pts = [
|
||||
ifc.createIfcCartesianPoint((0.0, -5.0)),
|
||||
ifc.createIfcCartesianPoint((float(rise), -5.0)),
|
||||
ifc.createIfcCartesianPoint((float(rise), 5.0)),
|
||||
]
|
||||
polyline = ifc.createIfcPolyline(pts)
|
||||
profile = ifc.createIfcArbitraryClosedProfileDef(ProfileType="CURVE", OuterCurve=polyline)
|
||||
placement = ifc.createIfcAxis2Placement3D(
|
||||
ifc.createIfcCartesianPoint((-5.0, 0.0, z)),
|
||||
ifc.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
ifc.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
)
|
||||
extrude_dir = ifc.createIfcDirection((0.0, 0.0, 1.0))
|
||||
solid = ifc.createIfcExtrudedAreaSolid(profile, placement, extrude_dir, 10.0)
|
||||
rep = ifc.createIfcShapeRepresentation(ctx, "Body", "SweptSolid", [solid])
|
||||
ifcopenshell.api.geometry.assign_representation(ifc, product=slab, representation=rep)
|
||||
return slab
|
||||
|
||||
def test_sloped_slab_returns_extrude_clip(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
self._create_sloped_slab(ifc, z=4.0, rise=3.0)
|
||||
space_polygon = shapely.box(-5, -5, 5, 5)
|
||||
strategy, top, bottom = subject.get_space_volume_strategy(space_polygon, 0.0, [])
|
||||
assert strategy == "EXTRUDE_CLIP"
|
||||
assert len(top) == 1
|
||||
assert len(bottom) == 0
|
||||
|
||||
|
||||
class TestRegenerateSpaceFromRealIfc2x3(NewFile):
|
||||
def load_house_with_garage(self):
|
||||
filepath = (
|
||||
Path(__file__).parents[3]
|
||||
/ "ifcopenshell-python"
|
||||
/ "test"
|
||||
/ "IfcRelSpaceBoundary_TestFiles"
|
||||
/ "IfcRelSpaceBoundary2ndLevel"
|
||||
/ "HouseWithGarage_AC22_IFC2X3.ifc"
|
||||
).resolve()
|
||||
bpy.ops.bim.load_project(filepath=filepath.as_posix())
|
||||
ifc = tool.Ifc.get()
|
||||
return ifc
|
||||
|
||||
def _regenerate_space(self, ifc, space_id):
|
||||
space = ifc.by_id(space_id)
|
||||
obj = tool.Ifc.get_object(space)
|
||||
assert obj
|
||||
import numpy as np
|
||||
|
||||
original_verts = np.array([obj.matrix_world @ v.co for v in obj.data.vertices])
|
||||
original_bounds = (
|
||||
original_verts[:, 0].min(),
|
||||
original_verts[:, 0].max(),
|
||||
original_verts[:, 1].min(),
|
||||
original_verts[:, 1].max(),
|
||||
original_verts[:, 2].min(),
|
||||
original_verts[:, 2].max(),
|
||||
)
|
||||
original_origin = obj.matrix_world.translation.copy()
|
||||
|
||||
# Delete existing related IfcRelSpaceBoundary as in the manual repro.
|
||||
for b in list(space.BoundedBy or []):
|
||||
ifcopenshell.api.boundary.remove_boundary(ifc, b)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# Patch Spatial helpers so generate_space uses the active IfcSpace.
|
||||
original_get_selected_objects = tool.Spatial.get_selected_objects
|
||||
original_get_active_obj = tool.Spatial.get_active_obj
|
||||
try:
|
||||
tool.Spatial.get_selected_objects = classmethod(lambda cls: [obj])
|
||||
tool.Spatial.get_active_obj = classmethod(lambda cls: obj)
|
||||
bpy.ops.bim.generate_space()
|
||||
finally:
|
||||
tool.Spatial.get_selected_objects = original_get_selected_objects
|
||||
tool.Spatial.get_active_obj = original_get_active_obj
|
||||
|
||||
regen_verts = np.array([obj.matrix_world @ v.co for v in obj.data.vertices])
|
||||
regen_bounds = (
|
||||
regen_verts[:, 0].min(),
|
||||
regen_verts[:, 0].max(),
|
||||
regen_verts[:, 1].min(),
|
||||
regen_verts[:, 1].max(),
|
||||
regen_verts[:, 2].min(),
|
||||
regen_verts[:, 2].max(),
|
||||
)
|
||||
regen_origin = obj.matrix_world.translation.copy()
|
||||
return (original_bounds, original_origin), (regen_bounds, regen_origin)
|
||||
|
||||
def test_regenerate_space_5710_keeps_world_location(self):
|
||||
ifc = self.load_house_with_garage()
|
||||
(original_bounds, original_origin), (regen_bounds, regen_origin) = self._regenerate_space(ifc, 5710)
|
||||
assert (regen_origin - original_origin).length < 0.02
|
||||
for o, r in zip(original_bounds, regen_bounds):
|
||||
assert r == pytest.approx(o, abs=0.02)
|
||||
|
||||
def test_regenerate_space_2363_keeps_world_location(self):
|
||||
ifc = self.load_house_with_garage()
|
||||
(original_bounds, original_origin), (regen_bounds, regen_origin) = self._regenerate_space(ifc, 2363)
|
||||
assert (regen_origin - original_origin).length < 0.02
|
||||
# X and Y stable; Z may differ because the regenerated space detects the
|
||||
# sloped roof and clips the extrusion.
|
||||
for j in (0, 1, 2, 3, 4):
|
||||
assert regen_bounds[j] == pytest.approx(original_bounds[j], abs=0.02)
|
||||
# Verify the regenerated body contains boolean clipping (roof clipping).
|
||||
space = ifc.by_id(2363)
|
||||
body = ifcopenshell.util.representation.get_representation(space, "Model", "Body", "MODEL_VIEW")
|
||||
assert body is not None
|
||||
boolean_items = [i for i in (body.Items or []) if i.is_a("IfcBooleanClippingResult")]
|
||||
assert len(boolean_items) >= 1, "Expected roof clipping but got no boolean result"
|
||||
|
||||
def test_regenerate_space_twice_does_not_duplicate_half_spaces(self):
|
||||
ifc = self.load_house_with_garage()
|
||||
space = ifc.by_id(2363)
|
||||
obj = tool.Ifc.get_object(space)
|
||||
assert obj
|
||||
|
||||
for b in list(space.BoundedBy or []):
|
||||
ifcopenshell.api.boundary.remove_boundary(ifc, b)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
original_get_selected_objects = tool.Spatial.get_selected_objects
|
||||
original_get_active_obj = tool.Spatial.get_active_obj
|
||||
try:
|
||||
tool.Spatial.get_selected_objects = classmethod(lambda cls: [obj])
|
||||
tool.Spatial.get_active_obj = classmethod(lambda cls: obj)
|
||||
bpy.ops.bim.generate_space()
|
||||
bpy.ops.bim.generate_space()
|
||||
finally:
|
||||
tool.Spatial.get_selected_objects = original_get_selected_objects
|
||||
tool.Spatial.get_active_obj = original_get_active_obj
|
||||
|
||||
body_reps = [r for r in (space.Representation.Representations or []) if r.RepresentationIdentifier == "Body"]
|
||||
assert len(body_reps) == 1
|
||||
rep = body_reps[0]
|
||||
boolean_chains = [item for item in rep.Items if item.is_a("IfcBooleanClippingResult")]
|
||||
assert len(boolean_chains) <= 1
|
||||
if boolean_chains:
|
||||
half_space_ids = set()
|
||||
for item in ifc.traverse(boolean_chains[0]):
|
||||
if item.is_a("IfcHalfSpaceSolid"):
|
||||
assert item.id() not in half_space_ids, "Duplicate half-space solid in boolean chain"
|
||||
half_space_ids.add(item.id())
|
||||
|
||||
def test_regenerate_space_after_moving_roof_updates_shape(self):
|
||||
ifc = self.load_house_with_garage()
|
||||
space = ifc.by_id(2363)
|
||||
space_obj = tool.Ifc.get_object(space)
|
||||
assert space_obj
|
||||
|
||||
roof = ifc.by_id(5773)
|
||||
roof_obj = tool.Ifc.get_object(roof)
|
||||
assert roof_obj
|
||||
|
||||
for b in list(space.BoundedBy or []):
|
||||
ifcopenshell.api.boundary.remove_boundary(ifc, b)
|
||||
bpy.context.view_layer.objects.active = space_obj
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
space_obj.select_set(True)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
original_get_selected_objects = tool.Spatial.get_selected_objects
|
||||
original_get_active_obj = tool.Spatial.get_active_obj
|
||||
try:
|
||||
tool.Spatial.get_selected_objects = classmethod(lambda cls: [space_obj])
|
||||
tool.Spatial.get_active_obj = classmethod(lambda cls: space_obj)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
|
||||
roof_obj.hide_set(False)
|
||||
roof_obj.location.z += 1.0
|
||||
bpy.context.view_layer.update()
|
||||
tool.Geometry.commit_placement_if_moved(roof_obj)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
finally:
|
||||
tool.Spatial.get_selected_objects = original_get_selected_objects
|
||||
tool.Spatial.get_active_obj = original_get_active_obj
|
||||
|
||||
body_reps = [r for r in (space.Representation.Representations or []) if r.RepresentationIdentifier == "Body"]
|
||||
assert len(body_reps) == 1
|
||||
|
||||
def test_regenerate_space_is_stable_across_multiple_iterations(self):
|
||||
"""Regenerating the same space 5+ times must produce identical Z and bounds."""
|
||||
ifc = self.load_house_with_garage()
|
||||
space = ifc.by_id(2363)
|
||||
obj = tool.Ifc.get_object(space)
|
||||
assert obj
|
||||
|
||||
for b in list(space.BoundedBy or []):
|
||||
ifcopenshell.api.boundary.remove_boundary(ifc, b)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
original_get_selected_objects = tool.Spatial.get_selected_objects
|
||||
original_get_active_obj = tool.Spatial.get_active_obj
|
||||
|
||||
def snapshot():
|
||||
verts = np.array([obj.matrix_world @ v.co for v in obj.data.vertices], dtype=float)
|
||||
return (
|
||||
obj.matrix_world.translation.copy(),
|
||||
(
|
||||
float(verts[:, 0].min()),
|
||||
float(verts[:, 0].max()),
|
||||
float(verts[:, 1].min()),
|
||||
float(verts[:, 1].max()),
|
||||
float(verts[:, 2].min()),
|
||||
float(verts[:, 2].max()),
|
||||
),
|
||||
)
|
||||
|
||||
snapshots = []
|
||||
try:
|
||||
tool.Spatial.get_selected_objects = classmethod(lambda cls: [obj])
|
||||
tool.Spatial.get_active_obj = classmethod(lambda cls: obj)
|
||||
for _ in range(5):
|
||||
bpy.ops.bim.generate_space()
|
||||
snapshots.append(snapshot())
|
||||
finally:
|
||||
tool.Spatial.get_selected_objects = original_get_selected_objects
|
||||
tool.Spatial.get_active_obj = original_get_active_obj
|
||||
|
||||
ref_origin, ref_bounds = snapshots[0]
|
||||
for i, (origin, bounds) in enumerate(snapshots[1:], start=1):
|
||||
assert (
|
||||
origin - ref_origin
|
||||
).length < 0.02, f"Iteration {i}: Z drifted from {list(ref_origin)} to {list(origin)}"
|
||||
for j, (o, r) in enumerate(zip(ref_bounds, bounds)):
|
||||
assert r == pytest.approx(
|
||||
o, abs=0.02
|
||||
), f"Iteration {i} axis {j}: {o} != {r} full ref={ref_bounds} cur={bounds}"
|
||||
|
||||
|
||||
class TestGenerateSpaceLocation(NewFile):
|
||||
def test_generate_space_at_non_zero_cursor_location(self):
|
||||
bpy.ops.bim.create_project()
|
||||
ifc = tool.Ifc.get()
|
||||
# 4 thin walls forming a hollow box around (10, 20).
|
||||
_BlockHelper.create_thin_wall(ifc, 10.0, 20.0 + 4.8, 10.0, 0.4)
|
||||
_BlockHelper.create_thin_wall(ifc, 10.0, 20.0 - 4.8, 10.0, 0.4)
|
||||
_BlockHelper.create_thin_wall(ifc, 10.0 + 4.8, 20.0, 0.4, 10.0)
|
||||
_BlockHelper.create_thin_wall(ifc, 10.0 - 4.8, 20.0, 0.4, 10.0)
|
||||
bpy.context.scene.cursor.location = (10, 20, 0)
|
||||
|
||||
bpy.ops.bim.generate_space()
|
||||
space = bpy.data.objects["IfcSpace/Space"]
|
||||
mesh = space.data
|
||||
assert isinstance(mesh, bpy.types.Mesh)
|
||||
world_verts = np.array([space.matrix_world @ v.co for v in mesh.vertices])
|
||||
center = (world_verts.min(axis=0) + world_verts.max(axis=0)) / 2
|
||||
assert center[0] == pytest.approx(10.0, abs=0.1)
|
||||
assert center[1] == pytest.approx(20.0, abs=0.1)
|
||||
|
||||
@@ -428,8 +428,7 @@ void MainWindow::setupPanels() {
|
||||
spatial_panel_ = new modules::spatial_hierarchy::SpatialHierarchyPanel(this);
|
||||
properties_panel_ = new modules::properties::PropertiesPanel(this);
|
||||
|
||||
models_view_ = new modules::models::ModelsPanelView(
|
||||
models_panel_, session_state_, viewport_widget_->viewport(), this);
|
||||
models_view_ = new modules::models::ModelsPanelView(models_panel_, session_state_, this);
|
||||
spatial_view_ = new modules::spatial_hierarchy::SpatialHierarchyPanelView(spatial_panel_, session_state_, this);
|
||||
properties_view_ = new modules::properties::PropertiesPanelView(properties_panel_, session_state_, this);
|
||||
|
||||
@@ -482,13 +481,6 @@ void MainWindow::setupStatus() {
|
||||
status_mode_label_ = new QLabel("Ready", this);
|
||||
status_selection_label_ = new QLabel("No selection", this);
|
||||
status_perf_label_ = new QLabel(this);
|
||||
status_memory_label_ = new QLabel(this);
|
||||
status_memory_label_->setVisible(false);
|
||||
status_memory_label_->setToolTip(
|
||||
"The geometry in view needs more GPU memory than is available, so the "
|
||||
"viewer keeps the largest on-screen parts resident and streams the rest "
|
||||
"as you move. Right-click a model in the Models panel and choose "
|
||||
"\"Unload Model\" to free its GPU memory for the others.");
|
||||
status_progress_bar_ = new QProgressBar(this);
|
||||
status_perf_label_->setVisible(AppSettings::instance().showStats());
|
||||
status_progress_bar_->setMaximumWidth(200);
|
||||
@@ -497,7 +489,6 @@ void MainWindow::setupStatus() {
|
||||
statusBar()->setSizeGripEnabled(false);
|
||||
statusBar()->addWidget(status_mode_label_);
|
||||
statusBar()->addWidget(status_selection_label_, 1);
|
||||
statusBar()->addPermanentWidget(status_memory_label_);
|
||||
statusBar()->addPermanentWidget(status_perf_label_);
|
||||
statusBar()->addPermanentWidget(status_progress_bar_);
|
||||
|
||||
@@ -563,59 +554,16 @@ void MainWindow::setupLoader() {
|
||||
|
||||
connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this,
|
||||
[this](const ViewportWindow::FrameStats& stats) {
|
||||
const double mb = 1.0 / (1024.0 * 1024.0);
|
||||
|
||||
// Missing chunks are normal for a moment after every camera move
|
||||
// while streaming catches up; only a shortfall that persists means
|
||||
// the view does not fit, and only that is worth telling the user.
|
||||
constexpr qint64 kShortfallNoticeMs = 3000;
|
||||
if (stats.chunks_wanted_missing == 0) {
|
||||
memory_shortfall_since_.invalidate();
|
||||
status_memory_label_->setVisible(false);
|
||||
} else {
|
||||
if (!memory_shortfall_since_.isValid()) memory_shortfall_since_.start();
|
||||
if (memory_shortfall_since_.elapsed() >= kShortfallNoticeMs) {
|
||||
status_memory_label_->setText(
|
||||
QString("GPU memory full: %1 of %2 visible chunks (%3 MB) not loaded")
|
||||
.arg(stats.chunks_wanted_missing)
|
||||
.arg(stats.chunks_wanted)
|
||||
.arg(double(stats.wanted_missing_bytes) * mb, 0, 'f', 0));
|
||||
status_memory_label_->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!status_perf_label_->isVisible()) return;
|
||||
QString text =
|
||||
QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws | VRAM %8/%9 MB")
|
||||
status_perf_label_->setText(
|
||||
QString("%1 fps | %2 ms | %3/%4 obj | %5/%6 tri | %7 draws")
|
||||
.arg(stats.fps, 0, 'f', 1)
|
||||
.arg(stats.frame_time_ms, 0, 'f', 1)
|
||||
.arg(stats.visible_objects)
|
||||
.arg(stats.total_objects)
|
||||
.arg(stats.visible_triangles)
|
||||
.arg(stats.total_triangles)
|
||||
.arg(stats.gl_draw_calls)
|
||||
.arg(double(stats.vram_used_bytes) * mb, 0, 'f', 0)
|
||||
.arg(double(stats.vram_capacity_bytes) * mb, 0, 'f', 0);
|
||||
// The budget is where the pool may grow to; the pool can also sit
|
||||
// a sub-buffer above it (a release would undershoot). Show it
|
||||
// only when it tells the user something capacity does not.
|
||||
if (stats.vram_budget_bytes > 0
|
||||
&& stats.vram_budget_bytes != stats.vram_capacity_bytes) {
|
||||
text += QString(" (budget %1)")
|
||||
.arg(double(stats.vram_budget_bytes) * mb, 0, 'f', 0);
|
||||
}
|
||||
// Device total is only known when a driver backend answered.
|
||||
if (stats.device_vram_total_bytes > 0) {
|
||||
text += QString(" | Device %1/%2 MB")
|
||||
.arg(double(stats.device_vram_used_bytes) * mb, 0, 'f', 0)
|
||||
.arg(double(stats.device_vram_total_bytes) * mb, 0, 'f', 0);
|
||||
}
|
||||
if (stats.chunks_wanted_missing > 0) {
|
||||
text += QString(" | %1/%2 chunks waiting")
|
||||
.arg(stats.chunks_wanted_missing)
|
||||
.arg(stats.chunks_wanted);
|
||||
}
|
||||
status_perf_label_->setText(text);
|
||||
.arg(stats.gl_draw_calls));
|
||||
});
|
||||
connect(viewport_widget_->viewport(), &ViewportWindow::objectPicked,
|
||||
this, [this](uint32_t object_id) {
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <QStringList>
|
||||
|
||||
class QLabel;
|
||||
#include <QElapsedTimer>
|
||||
class QDockWidget;
|
||||
class QMenu;
|
||||
class QProgressBar;
|
||||
@@ -71,10 +70,6 @@ private:
|
||||
QLabel* status_mode_label_ = nullptr;
|
||||
QLabel* status_selection_label_ = nullptr;
|
||||
QLabel* status_perf_label_ = nullptr;
|
||||
// Shown while the visible geometry persistently exceeds what fits in
|
||||
// GPU memory (see onFrameStats): the user's cue to unload models.
|
||||
QLabel* status_memory_label_ = nullptr;
|
||||
QElapsedTimer memory_shortfall_since_;
|
||||
QProgressBar* status_progress_bar_ = nullptr;
|
||||
bonsaiviewer::components::TabBar* ribbon_tabs_ = nullptr;
|
||||
QStackedWidget* ribbon_pages_ = nullptr;
|
||||
|
||||
@@ -199,10 +199,6 @@ void SessionState::notifyModelGeometryReady(uint32_t session_model_id) {
|
||||
emit modelGeometryReady(session_model_id);
|
||||
}
|
||||
|
||||
void SessionState::notifyModelLoadStateChanged(const QString& model_id) {
|
||||
emit modelLoadStateChanged(model_id);
|
||||
}
|
||||
|
||||
void SessionState::notifyProjectOpened(const QString& path) {
|
||||
emit projectOpened(path);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ public:
|
||||
void notifyFederationChanged();
|
||||
void notifyVisibilityChanged();
|
||||
void notifyModelGeometryReady(uint32_t session_model_id);
|
||||
void notifyModelLoadStateChanged(const QString& model_id);
|
||||
void notifyProjectOpened(const QString& path);
|
||||
void notifyProjectSaved(const QString& path);
|
||||
void notifyProjectReset();
|
||||
@@ -108,10 +107,6 @@ signals:
|
||||
// for both sidecar-cache and stream loads; subscribers that just need to
|
||||
// re-derive view state (e.g. ViewportView::refresh) listen to this.
|
||||
void modelGeometryReady(uint32_t session_model_id);
|
||||
// Fires when a model was unloaded from, or loaded back onto, the GPU
|
||||
// (commands::unloadModel / loadModel). The viewport is the authority
|
||||
// for the state itself — ViewportWindow::isModelUnloaded.
|
||||
void modelLoadStateChanged(const QString& model_id);
|
||||
// Fires when a model's live IFC data source (the .ifc/.rdb, opened in the
|
||||
// background after a sidecar-cache hit) becomes available for queries —
|
||||
// e.g. so the spatial hierarchy can be built once the file is loaded.
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "ViewerSettings.h"
|
||||
#include "components/Style.h"
|
||||
#include "modules/models/Commands.h"
|
||||
#include "../ifcparse/parse.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
@@ -56,7 +55,6 @@ int main(int argc, char* argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName("Bonsai Viewer");
|
||||
app.setOrganizationName("IfcOpenShell");
|
||||
app.setApplicationVersion(QString::fromUtf8(IFCOPENSHELL_VERSION));
|
||||
|
||||
// Clear any .rdbview extractions left in temp by a previous session.
|
||||
bonsaiviewer::modules::models::commands::cleanupRdbviewCache();
|
||||
@@ -72,7 +70,6 @@ int main(int argc, char* argv[]) {
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription("Bonsai Viewer — IfcOpenShell IFC viewer");
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
parser.process(app);
|
||||
|
||||
installUiFont();
|
||||
|
||||
@@ -262,28 +262,6 @@ void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host,
|
||||
session.setStatusMessage("Models", "Model removed");
|
||||
}
|
||||
|
||||
void unloadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id) {
|
||||
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
|
||||
if (session_model_id == 0) return;
|
||||
if (session.loader()->isLoadingModel(session_model_id)) return;
|
||||
const double freed_mb = double(viewport.modelVramBytes(session_model_id)) / (1024.0 * 1024.0);
|
||||
viewport.unloadModel(session_model_id);
|
||||
session.notifyModelLoadStateChanged(model_id);
|
||||
session.setStatusMessage("Models", QString("Model unloaded (freed %1 MB of GPU memory)")
|
||||
.arg(freed_mb, 0, 'f', 0));
|
||||
}
|
||||
|
||||
void loadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id) {
|
||||
const uint32_t session_model_id = session.sessionModelIdForModelId(model_id);
|
||||
if (session_model_id == 0) return;
|
||||
if (!viewport.loadModel(session_model_id)) {
|
||||
session.setStatusMessage("Models", "Not enough GPU memory to load this model");
|
||||
return;
|
||||
}
|
||||
session.notifyModelLoadStateChanged(model_id);
|
||||
session.setStatusMessage("Models", "Model loaded");
|
||||
}
|
||||
|
||||
void viewModels(SessionState& session, ViewportWindow& viewport, const QStringList& model_ids) {
|
||||
// Federation ids are the panel's currency; the viewport speaks session
|
||||
// model ids. sessionModelIdForModelId returns 0 for a model the viewport
|
||||
|
||||
@@ -60,12 +60,6 @@ void moveGroup(SessionState& session, const QString& id, const QString& parent_g
|
||||
void moveModels(SessionState& session, const QStringList& ids, const QString& parent_group_id);
|
||||
void removeGroup(SessionState& session, QWidget& host, const QString& group_id);
|
||||
void removeModel(SessionState& session, ViewportWindow& viewport, QWidget& host, const QString& model_id);
|
||||
// GPU residency, distinct from visibility (hide) and from membership
|
||||
// (remove): unloadModel frees everything the model holds on the device
|
||||
// while it stays in the federation; loadModel brings it back. Both emit
|
||||
// modelLoadStateChanged.
|
||||
void unloadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id);
|
||||
void loadModel(SessionState& session, ViewportWindow& viewport, const QString& model_id);
|
||||
// "View Selected Model" — frame the camera on just these models' geometry, the
|
||||
// way View All frames the whole federation. Models that carry no loaded
|
||||
// geometry (never loaded, or still streaming their metadata) contribute
|
||||
|
||||
@@ -25,25 +25,16 @@
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QFont>
|
||||
#include <QColor>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
QStandardItem* siblingItem(QStandardItem* name_item, Column column) {
|
||||
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
|
||||
QStandardItem* parent = name_item->parent();
|
||||
if (!parent) parent = name_item->model()->invisibleRootItem();
|
||||
return parent->child(name_item->row(), int(column));
|
||||
}
|
||||
|
||||
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
|
||||
return siblingItem(name_item, VisibilityColumn);
|
||||
}
|
||||
|
||||
QString formatMegabytes(quint64 bytes) {
|
||||
return QString("%1 MB").arg(double(bytes) / (1024.0 * 1024.0), 0, 'f', 0);
|
||||
return parent->child(name_item->row(), 1);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
@@ -60,7 +51,7 @@ FederationItemModel::FederationItemModel(Federation* federation, QObject* parent
|
||||
: QStandardItemModel(parent)
|
||||
, federation_(federation)
|
||||
{
|
||||
setColumnCount(ColumnCount);
|
||||
setColumnCount(2);
|
||||
rebuildAll();
|
||||
|
||||
connect(federation_, &Federation::groupAdded, this, &FederationItemModel::onGroupAdded);
|
||||
@@ -76,7 +67,7 @@ FederationItemModel::FederationItemModel(Federation* federation, QObject* parent
|
||||
|
||||
void FederationItemModel::rebuildAll() {
|
||||
clear();
|
||||
setColumnCount(ColumnCount);
|
||||
setColumnCount(2);
|
||||
id_to_name_item_.clear();
|
||||
|
||||
for (const auto& root_group : federation_->rootGroups()) {
|
||||
@@ -130,14 +121,6 @@ QStandardItem* FederationItemModel::makeVisibilityItem(ItemKind kind, bool visib
|
||||
return item;
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::makeMemoryItem() const {
|
||||
auto* item = new QStandardItem(QString());
|
||||
item->setEditable(false);
|
||||
item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
item->setForeground(QBrush(QColor(bonsaiviewer::ViewerSettings::instance().color("disabled_text"))));
|
||||
return item;
|
||||
}
|
||||
|
||||
void FederationItemModel::styleRowVisibility(QStandardItem* name_item, bool visible) const {
|
||||
QStandardItem* vis_item = siblingVisibilityItem(name_item);
|
||||
if (visible) {
|
||||
@@ -150,22 +133,6 @@ void FederationItemModel::styleRowVisibility(QStandardItem* name_item, bool visi
|
||||
}
|
||||
}
|
||||
|
||||
void FederationItemModel::setModelResidency(const QString& model_id, bool unloaded, quint64 vram_bytes) {
|
||||
QStandardItem* name_item = findItem(model_id);
|
||||
if (!name_item) return;
|
||||
QStandardItem* memory_item = siblingItem(name_item, MemoryColumn);
|
||||
if (!memory_item) return;
|
||||
const QString text = unloaded ? QStringLiteral("unloaded")
|
||||
: vram_bytes > 0 ? formatMegabytes(vram_bytes)
|
||||
: QString();
|
||||
if (memory_item->text() != text) memory_item->setText(text);
|
||||
QFont font = name_item->font();
|
||||
if (font.italic() != unloaded) {
|
||||
font.setItalic(unloaded);
|
||||
name_item->setFont(font);
|
||||
}
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::findItem(const QString& id) const {
|
||||
return id_to_name_item_.value(id, nullptr);
|
||||
}
|
||||
@@ -181,7 +148,7 @@ void FederationItemModel::appendModelTo(QStandardItem* parent_item, const QStrin
|
||||
if (!model) return;
|
||||
auto* name_item = makeModelNameItem(model_id, model->display_name);
|
||||
auto* vis_item = makeVisibilityItem(ItemKind::Model, federation_->isModelEffectivelyVisible(model_id));
|
||||
parent_item->appendRow({name_item, makeMemoryItem(), vis_item});
|
||||
parent_item->appendRow({name_item, vis_item});
|
||||
id_to_name_item_.insert(model_id, name_item);
|
||||
styleRowVisibility(name_item, federation_->isModelEffectivelyVisible(model_id));
|
||||
}
|
||||
@@ -191,7 +158,7 @@ void FederationItemModel::appendGroupSubtreeTo(QStandardItem* parent_item, const
|
||||
if (!group) return;
|
||||
auto* name_item = makeGroupNameItem(group_id, group->display_name);
|
||||
auto* vis_item = makeVisibilityItem(ItemKind::Group, group->visible);
|
||||
parent_item->appendRow({name_item, makeMemoryItem(), vis_item});
|
||||
parent_item->appendRow({name_item, vis_item});
|
||||
id_to_name_item_.insert(group_id, name_item);
|
||||
styleRowVisibility(name_item, group->visible);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class Federation;
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
// QStandardItemModel that mirrors the Federation tree (groups + models in
|
||||
// three columns: name, GPU memory, visibility icon). Subscribes directly to Federation's
|
||||
// two columns: name + visibility icon). Subscribes directly to Federation's
|
||||
// granular signals so each mutation only touches the affected rows — view
|
||||
// state (expansion, selection, scroll) is preserved automatically.
|
||||
//
|
||||
@@ -57,12 +57,6 @@ public:
|
||||
// previously- and newly-active model rows.
|
||||
void setActiveModelId(const QString& model_id);
|
||||
|
||||
// GPU residency is viewport state, not Federation state, so it is pushed
|
||||
// in by the owning View: the memory column shows `vram_bytes` for a
|
||||
// loaded model and "unloaded" for one the user unloaded (which is also
|
||||
// drawn in italics). Models the viewport knows nothing about show blank.
|
||||
void setModelResidency(const QString& model_id, bool unloaded, quint64 vram_bytes);
|
||||
|
||||
private slots:
|
||||
void onGroupAdded(const QString& group_id);
|
||||
void onGroupRemoved(const QString& group_id);
|
||||
@@ -78,7 +72,6 @@ private:
|
||||
QStandardItem* makeGroupNameItem(const QString& group_id, const QString& display_name) const;
|
||||
QStandardItem* makeModelNameItem(const QString& model_id, const QString& display_name) const;
|
||||
QStandardItem* makeVisibilityItem(ItemKind kind, bool visible) const;
|
||||
QStandardItem* makeMemoryItem() const;
|
||||
void styleRowVisibility(QStandardItem* name_item, bool visible) const;
|
||||
|
||||
QStandardItem* findItem(const QString& id) const;
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDrag>
|
||||
@@ -80,7 +79,6 @@ QStringList selectedModelIdsAt(QTreeView* tree, const QModelIndex& clicked_index
|
||||
}
|
||||
|
||||
constexpr int kVisibilityColumnWidth = 28;
|
||||
constexpr int kMemoryColumnWidth = 72; // "1234 MB" / "unloaded"
|
||||
|
||||
// QTreeView subclass that handles drag-and-drop. Drop logic dispatches
|
||||
// through commands (not directly into the model) so notifications + status
|
||||
@@ -252,7 +250,7 @@ ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
|
||||
|
||||
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
|
||||
if (!index.isValid()) return;
|
||||
if (index.column() == VisibilityColumn) {
|
||||
if (index.column() == 1) {
|
||||
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
|
||||
return;
|
||||
}
|
||||
@@ -382,24 +380,6 @@ ModelsPanel::ModelsPanel(bonsaiviewer::SessionState* session_state,
|
||||
commands::saveModelAsToCloud(*session_state_, *this, id);
|
||||
});
|
||||
|
||||
// GPU residency. Unload keeps the model in the federation (and
|
||||
// its visibility) but frees everything it holds on the GPU — the
|
||||
// lever when the scene does not fit in VRAM. Load brings it back.
|
||||
menu.addSeparator();
|
||||
const uint32_t session_model_id = session_state_->sessionModelIdForModelId(id);
|
||||
const bool unloaded = session_model_id != 0 && viewport_->isModelUnloaded(session_model_id);
|
||||
QAction* residency = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/cube.svg"),
|
||||
unloaded ? "Load Model" : "Unload Model");
|
||||
residency->setEnabled(session_model_id != 0);
|
||||
residency->setToolTip(unloaded
|
||||
? "Allocate GPU memory for this model again and stream its geometry back in."
|
||||
: "Free this model's GPU memory while keeping it in the federation.");
|
||||
connect(residency, &QAction::triggered, this, [this, id, unloaded]() {
|
||||
if (unloaded) commands::loadModel(*session_state_, *viewport_, id);
|
||||
else commands::unloadModel(*session_state_, *viewport_, id);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
QAction* remove = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model");
|
||||
@@ -429,16 +409,14 @@ void ModelsPanel::setModel(FederationItemModel* model) {
|
||||
}
|
||||
|
||||
void ModelsPanel::applyColumnLayout() {
|
||||
// The name stretches to fill; memory and visibility are fixed.
|
||||
// Column 0 (name) stretches to fill; column 1 (visibility icon) is fixed.
|
||||
QHeaderView* header = tree_->header();
|
||||
if (header->count() < ColumnCount) return;
|
||||
if (header->count() < 2) return;
|
||||
header->setStretchLastSection(false);
|
||||
header->setMinimumSectionSize(kVisibilityColumnWidth);
|
||||
header->setSectionResizeMode(NameColumn, QHeaderView::Stretch);
|
||||
header->setSectionResizeMode(MemoryColumn, QHeaderView::Fixed);
|
||||
header->resizeSection(MemoryColumn, kMemoryColumnWidth);
|
||||
header->setSectionResizeMode(VisibilityColumn, QHeaderView::Fixed);
|
||||
header->resizeSection(VisibilityColumn, kVisibilityColumnWidth);
|
||||
header->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
header->setSectionResizeMode(1, QHeaderView::Fixed);
|
||||
header->resizeSection(1, kVisibilityColumnWidth);
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
@@ -31,14 +31,6 @@ enum class ItemKind {
|
||||
Model,
|
||||
};
|
||||
|
||||
// Columns of the models tree: name | GPU memory | visibility eye.
|
||||
enum Column : int {
|
||||
NameColumn = 0,
|
||||
MemoryColumn = 1,
|
||||
VisibilityColumn = 2,
|
||||
ColumnCount = 3,
|
||||
};
|
||||
|
||||
struct TreeNode {
|
||||
QString id;
|
||||
QString name;
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
#include "../../../ifcviewer/ViewportWindow.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
|
||||
@@ -57,19 +54,17 @@ QList<GroupOption> validMoveTargets(const Federation& federation,
|
||||
|
||||
ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
bonsaiviewer::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, widget_(widget)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
, model_(new FederationItemModel(session_state->federation(), this))
|
||||
{
|
||||
widget_->setModel(model_);
|
||||
|
||||
// Coarse signals: full rebuild + re-style. The granular Federation
|
||||
// signals are handled inside FederationItemModel and don't reach here.
|
||||
auto rebuild = [this]() { model_->rebuildAll(); refreshResidency(); };
|
||||
auto rebuild = [this]() { model_->rebuildAll(); };
|
||||
connect(session_state_, &SessionState::projectReset, this, rebuild);
|
||||
connect(session_state_, &SessionState::projectOpened, this, rebuild);
|
||||
connect(&bonsaiviewer::ViewerSettings::instance(),
|
||||
@@ -78,30 +73,6 @@ ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
connect(session_state_, &SessionState::activeModelChanged, this, [this](const QString& model_id) {
|
||||
model_->setActiveModelId(model_id);
|
||||
});
|
||||
|
||||
// Residency: immediately on the events that change it, and on a slow
|
||||
// tick for the memory figures, which move as chunks stream.
|
||||
auto refresh = [this]() { refreshResidency(); };
|
||||
connect(session_state_, &SessionState::modelLoadStateChanged, this, refresh);
|
||||
connect(session_state_, &SessionState::modelGeometryReady, this, refresh);
|
||||
connect(session_state_, &SessionState::modelsChanged, this, refresh);
|
||||
auto* tick = new QTimer(this);
|
||||
tick->setInterval(1000);
|
||||
connect(tick, &QTimer::timeout, this, refresh);
|
||||
tick->start();
|
||||
}
|
||||
|
||||
void ModelsPanelView::refreshResidency() {
|
||||
for (const auto& model : session_state_->federation()->models()) {
|
||||
const uint32_t session_model_id = session_state_->sessionModelIdForModelId(model.id);
|
||||
if (session_model_id == 0) {
|
||||
model_->setModelResidency(model.id, false, 0);
|
||||
continue;
|
||||
}
|
||||
model_->setModelResidency(model.id,
|
||||
viewport_->isModelUnloaded(session_model_id),
|
||||
viewport_->modelVramBytes(session_model_id));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::models
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <QObject>
|
||||
|
||||
class Federation;
|
||||
class ViewportWindow;
|
||||
namespace bonsaiviewer { class SessionState; }
|
||||
|
||||
namespace bonsaiviewer::modules::models {
|
||||
@@ -46,25 +45,16 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -76,12 +76,12 @@ int main() {
|
||||
// By adding a building, a hierarchy has been automatically created that consists of the following
|
||||
// structure: IfcProject > IfcSite > IfcBuilding
|
||||
|
||||
// Lateron changing the name of the IfcProject can be done by obtaining a reference to the
|
||||
// Lateron changing the name of the IfcProject can be done by obtaining a reference to the
|
||||
// project, which has been created automatically.
|
||||
file.getSingle<IfcSchema::IfcProject>().setName("IfcAdvancedHouse"s);
|
||||
|
||||
// To demonstrate the ability to serialize arbitrary opencascade solids a building envelope is
|
||||
// constructed by applying boolean operations. Naturally, in IFC, building elements should be
|
||||
// constructed by applying boolean operations. Naturally, in IFC, building elements should be
|
||||
// modeled separately, with rich parametric and relational semantics. Creating geometry in this
|
||||
// way does not preserve any history and is merely a demonstration of technical capabilities.
|
||||
TopoDS_Shape outer = BRepPrimAPI_MakeBox(gp_Pnt(-5000., -180., -2000.), gp_Pnt(5000., 5180., 3000.)).Shape();
|
||||
@@ -101,13 +101,8 @@ int main() {
|
||||
// IfcFacetedBRep. If it would not be a polyhedron, serialise() can only be successful when linked
|
||||
// to the IFC4 model and with `advanced` set to `true` which introduces IfcAdvancedFace. It would
|
||||
// return `0` otherwise.
|
||||
auto building_shape_result = ifcopenshell::geom::serialise(file, building_shell, false);
|
||||
if (!building_shape_result) {
|
||||
std::cerr << "Failed to serialize building shell." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
auto building_shape = building_shape_result.as<IfcSchema::IfcProductDefinitionShape>();
|
||||
|
||||
auto building_shape = ifcopenshell::geom::serialise(file, building_shell, false).as<IfcSchema::IfcProductDefinitionShape>();
|
||||
|
||||
file.add_entity(building_shape);
|
||||
auto building_representations = building_shape.Representations();
|
||||
building_representations.front().setContextOfItems(file.getRepresentationContext("model"));
|
||||
@@ -127,7 +122,7 @@ int main() {
|
||||
ground_representation = ifcopenshell::geom::tesselate(file, shape, 100.);
|
||||
}
|
||||
file.getSingle<IfcSchema::IfcSite>().setRepresentation(ground_representation.as<IfcSchema::IfcProductDefinitionShape>());
|
||||
|
||||
|
||||
auto ground_reps = file.getSingle<IfcSchema::IfcSite>().Representation().Representations();
|
||||
for (auto& rep : ground_reps) {
|
||||
rep.setContextOfItems(file.getRepresentationContext("Model"));
|
||||
@@ -180,10 +175,10 @@ void createGroundShape(TopoDS_Shape& shape) {
|
||||
cv.SetValue(4, 4, gp_Pnt( 10000, 10000, -8130));
|
||||
TColStd_Array1OfReal knots(0, 1);
|
||||
knots(0) = 0;
|
||||
knots(1) = 1;
|
||||
knots(1) = 1;
|
||||
TColStd_Array1OfInteger mult(0, 1);
|
||||
mult(0) = 5;
|
||||
mult(1) = 5;
|
||||
mult(1) = 5;
|
||||
Handle(Geom_BSplineSurface) surf = new Geom_BSplineSurface(cv, knots, knots, mult, mult, 4, 4);
|
||||
#if OCC_VERSION_HEX < 0x60502
|
||||
shape = BRepBuilderAPI_MakeFace(surf);
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
// alignment explicitly
|
||||
|
||||
// Disable warnings coming from IfcOpenShell
|
||||
#if defined(_MSC_VER)
|
||||
#pragma warning(disable : 4018 4267 4250 4984 4985)
|
||||
#endif
|
||||
|
||||
#include "../ifcparse/schemas/Ifc4x3_add2.h"
|
||||
#include "../ifcparse/hierarchy_helper.h"
|
||||
@@ -72,7 +70,7 @@ Schema::IfcProject setup_project(hierarchy_helper<Schema>& file) {
|
||||
dimensions.setThermodynamicTemperatureExponent(0);
|
||||
dimensions.setAmountOfSubstanceExponent(0);
|
||||
dimensions.setLuminousIntensityExponent(0);
|
||||
|
||||
|
||||
auto conversion_factor = file.create<Schema::IfcMeasureWithUnit>();
|
||||
auto length = file.create<Schema::IfcLengthMeasure>();
|
||||
length.set_attribute_value(0, 304.80);
|
||||
@@ -84,7 +82,7 @@ Schema::IfcProject setup_project(hierarchy_helper<Schema>& file) {
|
||||
conversion_based_unit.setUnitType(Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT);
|
||||
conversion_based_unit.setName("FEET");
|
||||
conversion_based_unit.setConversionFactor(conversion_factor);
|
||||
|
||||
|
||||
units.erase(std::remove(units.begin(), units.end(), unit)); // remove the millimeter unit
|
||||
units.push_back(conversion_based_unit); // add the feet unit
|
||||
units_in_context.setUnits(units); // update the UnitsInContext
|
||||
@@ -388,7 +386,7 @@ int main() {
|
||||
nests_horizontal_segments.setName("Nests horizontal alignment segments with horizontal alignment");
|
||||
nests_horizontal_segments.setRelatingObject(horizontal_alignment);
|
||||
nests_horizontal_segments.setRelatedObjects(horizontal_segments);
|
||||
|
||||
|
||||
//
|
||||
// Create plan view footprint model representation for the horizontal alignment
|
||||
//
|
||||
@@ -405,7 +403,7 @@ int main() {
|
||||
footprint_shape_representation.setRepresentationType("Curve2D");
|
||||
// the composite curve is a representation item
|
||||
footprint_shape_representation.setItems({composite_curve});
|
||||
|
||||
|
||||
//
|
||||
// Define vertical profile segments
|
||||
//
|
||||
@@ -541,7 +539,7 @@ int main() {
|
||||
nests_alignment_layouts.setName("Nest horizontal and vertical alignment layouts with the alignment");
|
||||
nests_alignment_layouts.setRelatingObject(alignment);
|
||||
nests_alignment_layouts.setRelatedObjects({horizontal_alignment, vertical_profile});
|
||||
|
||||
|
||||
// Define the relationship with the project
|
||||
|
||||
// IFC 4.1.4.1.1 "Every IfcAlignment must be related to IfcProject using the IfcRelAggregates relationship"
|
||||
@@ -552,7 +550,7 @@ int main() {
|
||||
aggregate_alignments_with_project.setName("Alignments in project");
|
||||
aggregate_alignments_with_project.setRelatingObject(project);
|
||||
aggregate_alignments_with_project.setRelatedObjects({alignment});
|
||||
|
||||
|
||||
// Define the spatial structure of the alignment with respect to the site
|
||||
|
||||
// IFC 4.1.5.1 alignment is referenced in spatial structure of an IfcSpatialElement. In this case IfcSite is the highest level IfcSpatialElement
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
// Some convenience typedefs and definitions.
|
||||
// Some convenience typedefs and definitions.
|
||||
typedef ifcopenshell::global_id guid;
|
||||
typedef std::pair<double, double> XY;
|
||||
#ifdef SCHEMA_HAS_IfcPresentationStyleAssignment
|
||||
@@ -295,9 +295,9 @@ int main() {
|
||||
west_void.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
|
||||
west_void.setRelatingBuildingElement(west_wall);
|
||||
west_void.setRelatedOpeningElement(west_opening_copy);
|
||||
|
||||
// Up until now we have only used simple extrusions for the creation of the geometry. For the
|
||||
// ground mesh of the IfcSite we will use a Nurbs surface created in Open Cascade. The surface
|
||||
|
||||
// Up until now we have only used simple extrusions for the creation of the geometry. For the
|
||||
// ground mesh of the IfcSite we will use a Nurbs surface created in Open Cascade. The surface
|
||||
// will be tessellated using the deflection specified.
|
||||
TopoDS_Shape shape;
|
||||
createGroundShape(shape);
|
||||
@@ -325,7 +325,7 @@ int main() {
|
||||
site_prop.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
|
||||
site_prop.setRelatedObjects({file.getSingle<IfcSchema::IfcSite>()});
|
||||
site_prop.setRelatingPropertyDefinition(pset);
|
||||
|
||||
|
||||
auto ground_reps = file.getSingle<IfcSchema::IfcSite>().Representation().Representations();
|
||||
for (auto& rep : ground_reps) {
|
||||
rep.setContextOfItems(file.getRepresentationContext("Model"));
|
||||
@@ -334,11 +334,11 @@ int main() {
|
||||
setSurfaceColour(file,ground_representation, 0.15, 0.25, 0.05);
|
||||
|
||||
// According to the Ifc2x3 schema an IfcWallStandardCase needs to have an IfcMaterialLayerSet
|
||||
// assigned. Note that this material definition is independent of the surface styles we have
|
||||
// been assigning to the walls already. The surface styles determine the colour in the
|
||||
// assigned. Note that this material definition is independent of the surface styles we have
|
||||
// been assigning to the walls already. The surface styles determine the colour in the
|
||||
// '3D viewport' of most applications.
|
||||
// Some BIM authoring applications, such as Autodesk Revit, ignore the geometrical representation
|
||||
// by and large and construct native walls using the layer thickness and reference line offset
|
||||
// by and large and construct native walls using the layer thickness and reference line offset
|
||||
// provided here.
|
||||
auto material = file.create<IfcSchema::IfcMaterial>();
|
||||
material.setName("Brick");
|
||||
@@ -422,7 +422,7 @@ int main() {
|
||||
#endif
|
||||
|
||||
door.setRepresentation(file.addBox(80, 80, 2120, IfcSchema::IfcAxis2Placement2D{}, file.addPlacement3d(460, 0, 0)));
|
||||
|
||||
|
||||
auto door_representations = door.Representation().Representations();
|
||||
IfcSchema::IfcShapeRepresentation door_body;
|
||||
for (auto& rep : door_representations) {
|
||||
@@ -465,9 +465,9 @@ int main() {
|
||||
#endif
|
||||
|
||||
// Surface styles are assigned to representation items, hence there is no real limitation to
|
||||
// assign different colours within the same representation. However, some viewers have
|
||||
// difficulties rendering products with representation items with different surface styles.
|
||||
// Therefore we will construct the window as a decomposition of beams and a plate, in which
|
||||
// assign different colours within the same representation. However, some viewers have
|
||||
// difficulties rendering products with representation items with different surface styles.
|
||||
// Therefore we will construct the window as a decomposition of beams and a plate, in which
|
||||
// only the plate will have a transparent material assigned.
|
||||
|
||||
// The window frame will consists of four separate beams.
|
||||
@@ -476,7 +476,7 @@ int main() {
|
||||
// match the bounding box of the representation. Furthermore, the window placement needs
|
||||
// to align with the lowerleft corner of the constituent parts.
|
||||
std::vector<IfcSchema::IfcShapeRepresentation> frame_representations;
|
||||
|
||||
|
||||
auto horizontal_bar = file.addEmptyRepresentation();
|
||||
auto vertical_bar = file.addEmptyRepresentation();
|
||||
file.addBox(horizontal_bar, 1860, 90, 90);
|
||||
@@ -498,7 +498,7 @@ int main() {
|
||||
// Because of the duplication the iterator is incremented twice
|
||||
}
|
||||
|
||||
// This window will be placed at five locations within the building. A list of placements is
|
||||
// This window will be placed at five locations within the building. A list of placements is
|
||||
// created and is iterated over to create all window instances.
|
||||
std::vector<IfcSchema::IfcLocalPlacement> window_placements;
|
||||
window_placements.push_back(file.addLocalPlacement(storey_placement, 2*-1770-430-930, -45, 400));
|
||||
@@ -506,7 +506,7 @@ int main() {
|
||||
window_placements.push_back(file.addLocalPlacement(storey_placement, -430-930, -45, 400));
|
||||
window_placements.push_back(file.addLocalPlacement(storey_placement, 3000-930, -45, 400));
|
||||
window_placements.push_back(file.addLocalPlacement(storey_placement, -4855+45, 885-930, 400, 0, 0, 1, 0, 1, 0));
|
||||
|
||||
|
||||
for (auto& place : window_placements) {
|
||||
|
||||
// Create the window at the current location
|
||||
@@ -520,7 +520,7 @@ int main() {
|
||||
window.setPredefinedType(IfcSchema::IfcWindowTypeEnum::IfcWindowType_WINDOW);
|
||||
window.setPartitioningType(IfcSchema::IfcWindowTypePartitioningEnum::IfcWindowTypePartitioning_SINGLE_PANEL);
|
||||
#endif
|
||||
file.addBuildingProduct(window);
|
||||
file.addBuildingProduct(window);
|
||||
|
||||
// Initialize a list of parts for the window to be composed of
|
||||
std::vector<IfcSchema::IfcObjectDefinition> window_parts;
|
||||
@@ -532,7 +532,7 @@ int main() {
|
||||
frame_placements.push_back(file.addLocalPlacement(storey_placement, 930, 45, 1510));
|
||||
frame_placements.push_back(file.addLocalPlacement(storey_placement, -885+930, 45, 90));
|
||||
frame_placements.push_back(file.addLocalPlacement(storey_placement, 885+930, 45, 90));
|
||||
|
||||
|
||||
// Now iterate over the placements and representations of the beam and add them to list of parts
|
||||
std::vector<IfcSchema::IfcLocalPlacement>::const_iterator frame_placement;
|
||||
std::vector<IfcSchema::IfcShapeRepresentation>::const_iterator frame_representation;
|
||||
@@ -565,7 +565,7 @@ int main() {
|
||||
window_parts.push_back(glass_part);
|
||||
file.relatePlacements(window, glass_part);
|
||||
setSurfaceColour(file, glass_part.Representation(), 0.6, 0.7, 0.75, 0.1);
|
||||
|
||||
|
||||
// Now create a decomposition relation between the window and the parts. Most viewers and authoring
|
||||
// tools will consider the window a single entity that can be selected as a whole.
|
||||
{
|
||||
@@ -612,10 +612,10 @@ void createGroundShape(TopoDS_Shape& shape) {
|
||||
cv.SetValue(4, 4, gp_Pnt( 10000, 10000, -8130));
|
||||
TColStd_Array1OfReal knots(0, 1);
|
||||
knots(0) = 0;
|
||||
knots(1) = 1;
|
||||
knots(1) = 1;
|
||||
TColStd_Array1OfInteger mult(0, 1);
|
||||
mult(0) = 5;
|
||||
mult(1) = 5;
|
||||
mult(1) = 5;
|
||||
Handle(Geom_BSplineSurface) surf = new Geom_BSplineSurface(cv, knots, knots, mult, mult, 4, 4);
|
||||
#if OCC_VERSION_HEX < 0x60502
|
||||
shape = BRepBuilderAPI_MakeFace(surf);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
#include INCLUDE_SCHEMA(ifcparse/schemas, IfcSchema)
|
||||
#include INCLUDE_SCHEMA_DEFINITIONS(ifcparse/schemas, IfcSchema)
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#ifdef _MSC_VER
|
||||
#define strcasecmp _stricmp
|
||||
#endif
|
||||
|
||||
@@ -99,8 +99,6 @@ std::string format_string(const ifcopenshell::attribute_value& argument) {
|
||||
stream << v;
|
||||
return stream.str();
|
||||
break; }
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
@@ -153,7 +151,7 @@ void process_pset(element_properties& props, const T& inst) {
|
||||
|
||||
template <typename Schema>
|
||||
void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefinition& inst) {
|
||||
// Extracts the property definitions for an IFC instance.
|
||||
// Extracts the property definitions for an IFC instance.
|
||||
if (auto tyob = inst.template as<typename Schema::IfcTypeObject>()) {
|
||||
if (tyob.HasPropertySets()) {
|
||||
auto defs = *tyob.HasPropertySets();
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
// to simplify alignment construction
|
||||
|
||||
// Disable warnings coming from IfcOpenShell
|
||||
#if defined(_MSC_VER)
|
||||
#pragma warning(disable : 4018 4267 4250 4984 4985)
|
||||
#endif
|
||||
|
||||
#include "../ifcparse/schemas/Ifc4x3_add2.h"
|
||||
#include "../ifcparse/alignment_helper.h"
|
||||
|
||||
@@ -336,7 +336,7 @@ int main(int argc, char** argv) {
|
||||
std::string exterior_only_algo;
|
||||
|
||||
ifcopenshell::geom::settings settings;
|
||||
|
||||
|
||||
po::options_description geom_options("Geometry options");
|
||||
geom_options.add_options()
|
||||
("kernel", po::value<std::string>(&geometry_kernel)->default_value(default_kernel),
|
||||
@@ -389,7 +389,7 @@ int main(int argc, char** argv) {
|
||||
("model", "Specifies whether to include surfaces and solids in the output result. "
|
||||
"Typically these are representations of type Body or Facetation. ")
|
||||
;
|
||||
|
||||
|
||||
settings.define_options(geom_options);
|
||||
|
||||
std::string bounds;
|
||||
@@ -466,7 +466,7 @@ int main(int argc, char** argv) {
|
||||
num_threads = std::thread::hardware_concurrency();
|
||||
logger.notice("SYS", 7, "Using " + std::to_string(num_threads) + " threads");
|
||||
}
|
||||
|
||||
|
||||
if (vmap.count("log-format") == 1) {
|
||||
boost::to_lower(log_format);
|
||||
if (log_format == "plain") {
|
||||
@@ -479,7 +479,7 @@ int main(int argc, char** argv) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!filter_filename.empty()) {
|
||||
size_t num_filters = read_filters_from_file(ifcopenshell::path::to_utf8(filter_filename), include_filter, include_traverse_filter, exclude_filter, exclude_traverse_filter);
|
||||
if (num_filters) {
|
||||
@@ -524,10 +524,10 @@ int main(int argc, char** argv) {
|
||||
|
||||
// If no output filename is specified a Wavefront OBJ file will be output
|
||||
// to maintain backwards compatibility with the obsolete IfcObj executable.
|
||||
const path_t output_filename = vmap.count("output-file") == 1
|
||||
const path_t output_filename = vmap.count("output-file") == 1
|
||||
? vmap["output-file"].as<path_t>()
|
||||
: change_extension(input_filename, ifcopenshell::path::from_utf8(DEFAULT_EXTENSION));
|
||||
|
||||
|
||||
if (output_filename.size() < 5) {
|
||||
cerr_ << "[error] Invalid or unsupported output file '" << output_filename << "' given" << std::endl;
|
||||
print_usage();
|
||||
@@ -572,13 +572,13 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
path_t output_temp_filename = output_filename + ifcopenshell::path::from_utf8(TEMP_FILE_EXTENSION);
|
||||
|
||||
|
||||
std::vector<path_t> tokens;
|
||||
split(tokens, output_filename, boost::is_any_of("."));
|
||||
std::vector<path_t>::iterator tok_iter;
|
||||
path_t ext = *(tokens.end() - 1);
|
||||
path_t dot;
|
||||
dot = '.';
|
||||
dot = '.';
|
||||
path_t output_extension = dot + ext;
|
||||
|
||||
boost::to_lower(output_extension);
|
||||
@@ -785,7 +785,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
time_t start,end;
|
||||
time(&start);
|
||||
|
||||
|
||||
// @nb last argument true -> bypass_properties which are not read by any of the geometry serializers
|
||||
// Document serializers and IFC are already special-cased above
|
||||
// SVG requires properties for IfcAnnotation/DRAWING properties
|
||||
@@ -839,12 +839,12 @@ int main(int argc, char** argv) {
|
||||
|
||||
settings.get<ifcopenshell::geom::settings::ModelOffset>().value = offset;
|
||||
}
|
||||
|
||||
|
||||
if (is_tesselated && (center_model || center_model_geometry)) {
|
||||
std::vector<double> offset(3);
|
||||
|
||||
ifcopenshell::geom::iterator tmp_context_iterator(ifcopenshell::geom::kernels::construct(ifc_file, geometry_kernel, settings, logger), settings, ifc_file, filter_funcs, num_threads, logger);
|
||||
|
||||
|
||||
time_t bounds_start, bounds_end;
|
||||
time(&bounds_start);
|
||||
if (!quiet) logger.status("Computing bounds...");
|
||||
@@ -860,7 +860,7 @@ int main(int argc, char** argv) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tmp_context_iterator.compute_bounds(center_model_geometry);
|
||||
|
||||
time(&bounds_end);
|
||||
@@ -919,19 +919,19 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
// The functions ifcopenshell::geom::iterator::get() and ifcopenshell::geom::iterator::next()
|
||||
// wrap an iterator of all geometrical products in the Ifc file.
|
||||
// wrap an iterator of all geometrical products in the Ifc file.
|
||||
// ifcopenshell::geom::iterator::get() returns an ifcopenshell::geom::triangulation_element or
|
||||
// -native_element pointer, based on current settings. (see iterator.h
|
||||
// for definition) ifcopenshell::geom::iterator::next() is used to poll whether more
|
||||
// geometrical entities are available. None of these functions throw
|
||||
// exceptions, neither for parsing errors or geometrical errors. Upon
|
||||
// calling next() the entity to be returned has already been processed, a
|
||||
// non-null return value guarantees that a successfully processed product is
|
||||
// available.
|
||||
// geometrical entities are available. None of these functions throw
|
||||
// exceptions, neither for parsing errors or geometrical errors. Upon
|
||||
// calling next() the entity to be returned has already been processed, a
|
||||
// non-null return value guarantees that a successfully processed product is
|
||||
// available.
|
||||
size_t num_created = 0;
|
||||
|
||||
while (true) {
|
||||
|
||||
|
||||
auto geom_object = context_iterator->get();
|
||||
|
||||
if (is_tesselated)
|
||||
@@ -967,7 +967,7 @@ int main(int argc, char** argv) {
|
||||
if (!context_iterator->next()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!no_progress && quiet) {
|
||||
for (; old_progress < 100; ++old_progress) {
|
||||
cout_ << ".";
|
||||
@@ -1086,7 +1086,7 @@ bool init_input_file(const std::string& filename, ifcopenshell::file*& ifc_file,
|
||||
ifc_file->bypass_type("IfcProfileProperties");
|
||||
ifc_file->bypass_type("IfcPhysicalQuantity");
|
||||
}
|
||||
|
||||
|
||||
#ifdef USE_MMAP
|
||||
if (mmap) {
|
||||
ifc_file->initialize(filename, mmap);
|
||||
@@ -1382,20 +1382,20 @@ void fix_quantities(ifcopenshell::file& f, bool no_progress, bool quiet, bool st
|
||||
auto person = latebound_access::create(f, "IfcPerson");
|
||||
latebound_access::set(person, "FamilyName", std::string("IfcOpenShell"));
|
||||
latebound_access::set(person, "GivenName", std::string("IfcOpenShell"));
|
||||
|
||||
|
||||
auto org = latebound_access::create(f, "IfcOrganization");
|
||||
latebound_access::set(org, "Name", std::string("IfcOpenShell"));
|
||||
|
||||
|
||||
auto pando = latebound_access::create(f, "IfcPersonAndOrganization");
|
||||
latebound_access::set(pando, "ThePerson", person);
|
||||
latebound_access::set(pando, "TheOrganization", org);
|
||||
|
||||
|
||||
auto application = latebound_access::create(f, "IfcApplication");
|
||||
latebound_access::set(application, "ApplicationDeveloper", org);
|
||||
latebound_access::set(application, "Version", std::string(IFCOPENSHELL_VERSION));
|
||||
latebound_access::set(application, "ApplicationFullName", std::string("IfcConvert"));
|
||||
latebound_access::set(application, "ApplicationIdentifier", std::string("IfcConvert") + IFCOPENSHELL_VERSION);
|
||||
|
||||
|
||||
auto ownerhist = latebound_access::create(f, "IfcOwnerHistory");
|
||||
latebound_access::set(ownerhist, "OwningUser", pando);
|
||||
latebound_access::set(ownerhist, "OwningApplication", application);
|
||||
@@ -1440,7 +1440,7 @@ void fix_quantities(ifcopenshell::file& f, bool no_progress, bool quiet, bool st
|
||||
latebound_access::set(quantity_area, "AreaValue", a);
|
||||
quantities.push_back(quantity_area);
|
||||
}
|
||||
|
||||
|
||||
if (geom_object->geometry().calculate_volume(a)) {
|
||||
auto quantity_volume = latebound_access::create(f, "IfcQuantityVolume");
|
||||
latebound_access::set(quantity_volume, "Name", std::string("Volume"));
|
||||
@@ -1461,13 +1461,13 @@ void fix_quantities(ifcopenshell::file& f, bool no_progress, bool quiet, bool st
|
||||
|
||||
std::vector<express::base> quantities_2;
|
||||
|
||||
for (auto& part : geom_object->geometry()) {
|
||||
for (auto& part : geom_object->geometry()) {
|
||||
auto quantity_count = latebound_access::create(f, "IfcQuantityCount");
|
||||
latebound_access::set(quantity_count, "Name", std::string("Surface Genus"));
|
||||
latebound_access::set(quantity_count, "Description", '#' + boost::lexical_cast<std::string>(part.ItemId()));
|
||||
latebound_access::set(quantity_count, "CountValue", (int64_t) part.shape()->surface_genus());
|
||||
|
||||
quantities_2.push_back(quantity_count);
|
||||
quantities_2.push_back(quantity_count);
|
||||
}
|
||||
|
||||
latebound_access::set(quantity_complex, "HasQuantities", quantities_2);
|
||||
|
||||
@@ -41,7 +41,7 @@ void fix_storeycontainment(ifcopenshell::file& f, bool no_progress, bool quiet,
|
||||
elem_to_storey[*it] = storey;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
auto storeys = f.instances_by_type("IfcBuildingStorey");
|
||||
std::vector<const ifcopenshell::IfcBaseClass*> storeys_sorted(storeys->begin(), storeys->end());
|
||||
@@ -98,7 +98,7 @@ void fix_storeycontainment(ifcopenshell::file& f, bool no_progress, bool quiet,
|
||||
std::wcout << "---" << std::endl;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
if (!context_iterator.initialize()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ void fix_wallconnectivity(ifcopenshell::file& f, bool no_progress, bool quiet, b
|
||||
settings.get<ifcopenshell::geom::settings::DisableOpeningSubtractions>().value = true;
|
||||
|
||||
settings.get<ifcopenshell::geom::settings::OutputDimensionality>().value = ifcopenshell::geom::settings::CURVES;
|
||||
|
||||
|
||||
ifcopenshell::geom::converter c(ifcopenshell::geom::kernels::construct(&f, "cgal", settings, logger), &f, settings, logger);
|
||||
|
||||
auto rels = f.instances_by_type("IfcRelConnectsPathElements");
|
||||
@@ -54,7 +54,7 @@ void fix_wallconnectivity(ifcopenshell::file& f, bool no_progress, bool quiet, b
|
||||
if (!a_is_relating) {
|
||||
std::swap(a_type, b_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
auto a_poly = ifcopenshell::geom::utils::create_polyhedron(a.handle()->second);
|
||||
@@ -123,7 +123,7 @@ void fix_wallconnectivity(ifcopenshell::file& f, bool no_progress, bool quiet, b
|
||||
} else {
|
||||
auto p0 = boost::get<taxonomy::point3::ptr>(first_vertex);
|
||||
auto p1 = boost::get<taxonomy::point3::ptr>(last_vertex);
|
||||
|
||||
|
||||
auto v0 = taxonomy::cast<taxonomy::geom_item>(item)->matrix->ccomponents() * p0->ccomponents().homogeneous();
|
||||
auto v1 = taxonomy::cast<taxonomy::geom_item>(item)->matrix->ccomponents() * p1->ccomponents().homogeneous();
|
||||
|
||||
@@ -142,7 +142,7 @@ void fix_wallconnectivity(ifcopenshell::file& f, bool no_progress, bool quiet, b
|
||||
|
||||
auto pit = std::minmax_element(parameters.begin(), parameters.end());
|
||||
return std::make_pair(len, std::make_pair(CGAL::to_double(*pit.first), CGAL::to_double(*pit.second)));
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto& nan = std::numeric_limits<double>::quiet_NaN();
|
||||
return std::make_pair(nan, std::make_pair(nan, nan));
|
||||
|
||||
@@ -37,7 +37,7 @@ inline static bool ALMOST_THE_SAME(const T& a, const T& b, double tolerance = AL
|
||||
return fabs(a - b) < tolerance;
|
||||
}
|
||||
|
||||
namespace ifcopenshell {
|
||||
namespace ifcopenshell {
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#pragma warning(push)
|
||||
@@ -70,7 +70,7 @@ namespace ifcopenshell {
|
||||
public:
|
||||
bool propagate_exceptions = false;
|
||||
bool partial_success_is_success = true;
|
||||
|
||||
|
||||
abstract_kernel(const std::string& geometry_library, const ifcopenshell::geom::settings& settings, ifcopenshell::logger& logger = ifcopenshell::logger::root())
|
||||
: geometry_library_(geometry_library)
|
||||
, settings_(settings)
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace geom {
|
||||
/// should return true if the geometry for the product is wanted to be included in the output.
|
||||
/// http://www.boost.org/doc/libs/1_62_0/doc/html/function/tutorial.html
|
||||
typedef std::function<bool(const express::base&)> filter_function;
|
||||
|
||||
|
||||
class IFC_GEOM_API abstract_mapping {
|
||||
protected:
|
||||
ifcopenshell::geom::settings settings_;
|
||||
@@ -106,7 +106,7 @@ namespace geom {
|
||||
|
||||
IFC_GEOM_API mapping_factory_implementation& mapping_implementations();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -510,20 +510,20 @@ namespace ifcopenshell::geom {
|
||||
virtual void triangulate(ifcopenshell::geom::settings settings, const ifcopenshell::geom::taxonomy::matrix4& place, triangulation* t, int item_id, int surface_style_id, ifcopenshell::logger& logger = ifcopenshell::logger::root()) const = 0;
|
||||
ifcopenshell::geom::triangulation* triangulate(const ifcopenshell::geom::settings& settings, ifcopenshell::logger& logger = ifcopenshell::logger::root()) const;
|
||||
virtual void serialize(const ifcopenshell::geom::taxonomy::matrix4& place, std::string&) const = 0;
|
||||
|
||||
|
||||
virtual int surface_genus() const = 0;
|
||||
virtual bool is_manifold() const = 0;
|
||||
|
||||
|
||||
virtual int num_vertices() const = 0;
|
||||
virtual int num_edges() const = 0;
|
||||
virtual int num_faces() const = 0;
|
||||
|
||||
|
||||
// @todo choose one prototype
|
||||
virtual double bounding_box(void*&) const = 0;
|
||||
// @todo this must be something with a virtual dtor so that we can delete it.
|
||||
virtual std::pair<opaque_coordinate<3>, opaque_coordinate<3>> bounding_box() const = 0;
|
||||
virtual void set_box(void* b) = 0;
|
||||
|
||||
|
||||
virtual opaque_number length() = 0;
|
||||
virtual opaque_number area() = 0;
|
||||
virtual opaque_number volume() = 0;
|
||||
@@ -550,11 +550,11 @@ namespace ifcopenshell::geom {
|
||||
virtual std::size_t map(opaque_coordinate<4>& from, opaque_coordinate<4>& to) = 0;
|
||||
virtual std::size_t map(const std::vector<opaque_coordinate<4>>& from, const std::vector<opaque_coordinate<4>>& to) = 0;
|
||||
virtual conversion_result_shape* moved(ifcopenshell::geom::taxonomy::matrix4::ptr) const = 0;
|
||||
|
||||
|
||||
virtual bool surface_area_along_direction(double tol, const ifcopenshell::geom::taxonomy::matrix4::ptr&, double& along_x, double& along_y, double& along_z) const = 0;
|
||||
|
||||
virtual ~conversion_result_shape() {}
|
||||
|
||||
|
||||
};
|
||||
|
||||
class IFC_GEOM_API conversion_result {
|
||||
|
||||
@@ -498,7 +498,7 @@ namespace ifcopenshell {
|
||||
static constexpr const char* const description = "Slight variation of --model-offset where large offsets are applied by negating existing large offsets to retain maximum precision. Requires --no-parallel-mapping.";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
namespace impl {
|
||||
template <typename T>
|
||||
struct readable_name {
|
||||
|
||||
@@ -19,7 +19,7 @@ ifcopenshell::geom::converter::~converter() {
|
||||
|
||||
ifcopenshell::geom::native_element* ifcopenshell::geom::converter::create_brep_for_representation_and_product(taxonomy::ptr representation_node, const express::base product_, const taxonomy::matrix4::ptr& place_) {
|
||||
auto product = product_.as<express::entity>();
|
||||
|
||||
|
||||
std::stringstream representation_id_builder;
|
||||
|
||||
auto place = place_;
|
||||
@@ -32,7 +32,7 @@ ifcopenshell::geom::native_element* ifcopenshell::geom::converter::create_brep_f
|
||||
if (!kernel_->convert(representation_node, shapes)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (settings_.get<ifcopenshell::geom::settings::ApplyLayerSets>().get()) {
|
||||
ifcopenshell::geom::layerset_information layerinfo;
|
||||
std::vector<ifcopenshell::geom::endpoint_connection> neighbours;
|
||||
@@ -55,7 +55,7 @@ ifcopenshell::geom::native_element* ifcopenshell::geom::converter::create_brep_f
|
||||
/*
|
||||
if (util::flatten_shape_list(shapes, merge, false, getValue(GV_PRECISION))) {
|
||||
if (util::count(merge, TopAbs_FACE) > 0) {
|
||||
|
||||
|
||||
if (convert_layerset(product, layers, styles, thickness)) {
|
||||
|
||||
IfcSchema::IfcRelAssociates::list::ptr associations = product->HasAssociations();
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace ifcopenshell { namespace geom {
|
||||
ifcopenshell::geom::kernels::abstract_kernel* kernel() { return &*kernel_; }
|
||||
|
||||
converter(std::unique_ptr<ifcopenshell::geom::kernels::abstract_kernel>&& geometry_library, ifcopenshell::file* file, ifcopenshell::geom::settings& settings, ifcopenshell::logger& logger = ifcopenshell::logger::root());
|
||||
|
||||
|
||||
~converter();
|
||||
|
||||
ifcopenshell::geom::abstract_mapping* mapping() const { return mapping_; }
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace ifcopenshell::geom {
|
||||
}
|
||||
}
|
||||
const ifcopenshell::geom::taxonomy::matrix4::ptr& data() const {
|
||||
if (matrix_orig_units_) {
|
||||
if (matrix_orig_units_) {
|
||||
return matrix_orig_units_;
|
||||
}
|
||||
if (matrix_) {
|
||||
@@ -131,7 +131,7 @@ namespace ifcopenshell::geom {
|
||||
const std::string& guid, const std::string& context, const ifcopenshell::geom::taxonomy::matrix4::ptr& trsf, const express::entity& product)
|
||||
: _id(id), _parent_id(parent_id), _name(name), _type(type), _guid(guid), _context(context), _transformation(settings, trsf)
|
||||
, product_(product)
|
||||
{
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
if (type == "IfcProject") {
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// A purposely empty file so that the unrolled loop
|
||||
// can overflow into an existing empty include file.
|
||||
// can overflow into an existing empty include file.
|
||||
@@ -135,7 +135,7 @@ struct cant_fn_evaluator : public fn_evaluator {
|
||||
auto g = gradient_evaluator_.evaluate(u);
|
||||
auto c = cant_evaluator_.evaluate(u);
|
||||
|
||||
|
||||
|
||||
// curvature is stored in row 3 - capture it and remove it from the xy and uz matrices
|
||||
// so the matrix operations (ie multiplication) works correctly
|
||||
auto gradient_curvature = g.row(3);
|
||||
|
||||
@@ -13,8 +13,8 @@ IFC_GEOM_API std::vector<double> helmert_curve_point(double A0, double A1, doubl
|
||||
/// This is intended to be used from python side. Polylines are mapped to a loop, but when
|
||||
/// representing an alignment they need to be a function_item so the can be evaluated by function_item_evaluator.
|
||||
/// On the C++ side, the dcast operator take care of this, but dcast is not accessible on the python side.
|
||||
/// @param loop
|
||||
/// @return
|
||||
/// @param loop
|
||||
/// @return
|
||||
inline taxonomy::function_item::ptr convert_loop_to_function_item(taxonomy::loop::ptr loop) {
|
||||
return ifcopenshell::geom::taxonomy::dcast<taxonomy::function_item>(loop);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ taxonomy::loft::ptr ifcopenshell::geom::make_loft(const ifcopenshell::geom::sett
|
||||
while (dist_along > *(profile_index + 1)) {
|
||||
profile_index++;
|
||||
if (profile_index == longitudes.end()) {
|
||||
// @todo handle this?
|
||||
// @todo handle this?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ taxonomy::loft::ptr ifcopenshell::geom::make_loft(const ifcopenshell::geom::sett
|
||||
}
|
||||
interpolated->matrix->components() = lerp(m4a, m4b, relative_dist_along);
|
||||
}
|
||||
|
||||
|
||||
auto interpolated_offset = lerp(offset_a, offset_b, relative_dist_along);
|
||||
if (rotation_a == rotation_b && rotation_a) {
|
||||
// @todo we don't support an overridden rotation on only one of the placements
|
||||
@@ -215,7 +215,7 @@ taxonomy::loft::ptr ifcopenshell::geom::make_loft(const ifcopenshell::geom::sett
|
||||
std::vector<taxonomy::point3::ptr> points;
|
||||
std::vector<std::set<std::string>> tags;
|
||||
std::vector<std::string>::const_iterator tag_it;
|
||||
|
||||
|
||||
if (!loop->closed.value_or(false)) {
|
||||
points = {std::get<taxonomy::point3::ptr>(loop->children[0]->start)};
|
||||
if (input_tags) {
|
||||
@@ -352,7 +352,7 @@ taxonomy::loft::ptr ifcopenshell::geom::make_loft(const ifcopenshell::geom::sett
|
||||
for (auto& x : tags_for_this_point_on_subsequent_profile) {
|
||||
points.push_back(taxonomy::make<taxonomy::point3>(p3));
|
||||
common_tags_vec.push_back(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto tmp__ : boost::combine(w1_points, w2_points)) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
namespace ifcopenshell {
|
||||
|
||||
namespace geom {
|
||||
|
||||
|
||||
struct IFC_GEOM_API cross_section {
|
||||
double dist_along;
|
||||
taxonomy::geom_item::ptr section_geometry;
|
||||
|
||||
@@ -624,7 +624,7 @@ std::unique_ptr<ifcopenshell::geom::element> ifcopenshell::geom::iterator::get()
|
||||
hasParent = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add the previously found parent to the vector
|
||||
hasParent = hasParent && parent_object->parent_id() != -1;
|
||||
}
|
||||
@@ -688,13 +688,13 @@ express::base ifcopenshell::geom::iterator::create() {
|
||||
}
|
||||
|
||||
ifcopenshell::geom::taxonomy::direction3::ptr ifcopenshell::geom::iterator::remove_offset_() {
|
||||
|
||||
|
||||
using namespace ifcopenshell::geom::taxonomy;
|
||||
|
||||
|
||||
if (!settings_.get<ifcopenshell::geom::settings::MaxOffset>().has()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
if (!settings_.get<ifcopenshell::geom::settings::NoParallelMapping>().get()) {
|
||||
throw std::runtime_error("remove_offset() can only be called with defer-processing-first-element and no-parallel-mapping settings");
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace ifcopenshell::geom {
|
||||
bool task_result_ptr_initialized = false;
|
||||
bool task_result_ptr_exhausted = false;
|
||||
size_t async_elements_returned_ = 0;
|
||||
|
||||
|
||||
ifcopenshell::geom::settings settings_;
|
||||
ifcopenshell::file* ifc_file;
|
||||
std::vector<ifcopenshell::geom::filter_function> filters_;
|
||||
@@ -138,7 +138,7 @@ namespace ifcopenshell::geom {
|
||||
|
||||
// When single-threaded
|
||||
ifcopenshell::geom::converter* converter_;
|
||||
|
||||
|
||||
// When multi-threaded
|
||||
std::vector<ifcopenshell::geom::converter*> kernel_pool;
|
||||
std::vector<std::unique_ptr<ifcopenshell::logger>> worker_loggers_;
|
||||
|
||||
@@ -296,7 +296,7 @@ ifcopenshell::geom::cgal_shape::cgal_shape(const cgal_polyhedron& shape, bool co
|
||||
};
|
||||
|
||||
std::vector<CGAL::Point_2<kernel_>> ps;
|
||||
|
||||
|
||||
for (auto& he1 : CGAL::halfedges_around_face(face->halfedge(), poly)) {
|
||||
const auto& source = he1->vertex()->point();
|
||||
ps.push_back(transform_point(source));
|
||||
@@ -345,7 +345,7 @@ void ifcopenshell::geom::cgal_shape::to_poly() const {
|
||||
CGAL::Polygon_mesh_processing::orient_to_bound_a_volume(poly);
|
||||
}
|
||||
shape_ = poly;
|
||||
|
||||
|
||||
// nef_->convert_to_polyhedron(*shape_);
|
||||
}
|
||||
}
|
||||
@@ -384,7 +384,7 @@ void ifcopenshell::geom::cgal_shape::triangulate(ifcopenshell::geom::settings se
|
||||
}
|
||||
|
||||
const bool setting_use_original_edges = settings.get<ifcopenshell::geom::settings::CgalEmitOriginalEdges>().get();
|
||||
|
||||
|
||||
std::set<std::set<kernel_::Point_3>> original_edges;
|
||||
if (setting_use_original_edges) {
|
||||
for (auto it = shape_to_use->edges_begin(); it != shape_to_use->edges_end(); ++it) {
|
||||
@@ -457,7 +457,7 @@ void ifcopenshell::geom::cgal_shape::triangulate(ifcopenshell::geom::settings se
|
||||
|
||||
// std::map<cgal_vertex_descriptor, kernel_::Vector_3> vertex_normals;
|
||||
// boost::associative_property_map<std::map<cgal_vertex_descriptor, kernel_::Vector_3>> vertex_normals_map(vertex_normals);
|
||||
|
||||
|
||||
// triangulate the shape and compute the normals
|
||||
std::map<facet_const_handle, kernel_::Vector_3> face_normals;
|
||||
boost::associative_property_map<std::map<facet_const_handle, kernel_::Vector_3>> face_normals_map(face_normals);
|
||||
@@ -556,7 +556,7 @@ void ifcopenshell::geom::cgal_shape::triangulate(ifcopenshell::geom::settings se
|
||||
is_face_boundary[i] = setting_use_original_edges
|
||||
? original_edges.find({ current_halfedge->vertex()->point(), current_halfedge->prev()->vertex()->point() }) != original_edges.end()
|
||||
: facet_to_component[face] != facet_to_component[current_halfedge->opposite()->face()];
|
||||
|
||||
|
||||
++i;
|
||||
++num_vertices;
|
||||
++current_halfedge;
|
||||
@@ -764,7 +764,7 @@ opaque_coordinate<3> ifcopenshell::geom::cgal_shape::position()
|
||||
for (auto it = shp.points_begin(); it != shp.points_end(); ++it) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
p[i] += it->cartesian(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
kernel_::FT N(static_cast<double>(std::distance(shp.points_begin(), shp.points_end())));
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
|
||||
@@ -84,7 +84,7 @@ CGAL::Polyhedron_3<kernel_> ifcopenshell::geom::utils::create_polyhedron(std::li
|
||||
// fresult.close();
|
||||
return CGAL::Polyhedron_3<kernel_>();
|
||||
}
|
||||
|
||||
|
||||
// std::cout << "After: " << polyhedron.size_of_vertices() << " vertices and " << polyhedron.size_of_facets() << " facets" << std::endl;
|
||||
|
||||
return polyhedron;
|
||||
@@ -223,7 +223,7 @@ bool cgal_kernel::convert(const taxonomy::shell::ptr l, cgal_polyhedron& shape)
|
||||
} else {
|
||||
logger().message(ifcopenshell::logger::LOG_ERROR, "Failed to convert face:", f->instance);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// std::cout << "Face in ConnectedFaceSet: " << std::endl;
|
||||
@@ -673,9 +673,9 @@ namespace {
|
||||
|
||||
namespace {
|
||||
void face_to_poly_with_holes(const cgal_face& face, CGAL::Polygon_with_holes_2<kernel_>& pwh, CGAL::Aff_transformation_3<kernel_>& place) {
|
||||
// static
|
||||
// static
|
||||
kernel_::Vector_3 Z(0, 0, 1);
|
||||
// static
|
||||
// static
|
||||
kernel_::Vector_3 X(1, 0, 0);
|
||||
|
||||
auto refz = newell(face.outer);
|
||||
@@ -912,7 +912,7 @@ bool ifcopenshell::geom::kernels::cgal_kernel::convert_openings(const express::b
|
||||
#else
|
||||
CGAL::Nef_nary_union_3<CGAL::Nef_polyhedron_3<kernel_>> second_operand_collector;
|
||||
size_t second_operand_collector_size = 0;
|
||||
|
||||
|
||||
std::list<std::pair<express::base, std::list<cgal_polyhedron>>> operands;
|
||||
|
||||
std::list<express::base> second_operand_instances;
|
||||
@@ -1483,7 +1483,7 @@ bool cgal_kernel::preprocess_boolean_operand(const express::base& log_reference,
|
||||
for (auto& nef : first_operands_nef) {
|
||||
// @todo eliminate this copy (= to remove const)
|
||||
auto nef_copy = nef;
|
||||
auto tree = build_halfspace_tree_decomposed(nef_copy, planes_fixed);
|
||||
auto tree = build_halfspace_tree_decomposed(nef_copy, planes_fixed);
|
||||
}
|
||||
{
|
||||
// @nb we snap internally as well...
|
||||
@@ -1551,7 +1551,7 @@ bool cgal_kernel::preprocess_boolean_operand(const express::base& log_reference,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
{
|
||||
@@ -2026,7 +2026,7 @@ bool cgal_kernel::convert_impl(const taxonomy::boolean_result::ptr br, std::vect
|
||||
if (!convert(face, fs) || fs.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
auto& w = fs.front().outer;
|
||||
CGAL::Polygon_2<kernel_> ps;
|
||||
for (auto& wire_point : w) {
|
||||
@@ -2037,7 +2037,7 @@ bool cgal_kernel::convert_impl(const taxonomy::boolean_result::ptr br, std::vect
|
||||
continue;
|
||||
}
|
||||
|
||||
// static
|
||||
// static
|
||||
auto z = taxonomy::make<taxonomy::direction3>(0, 0, 1);
|
||||
cgal_polyhedron poly;
|
||||
process_extrusion(fs.front(), z, 200, poly);
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace ifcopenshell {
|
||||
class IFC_GEOMLIBRARY_API cgal_kernel : public abstract_kernel {
|
||||
private:
|
||||
#ifndef IFOPSH_SIMPLE_KERNEL
|
||||
enum boolean_operand_preprocess {
|
||||
enum boolean_operand_preprocess {
|
||||
PP_MINKOWSKY_DILATE,
|
||||
PP_SNAP_POINTS_TO_FIRST_OPERAND,
|
||||
PP_SNAP_PLANES_TO_FIRST_OPERAND,
|
||||
|
||||
@@ -95,7 +95,7 @@ std::string dump_facet(typename CGAL::Nef_polyhedron_3<Kernel>::Halffacet_const_
|
||||
|
||||
const auto& p = h->plane();
|
||||
oss << "Facet plane=" << p << std::endl;
|
||||
|
||||
|
||||
auto fc = h->facet_cycles_begin();
|
||||
auto se = shalfedge_const_handle(fc);
|
||||
CGAL_assertion(se != 0);
|
||||
@@ -187,7 +187,7 @@ plane_map<Kernel> snap_halfspaces(const std::list<CGAL::Plane_3<Kernel>>& planes
|
||||
|
||||
fuzzy_sphere fs(query, search_radius, 0.);
|
||||
// std::cout << "q " << query << std::endl;
|
||||
|
||||
|
||||
std::list<point_d> results_pos, results_neg;
|
||||
kdtree.search(std::back_inserter(results_pos), fs);
|
||||
|
||||
@@ -801,7 +801,7 @@ void bfs(graph<Kernel>& g, size_t start_vertex, Fn& fn) {
|
||||
for (boost::tie(ei, ei_end) = boost::out_edges(cur, g); ei != ei_end; ++ei) {
|
||||
auto s = boost::source(*ei, g);
|
||||
auto t = boost::target(*ei, g);
|
||||
|
||||
|
||||
// @todo is this necessary?
|
||||
if (cur == t) {
|
||||
std::swap(s, t);
|
||||
@@ -863,10 +863,10 @@ std::unique_ptr<halfspace_tree<TreeKernel>> build_halfspace_tree(graph<Kernel>&
|
||||
int largest_component_idx = -1;
|
||||
|
||||
int num_components = 0;
|
||||
|
||||
|
||||
// @nb we don't just randomly start from an arbitrary seed, but we sort planes by d / | abc |
|
||||
// for (size_t i = 0; i < boost::num_vertices(sub_graph_0); ++i) {
|
||||
|
||||
|
||||
std::vector<size_t> sorted_verts;
|
||||
for (size_t i = 0; i < boost::num_vertices(sub_graph_0); ++i) {
|
||||
sorted_verts.push_back(i);
|
||||
@@ -1232,7 +1232,7 @@ std::unique_ptr<halfspace_tree<TreeKernel>> build_halfspace_tree_decomposed(cons
|
||||
// directly, so for now we need to isolate the individual volumes.
|
||||
CGAL::Polyhedron_3<Kernel> P;
|
||||
poly.convert_inner_shell_to_polyhedron(ci->shells_begin(), P);
|
||||
CGAL::Nef_polyhedron_3<Kernel> Pnef(P);
|
||||
CGAL::Nef_polyhedron_3<Kernel> Pnef(P);
|
||||
|
||||
for (auto it = Pnef.halffacets_begin(); it != Pnef.halffacets_end(); ++it) {
|
||||
if (it->incident_volume()->mark()) {
|
||||
@@ -1315,7 +1315,7 @@ size_t edge_contract(graph<Kernel>& G) {
|
||||
bool exists = boost::edge(srcid, tt, G).second;
|
||||
if (!exists) {
|
||||
boost::add_edge(srcid, tt, G);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
++n;
|
||||
|
||||
@@ -19,7 +19,7 @@ struct IFC_GEOMLIBRARY_API manifold_part {
|
||||
auto copy = s;
|
||||
copy.CalculateNormals(3);
|
||||
mesh = copy.GetMeshGL64();
|
||||
solid = s;
|
||||
solid = s;
|
||||
}
|
||||
|
||||
manifold_part(const manifold::MeshGL64& s) : mesh(s) {}
|
||||
|
||||
@@ -114,10 +114,10 @@ namespace {
|
||||
mesh_type build() const {
|
||||
mesh_type mesh;
|
||||
mesh.numProp = 3;
|
||||
|
||||
|
||||
std::vector<size_t> vertex_use_count(vertices.size(), 0);
|
||||
std::vector<Eigen::Vector3d> vertex_normals(vertices.size(), Eigen::Vector3d::Zero());
|
||||
|
||||
|
||||
for (size_t i = 0; i < tri_verts.size(); i += 3) {
|
||||
for (size_t j = 0; j < 3; ++j) {
|
||||
vertex_use_count[tri_verts[i + j]]++;
|
||||
@@ -1242,13 +1242,13 @@ namespace {
|
||||
}
|
||||
|
||||
std::optional<part> part_from_halfspace_solid(halfspace_build_state& state, const taxonomy::solid::ptr& solid, const taxonomy::face::ptr& face,const manifold::Box& reference_box, double precision, double dilation) {
|
||||
|
||||
|
||||
auto plane = taxonomy::cast<taxonomy::plane>(face->basis);
|
||||
|
||||
// @todo verify order
|
||||
const auto transform = matrix_or_identity(solid->matrix) * matrix_or_identity(plane->matrix);
|
||||
const auto extrusion_dir = matrix_or_identity(solid->matrix).col(2).head<3>().eval();
|
||||
|
||||
|
||||
Eigen::Vector3d x = transform.col(0).head<3>();
|
||||
Eigen::Vector3d y = transform.col(1).head<3>();
|
||||
Eigen::Vector3d normal = transform.col(2).head<3>();
|
||||
@@ -1281,7 +1281,7 @@ namespace {
|
||||
const auto delta = corner - origin;
|
||||
const auto u = delta.dot(x);
|
||||
const auto v = delta.dot(y);
|
||||
|
||||
|
||||
u_min = std::min(u_min, u);
|
||||
u_max = std::max(u_max, u);
|
||||
v_min = std::min(v_min, v);
|
||||
|
||||
@@ -58,7 +58,7 @@ double ifcopenshell::geom::util::min_edge_length(const TopoDS_Shape & a) {
|
||||
TopExp_Explorer exp(a, TopAbs_EDGE);
|
||||
for (; exp.More(); exp.Next()) {
|
||||
const TopoDS_Edge& e = TopoDS::Edge(exp.Current());
|
||||
|
||||
|
||||
TopoDS_Vertex v0, v1;
|
||||
TopExp::Vertices(e, v0, v1);
|
||||
if (!v0.IsNull() && !v1.IsNull() && v0.IsSame(v1)) {
|
||||
|
||||
@@ -43,8 +43,8 @@ bool is_intersect_ray_box(const struct ray *ray, const struct box *box) {
|
||||
// https://github.com/NVIDIA-Omniverse/PhysX/blob/main/physx/source/geomutils/src/intersection/GuIntersectionRayTriangle.h
|
||||
// With minor modifications to use gp_Vec type.
|
||||
// More reading: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
double& at, double& au, double& av,
|
||||
bool cull, float enlarge) {
|
||||
// Find vectors for two edges sharing vert0
|
||||
@@ -147,7 +147,7 @@ void edgeEdgeDist(gp_Vec& x, gp_Vec& y, // closest points
|
||||
const double Denom = ADotA*BDotB - ADotB*ADotB;
|
||||
|
||||
double t; // We will clamp result so t is on the segment (p, a)
|
||||
if(Denom!=0.0)
|
||||
if(Denom!=0.0)
|
||||
t = ios_clamp((ADotT*BDotB - BDotT*ADotB) / Denom, 0.0, 1.0);
|
||||
else
|
||||
t = 0.0;
|
||||
@@ -268,7 +268,7 @@ double distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<
|
||||
if(Tp[2]>Tp[index]) index = 2;
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
if(index >= 0)
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
@@ -297,7 +297,7 @@ double distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<
|
||||
|
||||
gp_Vec Tn = Tv[0].Crossed(Tv[1]);
|
||||
double Tnl = Tn.Dot(Tn);
|
||||
|
||||
|
||||
if(Tnl>1e-15f)
|
||||
{
|
||||
const std::array<double, 3> Sp = {(q[0] - p[0]).Dot(Tn),
|
||||
@@ -317,7 +317,7 @@ double distanceTriangleTriangleSquared(gp_Vec& cp, gp_Vec& cq, const std::array<
|
||||
}
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
{
|
||||
shown_disjoint = true;
|
||||
|
||||
const gp_Vec& pIndex = p[index];
|
||||
@@ -525,11 +525,11 @@ bool trianglesIntersectCoplanar(const gp_Vec& p1_n, const gp_Vec& a1, const gp_V
|
||||
|
||||
const double third = (1.0 / 3.0);
|
||||
|
||||
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
|
||||
//A bit of the computations done inside the following functions could be shared but it's kept simple since the
|
||||
//difference is not very big and the coplanar case is not expected to be the most common case
|
||||
if (linesIntersect(a1, b1, a2, b2, x, y) || linesIntersect(a1, b1, b2, c2, x, y) || linesIntersect(a1, b1, c2, a2, x, y) ||
|
||||
linesIntersect(b1, c1, a2, b2, x, y) || linesIntersect(b1, c1, b2, c2, x, y) || linesIntersect(b1, c1, c2, a2, x, y) ||
|
||||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
|
||||
linesIntersect(c1, a1, a2, b2, x, y) || linesIntersect(c1, a1, b2, c2, x, y) || linesIntersect(c1, a1, c2, a2, x, y) ||
|
||||
pointInTriangle(a1, b1, c1, third * (a2 + b2 + c2), x, y) || pointInTriangle(a2, b2, c2, third * (a1 + b1 + c1), x, y))
|
||||
return true;
|
||||
|
||||
@@ -557,7 +557,7 @@ bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, co
|
||||
|
||||
if ((p1ToA > 0) == (p1ToB > 0) && (p1ToA > 0) == (p1ToC > 0))
|
||||
return false; //All points of triangle 2 on same side of triangle 1 -> no intersection
|
||||
|
||||
|
||||
gp_Dir p2_n((b2 - a2).Crossed(c2 - a2).Normalized());
|
||||
double p2_d = -a2.Dot(p2_n);
|
||||
// const PxPlane p2(a2, b2, c2);
|
||||
@@ -566,7 +566,7 @@ bool trianglesIntersect(const gp_Vec& a1, const gp_Vec& b1, const gp_Vec& c1, co
|
||||
const double p2ToC = c1.Dot(p2_n) + p2_d;
|
||||
|
||||
if ((p2ToA > 0) == (p2ToB > 0) && (p2ToA > 0) == (p2ToC > 0))
|
||||
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
|
||||
return false; //All points of triangle 1 on same side of triangle 2 -> no intersection
|
||||
|
||||
gp_Vec intersectionDirection = p1_n.Crossed(p2_n);
|
||||
const double l2 = intersectionDirection.SquareMagnitude();
|
||||
|
||||
@@ -20,7 +20,7 @@ struct IFC_GEOMLIBRARY_API box {
|
||||
IFC_GEOMLIBRARY_API bool is_intersect_ray_box(const struct ray *ray, const struct box *box);
|
||||
|
||||
IFC_GEOMLIBRARY_API bool intersectRayTriangle( const gp_Vec& orig, const gp_Vec& dir,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
const gp_Vec& vert0, const gp_Vec& vert1, const gp_Vec& vert2,
|
||||
double& at, double& au, double& av,
|
||||
bool cull, float enlarge=0.0f);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ bool open_cascade_kernel::convert(const taxonomy::extrusion::ptr extrusion, Topo
|
||||
|
||||
if (face.ShapeType() == TopAbs_COMPOUND) {
|
||||
|
||||
// For compounds (most likely the result of a IfcCompositeProfileDef)
|
||||
// For compounds (most likely the result of a IfcCompositeProfileDef)
|
||||
// create a compound solid shape.
|
||||
|
||||
TopExp_Explorer exp(face, TopAbs_FACE);
|
||||
|
||||
@@ -247,7 +247,7 @@ namespace {
|
||||
|
||||
auto crv = get_curve(e->basis);
|
||||
result = Handle(Geom_Surface)(new Geom_SurfaceOfRevolution(
|
||||
crv, ax
|
||||
crv, ax
|
||||
));
|
||||
|
||||
result->Transform(tr);
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace ifcopenshell::geom {
|
||||
std::pair<wire_it, wire_it> inner_wires() const {
|
||||
return { wires_.begin() + 1, wires_.end() };
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ bool open_cascade_kernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape&
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NCollection_List<TopoDS_Shape> faces;
|
||||
TopoDS_Compound comp;
|
||||
BRep_Builder BB;
|
||||
@@ -330,7 +330,7 @@ bool open_cascade_kernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape&
|
||||
std::array<std::vector<std::vector<std::set<std::string>>>::const_iterator, 2> tag_pairs = {
|
||||
all_tags.begin() + std::distance(shps.begin(), it),
|
||||
all_tags.begin() + std::distance(shps.begin(), jt)};
|
||||
|
||||
|
||||
for (size_t i = 0; i < 2; ++i) {
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> ancestors;
|
||||
const auto& wire = wp[i];
|
||||
@@ -357,7 +357,7 @@ bool open_cascade_kernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape&
|
||||
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator edge_it(incidentEdges); edge_it.More(); edge_it.Next()) {
|
||||
const TopoDS_Edge& e = TopoDS::Edge(edge_it.Value());
|
||||
|
||||
|
||||
TopoDS_Vertex ev0, ev1;
|
||||
TopExp::Vertices(e, ev0, ev1);
|
||||
|
||||
@@ -405,11 +405,11 @@ bool open_cascade_kernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape&
|
||||
++d;
|
||||
} else {
|
||||
throw std::runtime_error("Unable to construct surface");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& wp : ws) {
|
||||
BRepTools_WireExplorer a(wp[0]);
|
||||
|
||||
@@ -204,7 +204,7 @@ namespace {
|
||||
BRep_Tool::Pnt(v1).DumpJson(oss);
|
||||
auto osss = oss.str();
|
||||
std::wcout << osss.c_str() << std::endl;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
BRep_Builder B;
|
||||
TopoDS_Wire W;
|
||||
|
||||
@@ -75,7 +75,7 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
|
||||
|
||||
// A 3x3 matrix to rotate the vertex normals
|
||||
std::optional<gp_Mat> rotation_matrix;
|
||||
|
||||
|
||||
if (place.components_) {
|
||||
const auto& m = *place.components_;
|
||||
rotation_matrix.emplace(
|
||||
@@ -84,7 +84,7 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
|
||||
m(2, 0), m(2, 1), m(2, 2)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// When welding vertices, vertex coords will be shared among faces so we need to per-shape set
|
||||
// to keep track of which edges were already emitted.
|
||||
std::set<std::pair<int, int>> emitted_edges;
|
||||
@@ -119,9 +119,9 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
|
||||
for (exp.Init(shape_, TopAbs_FACE); exp.More(); exp.Next(), ++num_faces) {
|
||||
TopoDS_Face face = TopoDS::Face(exp.Current());
|
||||
|
||||
size_t num_bounds = 0;
|
||||
size_t num_bounds = 0;
|
||||
for (TopoDS_Iterator it(face); it.More(); it.Next(), ++num_bounds) {}
|
||||
|
||||
|
||||
const bool is_planar = BRep_Tool::Surface(face) && BRep_Tool::Surface(face)->DynamicType() == STANDARD_TYPE(Geom_Plane);
|
||||
const bool has_inner_bounds = num_bounds > 1;
|
||||
|
||||
@@ -187,11 +187,12 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i <= tri->NbTriangles(); ++i) {
|
||||
const NCollection_Array1<Poly_Triangle>& triangles = tri->Triangles();
|
||||
for (int i = 1; i <= triangles.Length(); ++i) {
|
||||
int n1, n2, n3;
|
||||
if (face.Orientation() == TopAbs_REVERSED)
|
||||
tri->Triangle(i).Get(n3, n2, n1);
|
||||
else tri->Triangle(i).Get(n1, n2, n3);
|
||||
triangles(i).Get(n3, n2, n1);
|
||||
else triangles(i).Get(n1, n2, n3);
|
||||
|
||||
if (dict[n1] == dict[n2] || dict[n2] == dict[n3] || dict[n3] == dict[n1]) {
|
||||
logger.warning("GEO", 185, "Mesher generated a degenerate triangle, ignoring");
|
||||
@@ -313,7 +314,7 @@ void ifcopenshell::geom::open_cascade_shape::triangulate(ifcopenshell::geom::set
|
||||
} else {
|
||||
p = tessellater.Value(i).XYZ();
|
||||
}
|
||||
|
||||
|
||||
auto p_local = p;
|
||||
taxonomy_transform(place.components_, p);
|
||||
|
||||
@@ -581,7 +582,7 @@ conversion_result_shape* ifcopenshell::geom::open_cascade_shape::concat(conversi
|
||||
{
|
||||
TopoDS_Compound compound;
|
||||
BRep_Builder builder;
|
||||
|
||||
|
||||
auto& left = shape_;
|
||||
auto& right = ((ifcopenshell::geom::open_cascade_shape*)other)->shape_;
|
||||
|
||||
@@ -593,7 +594,7 @@ conversion_result_shape* ifcopenshell::geom::open_cascade_shape::concat(conversi
|
||||
builder.MakeCompound(compound);
|
||||
builder.Add(compound, left);
|
||||
}
|
||||
|
||||
|
||||
builder.Add(compound, right);
|
||||
|
||||
return new open_cascade_shape(std::move(compound));
|
||||
@@ -655,13 +656,14 @@ namespace {
|
||||
coords.push_back(tri->Node(i).Transformed(loc).XYZ());
|
||||
}
|
||||
|
||||
for (int i = 1; i <= tri->NbTriangles(); ++i) {
|
||||
const NCollection_Array1<Poly_Triangle>& triangles = tri->Triangles();
|
||||
for (int i = 1; i <= triangles.Length(); ++i) {
|
||||
int n1, n2, n3;
|
||||
|
||||
if (face.Orientation() == TopAbs_REVERSED) {
|
||||
tri->Triangle(i).Get(n3, n2, n1);
|
||||
triangles(i).Get(n3, n2, n1);
|
||||
} else {
|
||||
tri->Triangle(i).Get(n1, n2, n3);
|
||||
triangles(i).Get(n1, n2, n3);
|
||||
}
|
||||
|
||||
const gp_XYZ& pt1 = coords[n1 - 1];
|
||||
|
||||
@@ -84,7 +84,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_openings(const express::ba
|
||||
// opening_trsf = relative;
|
||||
|
||||
std::vector<ifcopenshell::geom::conversion_result> opening_shapes;
|
||||
|
||||
|
||||
// @todo
|
||||
abstract_kernel::convert(op.first, opening_shapes);
|
||||
|
||||
@@ -309,13 +309,13 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
|
||||
// IfcSchema::IfcRelVoidsElement::list::ptr ifcopenshell::geom::Kernel::find_openings(IfcSchema::IfcProduct* product) {
|
||||
// std::vector<IfcSchema::IfcRelVoidsElement*> rs;
|
||||
//
|
||||
//
|
||||
// if (product->declaration().is(IfcSchema::IfcElement::Class()) && !product->declaration().is(IfcSchema::IfcOpeningElement::Class())) {
|
||||
// IfcSchema::IfcElement* element = (IfcSchema::IfcElement*)product;
|
||||
// auto rels = element->HasOpenings();
|
||||
// rs.insert(rs.end(), rels->begin(), rels->end());
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // Is the IfcElement a decomposition of an IfcElement with any IfcOpeningElements?
|
||||
// IfcSchema::IfcObjectDefinition* obdef = product->as<IfcSchema::IfcObjectDefinition>();
|
||||
// for (;;) {
|
||||
@@ -327,10 +327,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// auto rels = element->HasOpenings();
|
||||
// rs.insert(rs.end(), rels->begin(), rels->end());
|
||||
// }
|
||||
//
|
||||
//
|
||||
// obdef = rel_obdef;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // Filter openings in Reference view, solely marked as Reference.
|
||||
// IfcSchema::IfcRelVoidsElement::list::ptr openings(new IfcSchema::IfcRelVoidsElement::list);
|
||||
// std::for_each(rs.begin(), rs.end(), [&openings](IfcSchema::IfcRelVoidsElement* rel) {
|
||||
@@ -341,17 +341,17 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
//
|
||||
// return openings;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// const IfcSchema::IfcMaterial* ifcopenshell::geom::Kernel::get_single_material_association(const IfcSchema::IfcProduct* product) {
|
||||
// IfcSchema::IfcMaterial* single_material = 0;
|
||||
// IfcSchema::IfcRelAssociatesMaterial::list::ptr associated_materials = product->HasAssociations()->as<IfcSchema::IfcRelAssociatesMaterial>();
|
||||
// if (associated_materials->size() == 1) {
|
||||
// IfcSchema::IfcMaterialSelect* associated_material = (*associated_materials->begin())->RelatingMaterial();
|
||||
// single_material = associated_material->as<IfcSchema::IfcMaterial>();
|
||||
//
|
||||
//
|
||||
// // NB: IfcMaterialLayerSets are also considered, regardless of --enable-layerset-slicing. Picking
|
||||
// // the first material (in accordance with other viewers) when layerset-slicing is disabled.
|
||||
// if (!single_material && associated_material->as<IfcSchema::IfcMaterialLayerSetUsage>()) {
|
||||
@@ -366,21 +366,21 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// return single_material;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// ifcopenshell::geom::native_element* ifcopenshell::geom::Kernel::create_brep_for_representation_and_product(
|
||||
// const IteratorSettings& settings, IfcSchema::IfcRepresentation* representation, IfcSchema::IfcProduct* product)
|
||||
// {
|
||||
// std::stringstream representation_id_builder;
|
||||
//
|
||||
//
|
||||
// representation_id_builder << representation->data().id();
|
||||
//
|
||||
//
|
||||
// ifcopenshell::geom::native* shape;
|
||||
// std::vector<ifcopenshell::geom::conversion_result> shapes, shapes2;
|
||||
//
|
||||
//
|
||||
// if (!convert_shapes(representation, shapes)) {
|
||||
// return 0;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (settings.get(IteratorSettings::APPLY_LAYERSETS)) {
|
||||
// TopoDS_Shape merge;
|
||||
// if (util::flatten_shape_list(shapes, merge, false, getValue(GV_PRECISION))) {
|
||||
@@ -390,7 +390,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// std::vector< std::vector<Handle_Geom_Surface> > folded_layers;
|
||||
// std::vector<std::shared_ptr<const SurfaceStyle>> styles;
|
||||
// if (convert_layerset(product, layers, styles, thickness)) {
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcRelAssociates::list::ptr associations = product->HasAssociations();
|
||||
// for (IfcSchema::IfcRelAssociates::list::it it = associations->begin(); it != associations->end(); ++it) {
|
||||
// IfcSchema::IfcRelAssociatesMaterial* associates_material = (**it).as<IfcSchema::IfcRelAssociatesMaterial>();
|
||||
@@ -400,7 +400,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (styles.size() > 1) {
|
||||
// // If there's only a single layer there is no need to manipulate geometries.
|
||||
// bool success = true;
|
||||
@@ -415,7 +415,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// success = true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (!success) {
|
||||
// ifcopenshell::logger::root().error("Failed processing layerset");
|
||||
// }
|
||||
@@ -424,9 +424,9 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// bool material_style_applied = false;
|
||||
//
|
||||
//
|
||||
// const IfcSchema::IfcMaterial* single_material = get_single_material_association(product);
|
||||
// if (single_material) {
|
||||
// auto s = get_style(single_material);
|
||||
@@ -448,11 +448,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// ifcopenshell::logger::root().warning("No material and surface styles for:", product);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (material_style_applied) {
|
||||
// representation_id_builder << "-material-" << single_material->data().id();
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (settings.force_space_transparency() >= 0. && product->declaration().is("IfcSpace")) {
|
||||
// for (auto& s : shapes) {
|
||||
// if (s.hasStyle()) {
|
||||
@@ -464,7 +464,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// int parent_id = -1;
|
||||
// try {
|
||||
// express::entity* parent_object = get_decomposing_entity(product);
|
||||
@@ -474,10 +474,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// } catch (const std::exception& e) {
|
||||
// ifcopenshell::logger::root().error(e);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// const std::string name = product->Name().value_or("");
|
||||
// const std::string guid = product->GlobalId();
|
||||
//
|
||||
//
|
||||
// gp_Trsf trsf;
|
||||
// try {
|
||||
// if (product->ObjectPlacement()) {
|
||||
@@ -488,20 +488,20 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// } catch (...) {
|
||||
// ifcopenshell::logger::root().error("Failed to construct placement");
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // Does the IfcElement have any IfcOpenings?
|
||||
// // Note that openings for IfcOpeningElements are not processed
|
||||
// IfcSchema::IfcRelVoidsElement::list::ptr openings = find_openings(product);
|
||||
//
|
||||
//
|
||||
// const std::string product_type = product->declaration().name();
|
||||
// ElementSettings element_settings(settings, getValue(GV_LENGTH_UNIT), product_type);
|
||||
//
|
||||
//
|
||||
// if (!settings.get(ifcopenshell::geom::IteratorSettings::DISABLE_OPENING_SUBTRACTIONS) && openings && openings->size()) {
|
||||
// representation_id_builder << "-openings";
|
||||
// for (IfcSchema::IfcRelVoidsElement::list::it it = openings->begin(); it != openings->end(); ++it) {
|
||||
// representation_id_builder << "-" << (*it)->data().id();
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::vector<ifcopenshell::geom::conversion_result> opened_shapes;
|
||||
// bool caught_error = false;
|
||||
// try {
|
||||
@@ -512,11 +512,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// } catch (...) {
|
||||
// ifcopenshell::logger::root().message(ifcopenshell::logger::LOG_ERROR, "error processing openings for:", product);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (caught_error && opened_shapes.size() < shapes.size()) {
|
||||
// opened_shapes = shapes;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (settings.get(IteratorSettings::USE_WORLD_COORDS)) {
|
||||
// for (std::vector<ifcopenshell::geom::conversion_result>::iterator it = opened_shapes.begin(); it != opened_shapes.end(); ++it) {
|
||||
// it->prepend(trsf);
|
||||
@@ -535,14 +535,14 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// } else {
|
||||
// shape = new ifcopenshell::geom::native(element_settings, representation_id_builder.str(), shapes);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::string context_string = "";
|
||||
// if (representation->RepresentationIdentifier()) {
|
||||
// context_string = *representation->RepresentationIdentifier();
|
||||
// } else if (representation->ContextOfItems()->ContextType()) {
|
||||
// context_string = *representation->ContextOfItems()->ContextType();
|
||||
// }
|
||||
//
|
||||
//
|
||||
// auto elem = new native_element(
|
||||
// product->data().id(),
|
||||
// parent_id,
|
||||
@@ -554,7 +554,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// std::shared_ptr<ifcopenshell::geom::native>(shape),
|
||||
// product
|
||||
// );
|
||||
//
|
||||
//
|
||||
// if (settings.get(IteratorSettings::VALIDATE_QUANTITIES)) {
|
||||
// auto rels = product->IsDefinedBy();
|
||||
// for (auto& rel : *rels) {
|
||||
@@ -623,10 +623,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// return elem;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcRepresentation* ifcopenshell::geom::Kernel::representation_mapped_to(const IfcSchema::IfcRepresentation* representation) {
|
||||
// IfcSchema::IfcRepresentation* representation_mapped_to = 0;
|
||||
// try {
|
||||
@@ -651,36 +651,36 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// return representation_mapped_to;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcProduct::list::ptr ifcopenshell::geom::Kernel::products_represented_by(const IfcSchema::IfcRepresentation* representation) {
|
||||
// IfcSchema::IfcProduct::list::ptr products(new IfcSchema::IfcProduct::list);
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcProductRepresentation::list::ptr prodreps = representation->OfProductRepresentation();
|
||||
//
|
||||
//
|
||||
// for (IfcSchema::IfcProductRepresentation::list::it it = prodreps->begin(); it != prodreps->end(); ++it) {
|
||||
// // http://buildingsmart-tech.org/ifc/IFC2x3/TC1/html/ifcrepresentationresource/lexical/ifcproductrepresentation.htm
|
||||
// // IFC2x Edition 3 NOTE Users should not instantiate the entity IfcProductRepresentation from IFC2x Edition 3 onwards.
|
||||
// // It will be changed into an ABSTRACT supertype in future releases of IFC.
|
||||
//
|
||||
//
|
||||
// // IfcProductRepresentation also lacks the INVERSE relation to IfcProduct
|
||||
// // Let's find the IfcProducts that reference the IfcProductRepresentation anyway
|
||||
// products->push((*it)->data().get_inverse((&IfcSchema::IfcProduct::Class()), -1)->as<IfcSchema::IfcProduct>());
|
||||
// }
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcRepresentationMap::list::ptr maps = representation->RepresentationMap();
|
||||
//
|
||||
//
|
||||
// if (products->size() && maps->size()) {
|
||||
// ifcopenshell::logger::root().warning("Representation used by IfcRepresentationMap and IfcProductDefinitionShape", representation);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (prodreps->size() > 1) {
|
||||
// ifcopenshell::logger::root().warning("Multiple IfcProductDefinitionShapes for representation", representation);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (maps->size() > 1) {
|
||||
// ifcopenshell::logger::root().warning("Multiple IfcRepresentationMaps for representation", representation);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (maps->size() == 1) {
|
||||
// IfcSchema::IfcRepresentationMap* map = *maps->begin();
|
||||
// if (is_identity_transform(map->MappingOrigin())) {
|
||||
@@ -688,11 +688,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// for (IfcSchema::IfcMappedItem::list::it it = items->begin(); it != items->end(); ++it) {
|
||||
// IfcSchema::IfcMappedptr item = *it;
|
||||
// if (item->StyledByItem()->size() != 0) continue;
|
||||
//
|
||||
//
|
||||
// if (!is_identity_transform(item->MappingTarget())) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcRepresentation::list::ptr reps = item->data().get_inverse((&IfcSchema::IfcRepresentation::Class()), -1)->as<IfcSchema::IfcRepresentation>();
|
||||
// for (IfcSchema::IfcRepresentation::list::it jt = reps->begin(); jt != reps->end(); ++jt) {
|
||||
// IfcSchema::IfcRepresentation* rep = *jt;
|
||||
@@ -706,10 +706,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// return products;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// ifcopenshell::geom::native_element* ifcopenshell::geom::Kernel::create_brep_for_processed_representation(
|
||||
// const IteratorSettings& /*settings*/, IfcSchema::IfcRepresentation* representation, IfcSchema::IfcProduct* product,
|
||||
// ifcopenshell::geom::native_element* brep)
|
||||
@@ -723,10 +723,10 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// } catch (const std::exception& e) {
|
||||
// ifcopenshell::logger::root().error(e);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// const std::string name = product->Name().value_or("");
|
||||
// const std::string guid = product->GlobalId();
|
||||
//
|
||||
//
|
||||
// gp_Trsf trsf;
|
||||
// try {
|
||||
// if (product->ObjectPlacement()) {
|
||||
@@ -737,16 +737,16 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// } catch (...) {
|
||||
// ifcopenshell::logger::root().error("Failed to construct placement");
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::string context_string = "";
|
||||
// if (representation->RepresentationIdentifier()) {
|
||||
// context_string = *representation->RepresentationIdentifier();
|
||||
// } else if (representation->ContextOfItems()->ContextType()) {
|
||||
// context_string = *representation->ContextOfItems()->ContextType();
|
||||
// }
|
||||
//
|
||||
//
|
||||
// const std::string product_type = product->declaration().name();
|
||||
//
|
||||
//
|
||||
// return new native_element(
|
||||
// product->data().id(),
|
||||
// parent_id,
|
||||
@@ -759,24 +759,24 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// product
|
||||
// );
|
||||
// }
|
||||
//
|
||||
//
|
||||
// bool ifcopenshell::geom::Kernel::convert_layerset(const IfcSchema::IfcProduct* product, std::vector<Handle_Geom_Surface>& surfaces, std::vector<std::shared_ptr<const SurfaceStyle>>& styles, std::vector<double>& thicknesses) {
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
//
|
||||
// bool ifcopenshell::geom::Kernel::find_wall_end_points(const IfcSchema::IfcWall* wall, gp_Pnt& start, gp_Pnt& end) {
|
||||
// IfcSchema::IfcRepresentation* axis_representation = find_representation(wall, "Axis");
|
||||
// if (!axis_representation) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::vector<conversion_result> items;
|
||||
// {
|
||||
// Kernel temp = *this;
|
||||
// temp.setValue(GV_DIMENSIONALITY, -1.);
|
||||
// temp.convert_shapes(axis_representation, items);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// TopoDS_Vertex a, b;
|
||||
// for (std::vector<conversion_result>::const_iterator it = items.begin(); it != items.end(); ++it) {
|
||||
// TopExp_Explorer exp(it->shape(), TopAbs_VERTEX);
|
||||
@@ -787,36 +787,36 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (a.IsNull() || b.IsNull()) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// start = BRep_Tool::Pnt(a);
|
||||
// end = BRep_Tool::Pnt(b);
|
||||
//
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// bool ifcopenshell::geom::Kernel::fold_layers(const IfcSchema::IfcWall* wall, const std::vector<conversion_result>& items, const std::vector<Handle_Geom_Surface>& surfaces, const std::vector<double>& thicknesses, std::vector< std::vector<Handle_Geom_Surface> >& result) {
|
||||
// /*
|
||||
// * @todo isn't it easier to do this based on the non-folded surfaces of
|
||||
// * the connected walls and fold both pairs of layersets simultaneously?
|
||||
// */
|
||||
//
|
||||
//
|
||||
// bool folds_made = false;
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcRelConnectsPathElements::list::ptr connections(new IfcSchema::IfcRelConnectsPathElements::list);
|
||||
// connections->push(wall->ConnectedFrom()->as<IfcSchema::IfcRelConnectsPathElements>());
|
||||
// connections->push(wall->ConnectedTo()->as<IfcSchema::IfcRelConnectsPathElements>());
|
||||
//
|
||||
//
|
||||
// typedef std::vector<Handle_Geom_Surface> surfaces_t;
|
||||
// typedef std::pair<Handle_Geom_Surface, Handle_Geom_Curve> curve_on_surface;
|
||||
// typedef std::vector<curve_on_surface> curves_on_surfaces_t;
|
||||
// typedef std::vector< std::pair< std::pair<IfcSchema::IfcConnectionTypeEnum::Value, IfcSchema::IfcConnectionTypeEnum::Value>, const IfcSchema::IfcProduct*> > endpoint_connections_t;
|
||||
// typedef std::vector< std::vector<Handle_Geom_Surface> > result_t;
|
||||
// endpoint_connections_t endpoint_connections;
|
||||
//
|
||||
//
|
||||
// // Find the semantic connections to other wall elements when they are not connected 'AT_PATH' because
|
||||
// // in that latter case no folds need to be made.
|
||||
// for (IfcSchema::IfcRelConnectsPathElements::list::it it = connections->begin(); it != connections->end(); ++it) {
|
||||
@@ -838,18 +838,18 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (endpoint_connections.size() == 0) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // Count how many connections are made AT_START and AT_END respectively
|
||||
// int connection_type_count[2] = { 0,0 };
|
||||
// for (endpoint_connections_t::const_iterator it = endpoint_connections.begin(); it != endpoint_connections.end(); ++it) {
|
||||
// const int idx = it->first.first == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART;
|
||||
// connection_type_count[idx] ++;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// gp_Trsf local;
|
||||
// if (wall->ObjectPlacement()) {
|
||||
// if (!convert(wall->ObjectPlacement(), local)) {
|
||||
@@ -857,7 +857,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// local.Invert();
|
||||
//
|
||||
//
|
||||
// {
|
||||
// // Copy the unfolded surfaces
|
||||
// result.resize(surfaces.size());
|
||||
@@ -867,25 +867,25 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// result_it->push_back(*input_it);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// const double total_thickness = std::accumulate(thicknesses.begin(), thicknesses.end(), 0.);
|
||||
//
|
||||
//
|
||||
// gp_Pnt own_axis_start, own_axis_end;
|
||||
// find_wall_end_points(wall, own_axis_start, own_axis_end);
|
||||
//
|
||||
//
|
||||
// // Sometimes duplicate IfcRelConnectsPathElements exist. These are detected
|
||||
// // and the counts of connections are decremented accordingly.
|
||||
// for (int idx = 0; idx < 2; ++idx) {
|
||||
// if (connection_type_count[idx] <= 1) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// /*
|
||||
// IfcSchema::IfcConnectionTypeEnum::Value connection_type = idx == 1
|
||||
// ? IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART
|
||||
// : IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATEND;
|
||||
// */
|
||||
//
|
||||
//
|
||||
// std::set<const IfcSchema::IfcProduct*> others;
|
||||
// endpoint_connections_t::iterator it = endpoint_connections.begin();
|
||||
// while (it != endpoint_connections.end()) {
|
||||
@@ -899,38 +899,38 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // Check whether the end points are of the wall are really ~1 LayerThickness away from each other
|
||||
// /*
|
||||
// for (endpoint_connections_t::const_iterator it = endpoint_connections.begin(); it != endpoint_connections.end(); ++it) {
|
||||
// IfcSchema::IfcConnectionTypeEnum::Value own_type = it->first.first;
|
||||
// IfcSchema::IfcConnectionTypeEnum::Value other_type = it->first.second;
|
||||
//
|
||||
//
|
||||
// gp_Pnt other_axis_start, other_axis_end;
|
||||
// find_wall_end_points(it->second->as<IfcSchema::IfcWall>(), other_axis_start, other_axis_end);
|
||||
//
|
||||
//
|
||||
// gp_Trsf other;
|
||||
// if (!convert(it->second->ObjectPlacement(), other)) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// other.Transforms(other_axis_start.ChangeCoord());
|
||||
// local.Transforms(other_axis_start.ChangeCoord());
|
||||
// other.Transforms(other_axis_end.ChangeCoord());
|
||||
// local.Transforms(other_axis_end.ChangeCoord());
|
||||
//
|
||||
//
|
||||
// const gp_Pnt& a = own_type == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART
|
||||
// ? own_axis_start
|
||||
// : own_axis_end;
|
||||
//
|
||||
//
|
||||
// const gp_Pnt& b = other_type == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART
|
||||
// ? other_axis_start
|
||||
// : other_axis_end;
|
||||
//
|
||||
//
|
||||
// const double d = a.Distance(b);
|
||||
// }
|
||||
// */
|
||||
//
|
||||
//
|
||||
// const double length_required = endpoint_connections.size() * total_thickness;
|
||||
// // @todo this is not precisely the distance in case of curved walls. Also, it's safer
|
||||
// // to first reproject the body onto the axis to get the precise curve parametrization
|
||||
@@ -940,20 +940,20 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// ifcopenshell::logger::root().warning("The wall axis is not long enough to accommodate the fold points");
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// for (endpoint_connections_t::const_iterator it = endpoint_connections.begin(); it != endpoint_connections.end(); ++it) {
|
||||
// IfcSchema::IfcConnectionTypeEnum::Value connection_type = it->first.first;
|
||||
//
|
||||
//
|
||||
// // If more than one wall connects to this start/end -point assume layers do not need to be folded
|
||||
// const int idx = connection_type == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATSTART;
|
||||
// if (connection_type_count[idx] > 1) continue;
|
||||
//
|
||||
//
|
||||
// // Pick the corresponding point from the axis
|
||||
// const gp_Pnt& own_end_point = connection_type == IfcSchema::IfcConnectionTypeEnum::IfcConnectionType_ATEND
|
||||
// ? own_axis_end
|
||||
// : own_axis_start;
|
||||
// const IfcSchema::IfcProduct* other_wall = it->second;
|
||||
//
|
||||
//
|
||||
// gp_Trsf other;
|
||||
// if (other_wall->ObjectPlacement()) {
|
||||
// if (!convert(other_wall->ObjectPlacement(), other)) {
|
||||
@@ -961,32 +961,32 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcRepresentation* axis_representation = find_representation(other_wall, "Axis");
|
||||
//
|
||||
//
|
||||
// if (!axis_representation) {
|
||||
// ifcopenshell::logger::root().warning("Joined wall has no axis representation", other_wall);
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::vector<conversion_result> axis_items;
|
||||
// {
|
||||
// Kernel temp = *this;
|
||||
// temp.setValue(GV_DIMENSIONALITY, -1.);
|
||||
// temp.convert_shapes(axis_representation, axis_items);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// TopoDS_Shape axis_shape;
|
||||
// util::flatten_shape_list(axis_items, axis_shape, false, getValue(GV_PRECISION));
|
||||
//
|
||||
//
|
||||
// // local and other are IfcLocalPlacements and therefore have a unit
|
||||
// // scale factor that can be applied by means of TopoDS_Shape::Move()
|
||||
// axis_shape.Move(other);
|
||||
// axis_shape.Move(local);
|
||||
//
|
||||
//
|
||||
// TopoDS_Shape body_shape;
|
||||
// util::flatten_shape_list(items, body_shape, false, getValue(GV_PRECISION));
|
||||
//
|
||||
//
|
||||
// // Create a single paremetric range over a single curve
|
||||
// // that represents the entire 1d domain of the other wall
|
||||
// // Sometimes there are multiple edges in the Axis shape
|
||||
@@ -998,19 +998,19 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// if (!exp.More()) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// TopoDS_Edge axis_edge = TopoDS::Edge(exp.Current());
|
||||
// other_axis_curve = BRep_Tool::Curve(axis_edge, axis_u1, axis_u2);
|
||||
//
|
||||
//
|
||||
// gp_Pnt other_a_1, other_a_2;
|
||||
// other_axis_curve->D0(axis_u1, other_a_1);
|
||||
// other_axis_curve->D0(axis_u2, other_a_2);
|
||||
//
|
||||
//
|
||||
// if (axis_u2 < axis_u1) {
|
||||
// std::swap(axis_u1, axis_u2);
|
||||
// }
|
||||
// exp.Next();
|
||||
//
|
||||
//
|
||||
// for (; exp.More(); exp.Next()) {
|
||||
// TopoDS_Edge axis_edge2 = TopoDS::Edge(exp.Current());
|
||||
// TopExp_Explorer exp2(axis_edge2, TopAbs_VERTEX);
|
||||
@@ -1025,22 +1025,22 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// double layer_offset = 0;
|
||||
//
|
||||
//
|
||||
// std::vector<double>::const_iterator thickness = thicknesses.begin();
|
||||
// result_t::iterator result_vector = result.begin() + 1;
|
||||
//
|
||||
//
|
||||
// // nb The first layer is never folded, because it corresponds
|
||||
// // to one of the longitudinal faces of the wall. Hence the +1
|
||||
// for (surfaces_t::const_iterator jt = surfaces.begin() + 1; jt != surfaces.end() - 1; ++jt, ++result_vector) {
|
||||
// layer_offset += *thickness++;
|
||||
//
|
||||
//
|
||||
// bool found_intersection = false, parallel = false;
|
||||
// std::optional<gp_Pnt> point_outside_param_range;
|
||||
//
|
||||
//
|
||||
// const Handle_Geom_Surface& surface = *jt;
|
||||
//
|
||||
//
|
||||
// // Find the intersection point between the layerset surface
|
||||
// // and the other axis curve. If it's within the parametric
|
||||
// // range of the other wall it means the walls are connected
|
||||
@@ -1048,16 +1048,16 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// GeomAPI_IntCS intersections(other_axis_curve, surface);
|
||||
// if (intersections.IsDone() && intersections.NbPoints() == 1) {
|
||||
// const gp_Pnt& p = intersections.Point(1);
|
||||
//
|
||||
//
|
||||
// double u, v, w;
|
||||
// intersections.Parameters(1, u, v, w);
|
||||
//
|
||||
//
|
||||
// gp_Pnt Pc, Ps;
|
||||
// gp_Vec Vc, Vs1, Vs2;
|
||||
// other_axis_curve->D1(w, Pc, Vc);
|
||||
// surface->D1(u, v, Ps, Vs1, Vs2);
|
||||
// Vs1.Cross(Vs2);
|
||||
//
|
||||
//
|
||||
// if (Vs1.IsNormal(Vc, 1.e-5)) {
|
||||
// ifcopenshell::logger::root().warning("Connected walls are parallel");
|
||||
// parallel = true;
|
||||
@@ -1069,9 +1069,9 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (!parallel && !found_intersection && point_outside_param_range) {
|
||||
//
|
||||
//
|
||||
// /*
|
||||
// Is there a bug in Open Cascade related to the intersection
|
||||
// of offset surfaces constructed from linear extrusions?
|
||||
@@ -1083,13 +1083,13 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// Handle_Geom_Surface yz2 = new Geom_OffsetSurface(yz, 1.);
|
||||
// intersect(xy, yz2);
|
||||
// */
|
||||
//
|
||||
//
|
||||
// Handle_Geom_Surface plane = new Geom_Plane(*point_outside_param_range, gp::DZ());
|
||||
//
|
||||
//
|
||||
// // vertical edges at wall end point face.
|
||||
// curves_on_surfaces_t layer_ends;
|
||||
// util::intersect(surface, body_shape, layer_ends);
|
||||
//
|
||||
//
|
||||
// Handle_Geom_Curve layer_body_intersection;
|
||||
// Handle_Geom_Surface body_surface;
|
||||
// double mind = std::numeric_limits<double>::infinity();
|
||||
@@ -1111,9 +1111,9 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// if (d < total_thickness * 3 && d < mind) {
|
||||
// GeomAdaptor_Curve GAC(other_axis_curve);
|
||||
// GeomAdaptor_Surface GAS(kt->first);
|
||||
//
|
||||
//
|
||||
// Extrema_ExtCS x(GAC, GAS, getValue(GV_PRECISION), getValue(GV_PRECISION));
|
||||
//
|
||||
//
|
||||
// if (x.IsParallel()) {
|
||||
// body_surface = kt->first;
|
||||
// layer_body_intersection = kt->second;
|
||||
@@ -1122,16 +1122,16 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (body_surface.IsNull()) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // Intersect vertical edge with ground plane for point.
|
||||
// GeomAPI_IntCS intersection2(layer_body_intersection, plane);
|
||||
// if (intersection2.IsDone() && intersection2.NbPoints() == 1) {
|
||||
// const gp_Pnt& layer_end_point = intersection2.Point(1);
|
||||
//
|
||||
//
|
||||
// // Intersect layerset surface with ground plane
|
||||
// GeomAPI_IntSS intersection3(surface, plane, 1.e-7);
|
||||
// if (intersection3.IsDone() && intersection3.NbLines() == 1) {
|
||||
@@ -1140,14 +1140,14 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// ShapeAnalysis_Curve sac;
|
||||
// gp_Pnt layer_end_point_projected; double layer_end_point_param;
|
||||
// sac.Project(layer_line, layer_end_point, 1e-3, layer_end_point_projected, layer_end_point_param, false);
|
||||
//
|
||||
//
|
||||
// // Move point inwards by distance from other layerset
|
||||
// GCPnts_AbscissaPoint dst(layer_line_adaptor, layer_offset, layer_end_point_param);
|
||||
// if (dst.IsDone()) {
|
||||
// // Convert parameter to point
|
||||
// gp_Pnt layer_fold_point;
|
||||
// layer_line->D0(dst.Parameter(), layer_fold_point);
|
||||
//
|
||||
//
|
||||
// GeomAPI_IntSS intersection4(body_surface, plane, 1.e-7);
|
||||
// if (intersection4.IsDone() && intersection4.NbLines() == 1) {
|
||||
// Handle_Geom_Curve body_trim_curve = intersection4.Line(1);
|
||||
@@ -1155,7 +1155,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// gp_Pnt layer_fold_point_projected; double layer_fold_point_param;
|
||||
// sac2.Project(body_trim_curve, layer_fold_point, 1.e-7, layer_fold_point_projected, layer_fold_point_param, false);
|
||||
// Handle_Geom_Curve fold_curve = new Geom_OffsetCurve(body_trim_curve->Reversed(), layer_fold_point_projected.Distance(layer_fold_point), gp::DZ());
|
||||
//
|
||||
//
|
||||
// Handle_Geom_Surface fold_surface = new Geom_SurfaceOfLinearExtrusion(fold_curve, gp::DZ());
|
||||
// result_vector->push_back(fold_surface);
|
||||
// folds_made = true;
|
||||
@@ -1163,15 +1163,15 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// return folds_made;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcRepresentation* ifcopenshell::geom::Kernel::find_representation(const IfcSchema::IfcProduct* product, const std::string& identifier) {
|
||||
// if (!product->Representation()) return 0;
|
||||
// IfcSchema::IfcProductRepresentation* prod_rep = product->Representation();
|
||||
@@ -1183,12 +1183,12 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// return 0;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// const IfcSchema::IfcRepresentationptr ifcopenshell::geom::Kernel::find_item_carrying_style(const IfcSchema::IfcRepresentationptr item) {
|
||||
// if (item->StyledByItem()->size()) {
|
||||
// return item;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// while (item->declaration().is(IfcSchema::IfcBooleanResult::Class())) {
|
||||
// // All instantiations of IfcBooleanOperand (type of FirstOperand) are subtypes of
|
||||
// // IfcGeometricRepresentationItem
|
||||
@@ -1197,24 +1197,24 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// return item;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // TODO: Ideally this would be done for other entities (such as IfcCsgSolid) as well.
|
||||
// // But neither are these very prevalent, nor does the current IfcOpenShell style
|
||||
// // mechanism enable to conveniently style subshapes, which would be necessary for
|
||||
// // distinctly styled union operands.
|
||||
//
|
||||
//
|
||||
// return item;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// bool ifcopenshell::geom::Kernel::is_identity_transform(ifcopenshell::IfcBaseInterface* l) {
|
||||
// IfcSchema::IfcAxis2Placement2D* ax2d;
|
||||
// IfcSchema::IfcAxis2Placement3D* ax3d;
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcCartesianTransformationOperator2D* op2d;
|
||||
// IfcSchema::IfcCartesianTransformationOperator3D* op3d;
|
||||
// IfcSchema::IfcCartesianTransformationOperator2DnonUniform* op2dnonu;
|
||||
// IfcSchema::IfcCartesianTransformationOperator3DnonUniform* op3dnonu;
|
||||
//
|
||||
//
|
||||
// if ((op2dnonu = l->as<IfcSchema::IfcCartesianTransformationOperator2DnonUniform>()) != 0) {
|
||||
// gp_GTrsf2d gtrsf2d;
|
||||
// convert(op2dnonu, gtrsf2d);
|
||||
@@ -1243,18 +1243,18 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// throw ifcopenshell::exception("Invalid valuation for IfcAxis2Placement / IfcCartesianTransformationOperator");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// void ifcopenshell::geom::Kernel::set_conversion_placement_rel_to_type(const ifcopenshell::declaration* type) {
|
||||
// placement_rel_to_type_ = type;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// void ifcopenshell::geom::Kernel::set_conversion_placement_rel_to_instance(const express::entity* instance) {
|
||||
// placement_rel_to_instance_ = instance;
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// namespace {
|
||||
//
|
||||
//
|
||||
// bool process_colour(IfcSchema::IfcColourRgb* colour, double* rgb) {
|
||||
// if (colour != 0) {
|
||||
// rgb[0] = colour->Red();
|
||||
@@ -1263,7 +1263,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// return colour != 0;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// bool process_colour(IfcSchema::IfcNormalisedRatioMeasure* factor, double* rgb) {
|
||||
// if (factor != 0) {
|
||||
// const double f = *factor;
|
||||
@@ -1271,7 +1271,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// return factor != 0;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// bool process_colour(IfcSchema::IfcColourOrFactor* colour_or_factor, double* rgb) {
|
||||
// if (colour_or_factor == 0) {
|
||||
// return false;
|
||||
@@ -1283,11 +1283,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
//
|
||||
// #define Kernel POSTFIX_SCHEMA(Kernel)
|
||||
//
|
||||
//
|
||||
// std::shared_ptr<const ifcopenshell::geom::SurfaceStyle> ifcopenshell::geom::Kernel::internalize_surface_style(const std::pair<express::base, express::base>& shading_styles) {
|
||||
// if (shading_styles.second == 0) {
|
||||
// return 0;
|
||||
@@ -1297,22 +1297,22 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// if (it != style_cache.end()) {
|
||||
// return it->second;
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// IfcSchema::IfcSurfaceStyle* style = shading_styles.first->as<IfcSchema::IfcSurfaceStyle>();
|
||||
// IfcSchema::IfcSurfaceStyleShading* shading = shading_styles.second->as<IfcSchema::IfcSurfaceStyleShading>();
|
||||
//
|
||||
//
|
||||
// std::shared_ptr<SurfaceStyle> surface_style_ptr;
|
||||
//
|
||||
//
|
||||
// if (style->Name()) {
|
||||
// surface_style_ptr.reset(new SurfaceStyle(surface_style_id, *style->Name()));
|
||||
// } else {
|
||||
// surface_style_ptr.reset(new SurfaceStyle(surface_style_id));
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::shared_ptr<const SurfaceStyle> surface_style_ptr_const = std::const_pointer_cast<const SurfaceStyle>(surface_style_ptr);
|
||||
// SurfaceStyle& surface_style = *surface_style_ptr;
|
||||
//
|
||||
//
|
||||
// double rgb[3];
|
||||
// if (process_colour(shading->SurfaceColour(), rgb)) {
|
||||
// surface_style.Diffuse().reset(SurfaceStyle::ColorComponent(rgb[0], rgb[1], rgb[2]));
|
||||
@@ -1353,11 +1353,11 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// }
|
||||
// return style_cache[surface_style_id] = surface_style_ptr_const;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::shared_ptr<const ifcopenshell::geom::SurfaceStyle> ifcopenshell::geom::Kernel::get_style(const IfcSchema::IfcRepresentationptr item) {
|
||||
// return internalize_surface_style(get_surface_style<IfcSchema::IfcSurfaceStyleShading>(item));
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::shared_ptr<const ifcopenshell::geom::SurfaceStyle> ifcopenshell::geom::Kernel::get_style(const IfcSchema::IfcMaterial* material) {
|
||||
// IfcSchema::IfcMaterialDefinitionRepresentation::list::ptr defs = material->HasRepresentation();
|
||||
// for (IfcSchema::IfcMaterialDefinitionRepresentation::list::it jt = defs->begin(); jt != defs->end(); ++jt) {
|
||||
@@ -1376,14 +1376,14 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// auto material_style = std::make_shared<ifcopenshell::geom::SurfaceStyle>(material->data().id(), material->Name());
|
||||
// return style_cache[material->data().id()] = material_style;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// void ifcopenshell::geom::Kernel::apply_layerset(std::vector<ifcopenshell::geom::conversion_result>& r, const ifcopenshell::geom::layerset_information& info) {
|
||||
// convert(info.layers);
|
||||
//
|
||||
//
|
||||
// if (info.layers.empty()) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if (axis_curve->DynamicType() == STANDARD_TYPE(Geom_Line)) {
|
||||
// Handle_Geom_Line axis_line = Handle_Geom_Line::DownCast(axis_curve);
|
||||
// // @todo note that this creates an offset into the wrong order, the cross product arguments should be
|
||||
@@ -1397,7 +1397,7 @@ bool ifcopenshell::geom::open_cascade_kernel::convert_impl(const taxonomy::revol
|
||||
// ifcopenshell::logger::root().message(ifcopenshell::logger::LOG_ERROR, "Unsupported underlying curve of Axis representation:", product);
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// std::vector<ifcopenshell::geom::conversion_result> r2;
|
||||
// if (ifcopenshell::geom::util::apply_layerset(r, const std::vector<ifcopenshell::geom::taxonomy::style>&, std::vector<conversion_result>& r2, double tol)) {
|
||||
// std::swap(r, r2)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user