mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
Compare commits
226 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75ec717162 | |||
| ac3ee016e7 | |||
| 565393e0ec | |||
| c13471a8af | |||
| 75c93ce5be | |||
| 0594e88de1 | |||
| cf60ed3c59 | |||
| ecd328e42e | |||
| c5a24a3f47 | |||
| 7997d5a2c8 | |||
| 3a67602bd2 | |||
| 2a9786bac2 | |||
| df31069743 | |||
| 48eddeec01 | |||
| 453bf9b8e8 | |||
| 705aacf659 | |||
| 94fa2dea42 | |||
| 57f0f89993 | |||
| ab8567fdca | |||
| 57eb5f474e | |||
| ec0e0ab3ff | |||
| 25ffdc3943 | |||
| 147c87a7be | |||
| 3251c1b225 | |||
| d2da2988a4 | |||
| 5903c3c895 | |||
| 028497d4c0 | |||
| b6b8d27f53 | |||
| 3b908aff79 | |||
| 05ba93ab61 | |||
| 05e5a375f5 | |||
| 15deb1c02c | |||
| cc78d0bcc7 | |||
| 9246e9f1be | |||
| 12338714aa | |||
| 99c3304365 | |||
| 087f24185f | |||
| aa4a73f62c | |||
| feba4cedb6 | |||
| be05a04c9e | |||
| 13bc8308b1 | |||
| e104e7486c | |||
| 3103b3c747 | |||
| ea1671933f | |||
| 3c74891c4e | |||
| 42dccbc74b | |||
| 151aa2c45a | |||
| d808104725 | |||
| b71b217814 | |||
| d9f218eacf | |||
| 0faba0fdd8 | |||
| ad113e1283 | |||
| 4a761b51f5 | |||
| 73f8e6aea7 | |||
| e5072460cd | |||
| 311b75a955 | |||
| 6f24133d35 | |||
| 091b4d4113 | |||
| 06d87e21a7 | |||
| b7f1f1728f | |||
| 8074541057 | |||
| d755ca3a59 | |||
| 777b728205 | |||
| 24616ed655 | |||
| 6201c4052b | |||
| ab99024307 | |||
| b7d2b2fa3a | |||
| 5fa14c3dea | |||
| 7fa8506fac | |||
| 9d68e7b9ca | |||
| 9ffbfe0dbb | |||
| 5b00c8b451 | |||
| a5b6f83a3d | |||
| 908d85a51a | |||
| 9089a20ce3 | |||
| d4a5420851 | |||
| 87bc6bfbab | |||
| a5e94cf0d8 | |||
| 2c1d445d5b | |||
| d86f89090b | |||
| 973f61c6dc | |||
| f47aa4d81a | |||
| fa9a3383aa | |||
| 104591a80b | |||
| 030e6e5bb4 | |||
| a031310a66 | |||
| 436e3f7b2a | |||
| 335d571854 | |||
| f78b380b71 | |||
| 2cebc3f60b | |||
| 0a8159505d | |||
| 301fba5a8b | |||
| ba90cf220d | |||
| 6318892a97 | |||
| 1a6336bd20 | |||
| 511584b36f | |||
| f65de78c46 | |||
| 59b957daff | |||
| 81a0941d5a | |||
| dba735f1ee | |||
| e100cf5a34 | |||
| 665502cbc5 | |||
| 252831d7f0 | |||
| 3a6055a558 | |||
| 7b1b0b986c | |||
| cd34d92fdb | |||
| 1391c7d974 | |||
| 223d6da3b1 | |||
| 171e899eb0 | |||
| e2561ffa3b | |||
| 4b87ab5d0d | |||
| 8cc36f0d4d | |||
| 13cc190849 | |||
| b71354ce19 | |||
| 17042f6f80 | |||
| beb0db89e5 | |||
| 572f718007 | |||
| 6bab0603e6 | |||
| b408e64e5e | |||
| f580f7255f | |||
| b252cd25f8 | |||
| 935562142e | |||
| d1d0fb4636 | |||
| e0b226f4ca | |||
| 79bd3563de | |||
| 4e887e1c59 | |||
| fa9f3b5cb7 | |||
| 77dc679a6e | |||
| ce9e2b94d5 | |||
| 58dcaed89a | |||
| 109bd58384 | |||
| b6dccce12a | |||
| daa7d98b3f | |||
| 321760cea4 | |||
| 7b9615f4e5 | |||
| ff22a9d1f3 | |||
| 055f64fa9b | |||
| 7370d07db1 | |||
| 717d6aa2af | |||
| 69b0409aa0 | |||
| 4095d5c8d6 | |||
| 19a3707f72 | |||
| 262117c4f8 | |||
| 246fa24be0 | |||
| 35d2fb43e2 | |||
| f10f7eba83 | |||
| 785936000a | |||
| 83fc219a8a | |||
| 7a1dcd07c8 | |||
| b63137e859 | |||
| 3d15500976 | |||
| 64aed6a766 | |||
| a08eed7ac9 | |||
| 076f46cfeb | |||
| a353edb9e0 | |||
| 30fb379e32 | |||
| dbea3f0362 | |||
| b5eca83357 | |||
| e5aaf7c602 | |||
| 9e53d0dcc9 | |||
| 17c4d8faff | |||
| c9c7edd4d6 | |||
| 96653029cf | |||
| e9fffc221b | |||
| a441757080 | |||
| c818f48a47 | |||
| 28c9c1d34d | |||
| f05dd4aea5 | |||
| dcfc22e29e | |||
| be3c2ee770 | |||
| fbfa51c451 | |||
| 19f3261dc3 | |||
| 7ed8584edc | |||
| 61f30dd200 | |||
| b706121f53 | |||
| 99a09a2a3c | |||
| 2ba55ba984 | |||
| 616c7a00d5 | |||
| 8c003110fe | |||
| 4e49b640a7 | |||
| c30841aad6 | |||
| 4597929df9 | |||
| 2859c1ef17 | |||
| 7ae6bf4374 | |||
| 02481b3247 | |||
| 2c47c9d4fa | |||
| c2abc3f844 | |||
| 4dcd644a32 | |||
| 6fea72b045 | |||
| 1573730f18 | |||
| 8f4832651a | |||
| af58eaf79f | |||
| 8870ffb018 | |||
| c5ba22451f | |||
| 1a931ddfd9 | |||
| f7876a97ee | |||
| 4d0e5f6aee | |||
| dfc60196ec | |||
| ef4bba8b33 | |||
| e044b865dd | |||
| 47812b2d32 | |||
| 16e58f7369 | |||
| 253918c100 | |||
| c57554a07e | |||
| 2aff2adb42 | |||
| 3d05ccbd59 | |||
| 027203008d | |||
| 74b405a9f7 | |||
| 38e0e0e297 | |||
| 048242783e | |||
| d1027c5877 | |||
| d65df3665a | |||
| 393640b121 | |||
| 5d6e3fbdc8 | |||
| 24c3d855de | |||
| ae4f5c92fb | |||
| 75a3bf0175 | |||
| beb5c1f7ed | |||
| 8bdaa8c7cb | |||
| 3340d88b95 | |||
| 0e6e3edaa6 | |||
| ccb9be0940 | |||
| 82f0a74ab2 | |||
| 6f3acc84ee | |||
| e077390e3d | |||
| 80cc603932 |
@@ -1,5 +1,4 @@
|
||||
Checks: 'bugprone-*,cert-*,clang-analyzer-*,readability-*'
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
AnalyzeTemporaryDtors: false
|
||||
FormatStyle: none
|
||||
+4
-2
@@ -19,6 +19,8 @@
|
||||
|
||||
|
||||
# normalize the line endings of the following files
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.cpp text
|
||||
*.css text
|
||||
*.csv text
|
||||
@@ -28,20 +30,20 @@
|
||||
*.gitkeep
|
||||
*.h text
|
||||
*.html text
|
||||
*.i text
|
||||
*.ifc text
|
||||
*.json text
|
||||
*.md text
|
||||
*.po text
|
||||
*.pot text
|
||||
*.py text
|
||||
*.sh text eol=lf
|
||||
*.txt text
|
||||
|
||||
|
||||
# files not normalized ATM
|
||||
# bat
|
||||
# bnf
|
||||
# blend
|
||||
# i
|
||||
# ico
|
||||
# mo
|
||||
# mpass
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
# /// 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@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ jobs:
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
brew update
|
||||
@@ -61,7 +64,7 @@ jobs:
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
python ../nix/cache_dependencies.py unpack
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
@@ -102,7 +105,7 @@ jobs:
|
||||
# INSTALL_RPATH to "@loader_path" on Apple.
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
|
||||
BUILD_BONSAIVIEWER=ON QT_DIR="${QT_DIR}" \
|
||||
python3 ./nix/build-all.py -v --diskcleanup --shared ${MAC_INTEL} \
|
||||
uv run ./nix/build-all.py -v --diskcleanup --shared ${MAC_INTEL} \
|
||||
| tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
@@ -119,7 +122,7 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
python ../nix/cache_dependencies.py pack
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
@@ -132,80 +135,7 @@ jobs:
|
||||
|
||||
- name: Package .zip archives
|
||||
run: |
|
||||
VERSION=v`cat VERSION`
|
||||
# packaging/build.py stages the connector binary + connector.json
|
||||
# into dist/autodesk/; the .app loop below copies that folder into
|
||||
# the bundle. Same on-disk shape as the Linux and Windows builds.
|
||||
python3 src/bonsaiviewer-autodesk/packaging/build.py
|
||||
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
|
||||
test -d "$autodesk_connector_dir"
|
||||
|
||||
cd ./build/`uname`/*/10.15/install/ifcopenshell
|
||||
mkdir -p ~/output
|
||||
install_root="$PWD"
|
||||
|
||||
stage_runtime_payload() {
|
||||
dest="$1"
|
||||
while IFS= read -r runtime_file; do
|
||||
cp -L "$runtime_file" "$dest/"
|
||||
done < <(
|
||||
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
|
||||
[ -d "$runtime_dir" ] || continue
|
||||
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
|
||||
py_version_major=python-${numbers}$postfix
|
||||
pushd . > /dev/null
|
||||
cd $py_version
|
||||
if [ ! -d ifcopenshell ]; then
|
||||
mkdir ../ifcopenshell_
|
||||
mv * ../ifcopenshell_
|
||||
mv ../ifcopenshell_ ifcopenshell
|
||||
fi
|
||||
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
|
||||
find ifcopenshell -name "*.pyc" -delete
|
||||
stage_runtime_payload ifcopenshell
|
||||
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell
|
||||
mv *.zip ~/output
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
rm -f "$install_root"/bin/*.zip
|
||||
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
|
||||
uv run nix/package-zip-archives.py "macos${{ matrix.oldarch }}64" --shared
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -8,6 +8,13 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install Python
|
||||
# Installs latest Python version so it's preferred by uv over system Python.
|
||||
run: uv python install
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
@@ -26,7 +33,7 @@ jobs:
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/nix/cache_dependencies.py unpack
|
||||
uv run ../IfcOpenShell/nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
@@ -40,18 +47,6 @@ jobs:
|
||||
NEW_FILE=`echo $FILE | sed "s/-/+${GITHUB_SHA:0:7}-/2"`
|
||||
mv $FILE $NEW_FILE
|
||||
|
||||
- name: Order wheel shared objects
|
||||
run: |
|
||||
python ./IfcOpenShell/pyodide/order_pyodide_wheel_shared_objects.py dist/ifcopenshell-*.whl
|
||||
|
||||
- name: Split packages
|
||||
run: |
|
||||
VERSION=v`cat ./IfcOpenShell/VERSION`
|
||||
mkdir -p dist-modular
|
||||
python ./IfcOpenShell/pyodide/split_pyodide_ifcopenshell_wheel.py dist/ifcopenshell-*.whl ./dist-modular
|
||||
cd dist-modular
|
||||
zip -r -qq ifcopenshell-modular-${VERSION}-${GITHUB_SHA:0:7}-pyodide.zip *.whl
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -61,23 +56,10 @@ jobs:
|
||||
ifcopenshell_build/*/*/logs/*.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Run wheel tests
|
||||
run: |
|
||||
cp -r IfcOpenShell/pyodide/test test
|
||||
# venv set up in build_pyodide.sh.
|
||||
source .venv/bin/activate
|
||||
uv pip install pytest-pyodide
|
||||
PYODIDE_ROOT_DIST=`pyodide config get pyodide_root`/dist
|
||||
# `pytest-pyodide` requires pyodide in 'pyodide' directory in cwd, when running `pytest`.
|
||||
cp -r $PYODIDE_ROOT_DIST test/pyodide
|
||||
cp dist/ifcopenshell-*.whl test/pyodide
|
||||
cd test
|
||||
pytest --capture=no
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/nix/cache_dependencies.py pack
|
||||
uv run ../IfcOpenShell/nix/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
@@ -88,6 +70,28 @@ jobs:
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Order wheel shared objects
|
||||
run: |
|
||||
uv run ./IfcOpenShell/pyodide/order_pyodide_wheel_shared_objects.py dist/ifcopenshell-*.whl
|
||||
|
||||
- name: Split packages
|
||||
run: |
|
||||
VERSION=v`cat ./IfcOpenShell/VERSION`
|
||||
mkdir -p dist-modular
|
||||
uv run ./IfcOpenShell/pyodide/split_pyodide_ifcopenshell_wheel.py dist/ifcopenshell-*.whl ./dist-modular
|
||||
cd dist-modular
|
||||
zip -r -qq ifcopenshell-modular-${VERSION}-${GITHUB_SHA:0:7}-pyodide.zip *.whl
|
||||
|
||||
- name: Run wheel tests
|
||||
run: |
|
||||
# venv set up in build_pyodide.sh.
|
||||
source .venv/bin/activate
|
||||
ln -s "$PWD/dist" IfcOpenShell/dist
|
||||
ln -s "$PWD/dist-modular" IfcOpenShell/dist-modular
|
||||
cd IfcOpenShell/pyodide
|
||||
./run_pytest.py setup
|
||||
./run_pytest.py run
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
|
||||
@@ -33,14 +33,11 @@ jobs:
|
||||
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
- name: Install Rust
|
||||
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
|
||||
# .zip archives" step below runs `cargo build --release` via
|
||||
# packaging/build.py. Match the dedicated connector workflow's stable
|
||||
# toolchain (dtolnay/rust-toolchain@stable).
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
|
||||
# .zip archives" step below runs `cargo build --release` via
|
||||
# packaging/build.py. Match the dedicated connector workflow's stable
|
||||
# toolchain, rather than whatever Rust the runner image happens to ship.
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install aws cli
|
||||
run: |
|
||||
@@ -110,149 +107,7 @@ jobs:
|
||||
- name: Package .zip archives
|
||||
shell: bash
|
||||
run: |
|
||||
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}-linux64.zip ifcopenshell
|
||||
mv *.zip ~/output
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
rm -f "$install_root"/bin/*.zip
|
||||
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
|
||||
uv run nix/package-zip-archives.py linux64 --shared
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -45,14 +45,11 @@ jobs:
|
||||
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
- name: Install Rust
|
||||
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
|
||||
# .zip archives" step below runs `cargo build --release` via
|
||||
# packaging/build.py. Match the dedicated connector workflow's stable
|
||||
# toolchain (dtolnay/rust-toolchain@stable).
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
|
||||
# .zip archives" step below runs `cargo build --release` via
|
||||
# packaging/build.py. Match the dedicated connector workflow's stable
|
||||
# toolchain, rather than whatever Rust the runner image happens to ship.
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install aws cli
|
||||
run: |
|
||||
@@ -122,149 +119,7 @@ jobs:
|
||||
- name: Package .zip archives
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
|
||||
rm -f "$install_root"/bin/*.zip
|
||||
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
|
||||
uv run nix/package-zip-archives.py linuxarm64 --shared
|
||||
|
||||
- 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@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
- 'src/ifc5d/ifc5d/**'
|
||||
- 'src/ifccityjson/**'
|
||||
branches:
|
||||
- v0.8.0
|
||||
- v0.9.0
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -51,19 +51,10 @@ jobs:
|
||||
name: "Linux Build",
|
||||
short_name: linux,
|
||||
}
|
||||
- {
|
||||
name: "MacOS Build",
|
||||
short_name: macos,
|
||||
}
|
||||
- {
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1,
|
||||
}
|
||||
exclude:
|
||||
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
|
||||
- pyver: py313
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
@@ -72,7 +63,7 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
- name: Compile
|
||||
run: |
|
||||
cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
|
||||
@@ -128,7 +119,7 @@ jobs:
|
||||
blender --command extension install-file -r user_default -e $bonsai_zip
|
||||
blender --command extension list
|
||||
|
||||
git clone https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
|
||||
git clone --branch ${{ github.ref_name }} --single-branch https://github.com/IfcOpenShell/IfcOpenShell.git IfcOpenShell
|
||||
|
||||
# Reregister Bonsai.
|
||||
# Note that running it in background might miss some errors
|
||||
|
||||
@@ -34,19 +34,10 @@ jobs:
|
||||
name: "Linux Build",
|
||||
short_name: linux,
|
||||
}
|
||||
- {
|
||||
name: "MacOS Build",
|
||||
short_name: macos,
|
||||
}
|
||||
- {
|
||||
name: "MacOS ARM Build",
|
||||
short_name: macosm1,
|
||||
}
|
||||
exclude:
|
||||
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
|
||||
- pyver: py313
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7 # https://github.com/actions/setup-python
|
||||
|
||||
@@ -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,11 +24,13 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Strip any trailing prerelease label and number; the dated alpha
|
||||
# suffix is added below.
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
|
||||
@@ -37,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
|
||||
@@ -62,7 +64,7 @@ jobs:
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
|
||||
- name: set ARTIFACTS ENV vars
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -74,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,13 +37,14 @@ 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 \
|
||||
@@ -66,12 +67,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
|
||||
@@ -88,8 +89,8 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Download
|
||||
|
||||
- name: Download
|
||||
uses: actions/download-artifact@v8.0.1
|
||||
with:
|
||||
# Artifact name
|
||||
@@ -100,17 +101,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.8.0
|
||||
- v0.9.0
|
||||
|
||||
jobs:
|
||||
activate:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -3,11 +3,13 @@ name: ci-ifctester-org
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- v0.9.0
|
||||
paths:
|
||||
- src/ifctester/**
|
||||
|
||||
jobs:
|
||||
publish_website:
|
||||
publish_ifctester_org:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -43,6 +43,7 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt-get install --no-install-recommends -y \
|
||||
cmake \
|
||||
bison \
|
||||
gcc \
|
||||
g++ \
|
||||
libboost-date-time-dev \
|
||||
@@ -61,13 +62,23 @@ jobs:
|
||||
libocct-ocaf-dev \
|
||||
libocct-visualization-dev \
|
||||
libpcre3-dev \
|
||||
libpcre2-dev \
|
||||
libtbb-dev \
|
||||
libxml2-dev \
|
||||
libxi-dev \
|
||||
occt-misc \
|
||||
tcl-dev \
|
||||
tk-dev \
|
||||
swig
|
||||
tk-dev
|
||||
|
||||
- name: Build SWIG
|
||||
# IfcOpenShell requires SWIG 4.1+, ubuntu-22.04 ships 4.0.2.
|
||||
run: |
|
||||
sudo apt-get remove --purge -y swig swig4.0
|
||||
git clone https://github.com/swig/swig --branch v4.2.1 --depth 1
|
||||
cmake -S swig -B swig/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build swig/build -j "$(nproc)"
|
||||
sudo cmake --install swig/build
|
||||
swig -version
|
||||
|
||||
- name: Configure minimal IfcOpenShell
|
||||
run: |
|
||||
|
||||
@@ -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:
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
cd OpenCOLLADA
|
||||
git checkout v1.6.68
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/pr622_and_disable_subdirs.patch
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/allow_static_libraries_config_on_unix.patch
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/config_select_libs_by_use_shared.patch
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
run: |
|
||||
echo $Python3_ROOT_DIR
|
||||
echo ${{ env.pythonLocation }}
|
||||
|
||||
|
||||
mkdir build && cd build
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
@@ -185,6 +185,7 @@ 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 \
|
||||
@@ -223,7 +224,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
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
name: Publish C++ API documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v0.9.0
|
||||
paths:
|
||||
- '.github/workflows/publish-cpp-api-docs.yml'
|
||||
- 'docs/cpp-api/**'
|
||||
- 'src/ifcgeom/**'
|
||||
- 'src/ifcparse/**'
|
||||
- 'src/serializers/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-cpp-api-docs
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout IfcOpenShell
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install documentation dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes doxygen graphviz
|
||||
python -m pip install --requirement docs/cpp-api/requirements.txt
|
||||
|
||||
- name: Build C++ API documentation
|
||||
working-directory: docs/cpp-api
|
||||
run: |
|
||||
export PROJECT_NUMBER="$(git rev-parse --short HEAD)"
|
||||
python -m sphinx -M html . output -W --keep-going
|
||||
|
||||
- name: Checkout documentation repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/cpp_docs
|
||||
ref: master
|
||||
path: published-docs
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
- name: Replace published documentation
|
||||
run: |
|
||||
publish_tree="${RUNNER_TEMP}/published-docs-tree"
|
||||
mkdir -p "${publish_tree}/v0.9.0-latest"
|
||||
|
||||
rsync --archive docs/cpp-api/output/html/ "${publish_tree}/v0.9.0-latest/"
|
||||
touch "${publish_tree}/.nojekyll"
|
||||
|
||||
if [[ -f published-docs/CNAME ]]; then
|
||||
cp published-docs/CNAME "${publish_tree}/CNAME"
|
||||
fi
|
||||
|
||||
rsync --archive --delete --exclude='.git/' "${publish_tree}/" published-docs/
|
||||
|
||||
- name: Commit and push if changed
|
||||
working-directory: published-docs
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
git config user.name 'IfcOpenBot'
|
||||
git config user.email 'IfcOpenBot@users.noreply.github.com'
|
||||
|
||||
git add --all
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "Update C++ API docs from ${SOURCE_SHA:0:7}"
|
||||
git push origin master
|
||||
@@ -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
|
||||
|
||||
+13
-3
@@ -16,6 +16,9 @@
|
||||
/src/ifcmax/out/
|
||||
/src/ifcwrap/out/
|
||||
/src/ifctester/webapp/public/pyodide/
|
||||
# pyodide wheels
|
||||
/dist/
|
||||
/dist-modular/
|
||||
|
||||
/win/BuildDepsCache*.txt
|
||||
|
||||
@@ -108,9 +111,13 @@ src/ifcopenshell-python/ifcopenshell/_ifcopenshell_wrapper*.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.py
|
||||
|
||||
# plugins
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell.document.*.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell.geometry.*.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell.parse.schema*.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell_document_*.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell_geometry_*.so
|
||||
src/ifcopenshell-python/ifcopenshell/ifcopenshell_parse_schema*.so
|
||||
src/ifcopenshell-python/ifcopenshell/libifcopenshell.geometry.so
|
||||
src/ifcopenshell-python/ifcopenshell/libifcopenshell.geometry.writer.so
|
||||
src/ifcopenshell-python/ifcopenshell/libifcopenshell.parse.so
|
||||
src/ifcopenshell-python/ifcopenshell/libifcopenshell.plugin.so
|
||||
|
||||
# apple
|
||||
.DS_Store
|
||||
@@ -119,6 +126,9 @@ src/ifcopenshell-python/ifcopenshell/ifcopenshell.parse.schema*.so
|
||||
.clangd
|
||||
# clangd cache
|
||||
.cache
|
||||
# Useful for symlinking json compilation database from cmake,
|
||||
# allowing clang commands without `-p path/to/build`.
|
||||
/compile_commands.json
|
||||
|
||||
# Brickschema
|
||||
src/bonsai/bonsai/bim/schema/Brick.ttl
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
|
||||
path = src/ifcopenshell-python/test/Sample-BIM-Files
|
||||
url = https://github.com/IfcOpenShell/ids-test-files
|
||||
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
|
||||
path = docs/cpp-api/assets/doxygen-awesome-css
|
||||
url = https://github.com/jothepro/doxygen-awesome-css.git
|
||||
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
|
||||
path = src/ifcopenshell-python/ifcopenshell/simple_spf
|
||||
url = https://github.com/IfcOpenShell/step-file-parser
|
||||
|
||||
+57
-34
@@ -36,6 +36,14 @@ file(READ "../VERSION" "RELEASE_VERSION_")
|
||||
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
|
||||
message(STATUS "Detected version '${RELEASE_VERSION}'")
|
||||
|
||||
# CMake's project(VERSION) only accepts numeric components. Keep the complete
|
||||
# release identifier for build information, but use its numeric release part
|
||||
# for PROJECT_VERSION, SOVERSION, and generated CMake package metadata.
|
||||
string(REGEX MATCH "^[0-9]+\\.[0-9]+\\.[0-9]+" PROJECT_VERSION_NUMERIC "${RELEASE_VERSION}")
|
||||
if(NOT PROJECT_VERSION_NUMERIC)
|
||||
message(FATAL_ERROR "VERSION must start with a numeric major.minor.patch version: '${RELEASE_VERSION}'")
|
||||
endif()
|
||||
|
||||
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
|
||||
|
||||
if(POLICY CMP0141) # 3.25+
|
||||
@@ -55,9 +63,15 @@ endif()
|
||||
# Include utility macros and functions
|
||||
include(utilities.cmake)
|
||||
|
||||
# use extra version to make pre-release using eg semver
|
||||
# Use a SemVer-compatible spelling for CPack artifact names. A trailing
|
||||
# alphabetic label and number is separated from the numeric version by a
|
||||
# hyphen: for example, 0.9.0alpha0 becomes 0.9.0-alpha0.
|
||||
if(NOT DEFINED EXTRA_VERSION)
|
||||
set(EXTRA_VERSION "-alpha.3")
|
||||
if(RELEASE_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+([A-Za-z]+)([0-9]+)$")
|
||||
set(EXTRA_VERSION "-${CMAKE_MATCH_1}${CMAKE_MATCH_2}")
|
||||
else()
|
||||
set(EXTRA_VERSION "")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
|
||||
@@ -119,7 +133,8 @@ option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
|
||||
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
|
||||
option(USE_DEBUG_PYTHON "Use debug binaries when building Debug IfcPython on Windows." OFF)
|
||||
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
|
||||
option(VERSION_OVERRIDE "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" OFF)
|
||||
option(VERSION_OVERRIDE "Use VERSION as the branch label when commit information is embedded" OFF)
|
||||
option(USE_CCACHE "Use ccache as a compiler launcher if it is found" OFF)
|
||||
|
||||
set(
|
||||
PYTHON_MODULE_INSTALL_DIR
|
||||
@@ -127,15 +142,7 @@ set(
|
||||
"Directory to install IfcPython package to. By default package is installed in found Python's site-packages."
|
||||
)
|
||||
|
||||
if (VERSION_OVERRIDE)
|
||||
file(READ "../VERSION" "RELEASE_VERSION_")
|
||||
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
|
||||
message(STATUS "Detected version '${RELEASE_VERSION}'")
|
||||
else()
|
||||
set(RELEASE_VERSION "0.8.0")
|
||||
endif()
|
||||
|
||||
project(IfcOpenShell VERSION ${RELEASE_VERSION})
|
||||
project(IfcOpenShell VERSION ${PROJECT_VERSION_NUMERIC})
|
||||
|
||||
# Make sure CMake modules in this project are found first
|
||||
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
|
||||
@@ -172,26 +179,30 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
|
||||
set(BUILD_IFCGEOM ON)
|
||||
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"
|
||||
)
|
||||
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()
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "ccache usage is disabled, set `USE_CCACHE=ON` to enable it.")
|
||||
endif()
|
||||
|
||||
if(MSVC AND MSVC_PARALLEL_BUILD)
|
||||
@@ -305,12 +316,25 @@ if (WITH_ROCKSDB)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB)
|
||||
# See https://github.com/facebook/rocksdb/issues/981.
|
||||
if(TARGET RocksDB::rocksdb)
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb)
|
||||
set(IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET RocksDB::rocksdb)
|
||||
elseif(TARGET RocksDB::rocksdb-shared)
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb-shared)
|
||||
set(IFCOPENSHELL_ROCKSDB_IMPORTED_TARGET 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
|
||||
@@ -693,8 +717,7 @@ endif()
|
||||
|
||||
# Documentation
|
||||
if(BUILD_DOCUMENTATION)
|
||||
set(CMAKE_MODULE_PATH "../docs/cmake")
|
||||
add_subdirectory(../docs docs)
|
||||
add_subdirectory(../docs/cpp-api docs/cpp-api)
|
||||
endif()
|
||||
|
||||
if(BUILD_EXAMPLES)
|
||||
|
||||
@@ -52,6 +52,7 @@ macro(SET_INSTALL_SELF_RPATH _target)
|
||||
endmacro()
|
||||
|
||||
function(ifcopenshell_plugin_target TARGET)
|
||||
# Plug-ins are loaded by exact filename and should not receive a platform library prefix.
|
||||
set_target_properties(${TARGET} PROPERTIES PREFIX "")
|
||||
if((NOT WIN32) AND BUILD_SHARED_LIBS AND NOT WASM_BUILD AND NOT CREATE_BUNDLE AND NOT CMAKE_INSTALL_RPATH AND COMMAND SET_INSTALL_SELF_RPATH)
|
||||
SET_INSTALL_SELF_RPATH(${TARGET})
|
||||
|
||||
+13
-33
@@ -1,35 +1,15 @@
|
||||
#Look for an executable called sphinx-build
|
||||
find_program(SPHINX_EXECUTABLE NAMES sphinx-build DOC "Path to sphinx-build executable")
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
#Handle standard arguments to find_package like REQUIRED and QUIET
|
||||
find_package_handle_standard_args(Sphinx "Failed to find sphinx-build executable" SPHINX_EXECUTABLE)
|
||||
|
||||
find_package(Doxygen REQUIRED)
|
||||
#find_package(Sphinx REQUIRED)
|
||||
find_program(
|
||||
SPHINX_EXECUTABLE
|
||||
NAMES sphinx-build
|
||||
REQUIRED
|
||||
DOC "Path to the sphinx-build executable"
|
||||
)
|
||||
|
||||
set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/docs/sphinx)
|
||||
|
||||
message(STATUS "SPHINX BUILD ${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
file(MAKE_DIRECTORY ./output/doxygen)
|
||||
|
||||
if(DOXYGEN_FOUND)
|
||||
add_custom_target(
|
||||
Sphinx
|
||||
ALL
|
||||
COMMAND ${SPHINX_EXECUTABLE} -v -T -b html ${SPHINX_SOURCE} ${CMAKE_CURRENT_SOURCE_DIR}/output
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/output
|
||||
COMMENT "Generating documentation with Sphinx"
|
||||
)
|
||||
|
||||
# add_custom_target(ifcopenshell_python_docs ALL
|
||||
# COMMAND make html
|
||||
# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../src/ifcblenderexport/docs
|
||||
# OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/../src/ifcblenderexport/docs
|
||||
# COMMENT "Generating documentation with Sphinx")
|
||||
else(DOXYGEN_FOUND)
|
||||
message("Doxygen need to be installed to generate the doxygen documentation")
|
||||
endif(DOXYGEN_FOUND)
|
||||
add_custom_target(
|
||||
cpp_api_docs
|
||||
COMMAND ${SPHINX_EXECUTABLE} -M html . output -W --keep-going
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMENT "Generating the IfcOpenShell C++ API documentation"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
+63
-22
@@ -68,7 +68,7 @@ PROJECT_LOGO =
|
||||
# entered, it will be relative to the location where doxygen was started. If
|
||||
# left blank the current directory will be used.
|
||||
|
||||
OUTPUT_DIRECTORY = ./output
|
||||
OUTPUT_DIRECTORY = ./output/doxygen
|
||||
|
||||
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
|
||||
# sub-directories (in 2 levels) under the output directory of each output format
|
||||
@@ -852,7 +852,7 @@ WARNINGS = YES
|
||||
# will automatically be disabled.
|
||||
# The default value is: YES.
|
||||
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
WARN_IF_UNDOCUMENTED = NO
|
||||
|
||||
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
|
||||
# potential errors in the documentation, such as documenting some parameters in
|
||||
@@ -901,7 +901,7 @@ WARN_IF_UNDOC_ENUM_VAL = NO
|
||||
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
|
||||
# The default value is: NO.
|
||||
|
||||
WARN_AS_ERROR = NO
|
||||
WARN_AS_ERROR = FAIL_ON_WARNINGS
|
||||
|
||||
# The WARN_FORMAT tag determines the format of the warning messages that doxygen
|
||||
# can produce. The string should contain the $file, $line, and $text tags, which
|
||||
@@ -944,7 +944,6 @@ WARN_LOGFILE =
|
||||
# Note: If this tag is empty the current directory is searched.
|
||||
|
||||
INPUT = ../../src/ifcgeom \
|
||||
../../src/ifcgeom_schema_agnostic \
|
||||
../../src/ifcparse \
|
||||
../../src/serializers \
|
||||
|
||||
@@ -1001,7 +1000,7 @@ RECURSIVE = YES
|
||||
# Note that relative paths are relative to the directory from which doxygen is
|
||||
# run.
|
||||
|
||||
EXCLUDE =
|
||||
EXCLUDE = ../../src/ifcparse/schemas
|
||||
|
||||
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
|
||||
# directories that are symbolic links (a Unix file system feature) are excluded
|
||||
@@ -1025,7 +1024,33 @@ EXCLUDE_PATTERNS =
|
||||
# wildcard * is used, a substring. Examples: ANamespace, AClass,
|
||||
# ANamespace::AClass, ANamespace::*Test
|
||||
|
||||
EXCLUDE_SYMBOLS =
|
||||
EXCLUDE_SYMBOLS = "ifcopenshell::geom::opaque_number::*" \
|
||||
ifcopenshell::entity::attribute_by_name_cmp \
|
||||
ifcopenshell::impl::rocks_db_file_storage::rocksdb_types_iterator \
|
||||
ifcopenshell::impl::in_memory_file_storage::type_iterator \
|
||||
"util::string_buffer::*_item" \
|
||||
util::string_buffer::item \
|
||||
ifcopenshell::geom::layer_filter::wildcards_match \
|
||||
ifcopenshell::paged_file_impl::entry \
|
||||
ifcopenshell::token \
|
||||
attribute_value::pointer_type \
|
||||
INCLUDE_PARENT_PARENT_DIR \
|
||||
POSTFIX_SCHEMA_ \
|
||||
POSTFIX_SCHEMA__ \
|
||||
STRINGIFY_ \
|
||||
MAKE_INIT_FN_ \
|
||||
MAKE_INIT_FN__ \
|
||||
key_from_string \
|
||||
add_ \
|
||||
subtract_ \
|
||||
multiply_ \
|
||||
divide_ \
|
||||
equals_ \
|
||||
less_than_ \
|
||||
negate_ \
|
||||
ifcopenshell::geom::utils::create_cube \
|
||||
ifcopenshell::geom::utils::create_polyhedron \
|
||||
ifcopenshell::geom::utils::create_nef_polyhedron
|
||||
|
||||
# The EXAMPLE_PATH tag can be used to specify one or more files or directories
|
||||
# that contain example code fragments that are included (see the \include
|
||||
@@ -1236,7 +1261,7 @@ IGNORE_PREFIX =
|
||||
# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
|
||||
# The default value is: YES.
|
||||
|
||||
GENERATE_HTML = YES
|
||||
GENERATE_HTML = NO
|
||||
|
||||
# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
|
||||
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
|
||||
@@ -1311,7 +1336,7 @@ HTML_STYLESHEET =
|
||||
# documentation.
|
||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||
|
||||
HTML_EXTRA_STYLESHEET = assets/doxygen-awesome-css/doxygen-awesome.css
|
||||
HTML_EXTRA_STYLESHEET =
|
||||
|
||||
# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
|
||||
# other source files which should be copied to the HTML output directory. Note
|
||||
@@ -2166,7 +2191,7 @@ MAN_LINKS = NO
|
||||
# captures the structure of the code including all documentation.
|
||||
# The default value is: NO.
|
||||
|
||||
GENERATE_XML = NO
|
||||
GENERATE_XML = YES
|
||||
|
||||
# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
|
||||
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
|
||||
@@ -2303,7 +2328,7 @@ ENABLE_PREPROCESSING = YES
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
MACRO_EXPANSION = NO
|
||||
MACRO_EXPANSION = YES
|
||||
|
||||
# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
|
||||
# the macro expansion is limited to the macros specified with the PREDEFINED and
|
||||
@@ -2311,7 +2336,7 @@ MACRO_EXPANSION = NO
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
EXPAND_ONLY_PREDEF = YES
|
||||
|
||||
# If the SEARCH_INCLUDES tag is set to YES, the include files in the
|
||||
# INCLUDE_PATH will be searched if a #include is found.
|
||||
@@ -2344,7 +2369,17 @@ INCLUDE_FILE_PATTERNS =
|
||||
# recursively expanded use the := operator instead of the = operator.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
PREDEFINED =
|
||||
PREDEFINED = IFC_PARSE_API= \
|
||||
IFC_SCHEMA_API= \
|
||||
IFC_GEOM_API= \
|
||||
IFC_GEOMLIBRARY_API= \
|
||||
IFC_GEOMSERIALIZATION_API= \
|
||||
SERIALIZERS_API= \
|
||||
"POSTFIX_SCHEMA(name)=name##_Schema" \
|
||||
"Handle(name):=opencascade::handle<name>" \
|
||||
kernel_=kernel \
|
||||
Simplekernel_=Simplekernel \
|
||||
inline=
|
||||
|
||||
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
|
||||
# tag can be used to specify a list of macro names that should be expanded. The
|
||||
@@ -2353,7 +2388,22 @@ PREDEFINED =
|
||||
# definition found in the source code.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
EXPAND_AS_DEFINED =
|
||||
EXPAND_AS_DEFINED = kernel_ \
|
||||
cgal_shape \
|
||||
cgal_kernel \
|
||||
cgal_placement \
|
||||
cgal_point \
|
||||
cgal_direction \
|
||||
cgal_vector \
|
||||
cgal_plane \
|
||||
cgal_curve \
|
||||
cgal_wire \
|
||||
cgal_face \
|
||||
cgal_polyhedron \
|
||||
cgal_vertex_descriptor \
|
||||
cgal_face_descriptor \
|
||||
create_cube \
|
||||
create_polyhedron
|
||||
|
||||
# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
|
||||
# remove all references to function-like macros that are alone on a line, have
|
||||
@@ -2731,15 +2781,6 @@ DOT_GRAPH_MAX_NODES = 50
|
||||
|
||||
MAX_DOT_GRAPH_DEPTH = 0
|
||||
|
||||
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
||||
# files in one run (i.e. multiple -o and -T options on the command line). This
|
||||
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
|
||||
# this, this feature is disabled by default.
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_MULTI_TARGETS = NO
|
||||
|
||||
# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
|
||||
# explaining the meaning of the various boxes and arrows in the dot generated
|
||||
# graphs.
|
||||
|
||||
+41
-18
@@ -1,33 +1,56 @@
|
||||
# IfcOpenShell C++ API documentation
|
||||
|
||||
This folder contains the setup to build the IfcOpenShell C++ API documentation from the source code.
|
||||
This directory contains the Sphinx, Doxygen, Breathe, and Exhale configuration
|
||||
for the IfcOpenShell C++ API reference. During a Sphinx build, Exhale runs
|
||||
Doxygen, Breathe consumes the generated XML, and Exhale creates the API pages.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10 or newer
|
||||
- [Doxygen](https://www.doxygen.nl/)
|
||||
- [Graphviz](https://graphviz.org/)
|
||||
|
||||
Install the Python dependencies from this directory:
|
||||
|
||||
```shell
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Both `doxygen` and `dot` must be available on `PATH`. For the standard Windows
|
||||
install locations, this can be done for the current PowerShell session with:
|
||||
|
||||
```powershell
|
||||
$env:Path = "C:\Program Files\doxygen\bin;C:\Program Files\Graphviz\bin;$env:Path"
|
||||
```
|
||||
|
||||
## Generating the documentation
|
||||
|
||||
> Prerequisites:
|
||||
>
|
||||
> Make sure to have [Doxygen](https://www.doxygen.nl) and [Graphviz](https://graphviz.org) installed into your `$PATH` variable.
|
||||
>
|
||||
> The documentation also use the [doxygen-awesome](https://jothepro.github.io/doxygen-awesome-css) theme as a git submodule.
|
||||
|
||||
Build with the command (from within the `/docs/cpp-api` folder):
|
||||
From this directory, run:
|
||||
|
||||
```shell
|
||||
$ doxygen
|
||||
python -m sphinx -M html . output -W --keep-going
|
||||
```
|
||||
|
||||
To include the current git commit hash into the build documentation, use the following command:
|
||||
To include the current Git commit in Doxygen's project metadata, set
|
||||
`PROJECT_NUMBER` before building. For example, in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:PROJECT_NUMBER = git rev-parse --short HEAD
|
||||
python -m sphinx -M html . output -W --keep-going
|
||||
```
|
||||
|
||||
Or in a POSIX shell:
|
||||
|
||||
```shell
|
||||
$ PROJECT_NUMBER=$(git rev-parse --short HEAD) doxygen
|
||||
PROJECT_NUMBER=$(git rev-parse --short HEAD) python -m sphinx -M html . output -W --keep-going
|
||||
```
|
||||
|
||||
This will extract the current commit hash in short version and sets the propper ENV variable used by doxygen.
|
||||
Alternatively, configure the main CMake project with
|
||||
`-DBUILD_DOCUMENTATION=ON` and build the `cpp_api_docs` target.
|
||||
|
||||
The generation of the documentation might take a while depending on your systems hardware, as it is configured to generate the Class graphs using .
|
||||
The generated documentation is written to `output/html/index.html`. The
|
||||
generated Doxygen XML and Exhale sources are kept under `output/` as build
|
||||
artifacts.
|
||||
|
||||
The resulting documentation is located unter `/cpp-api/output/html` and can be directly accessed with your browser:
|
||||
|
||||
```shell
|
||||
$ open ./output/html/index.html
|
||||
```
|
||||
The generated headers under `src/ifcparse/schemas` are intentionally excluded
|
||||
from this documentation build.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
|
||||
from sphinx.deprecation import RemovedInSphinx90Warning
|
||||
|
||||
warnings.filterwarnings("ignore", category=RemovedInSphinx90Warning, module=r"exhale\.configs")
|
||||
|
||||
generated_directories = (
|
||||
Path(__file__).parent / "output" / "api",
|
||||
Path(__file__).parent / "output" / "doxygen",
|
||||
)
|
||||
for generated_directory in generated_directories:
|
||||
if generated_directory.is_dir():
|
||||
rmtree(generated_directory)
|
||||
|
||||
project = "IfcOpenShell"
|
||||
copyright = "2020, IfcOpenShell"
|
||||
|
||||
extensions = [
|
||||
"breathe",
|
||||
"exhale",
|
||||
]
|
||||
|
||||
primary_domain = "cpp"
|
||||
highlight_language = "cpp"
|
||||
html_theme = "alabaster"
|
||||
|
||||
breathe_projects = {
|
||||
"IfcOpenShell": "./output/doxygen/xml",
|
||||
}
|
||||
breathe_default_project = "IfcOpenShell"
|
||||
|
||||
exhale_args = {
|
||||
"containmentFolder": "./output/api",
|
||||
"rootFileName": "library_root.rst",
|
||||
"rootFileTitle": "IfcOpenShell C++ API",
|
||||
"doxygenStripFromPath": "../..",
|
||||
"createTreeView": False,
|
||||
"exhaleExecutesDoxygen": True,
|
||||
"exhaleUseDoxyfile": True,
|
||||
}
|
||||
|
||||
cpp_id_attributes = [
|
||||
"IFC_PARSE_API",
|
||||
"IFC_SCHEMA_API",
|
||||
"IFC_GEOM_API",
|
||||
"IFC_GEOMLIBRARY_API",
|
||||
"IFC_GEOMSERIALIZATION_API",
|
||||
"SERIALIZERS_API",
|
||||
]
|
||||
|
||||
exclude_patterns = [
|
||||
"output/doctrees",
|
||||
"output/doxygen",
|
||||
"output/html",
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
.. This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
IfcOpenShell C++ API
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
output/api/library_root
|
||||
@@ -0,0 +1,5 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
Sphinx==8.1.3
|
||||
breathe==4.36.0
|
||||
exhale==0.3.7
|
||||
+532
-211
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,506 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# ///
|
||||
|
||||
import argparse
|
||||
import json
|
||||
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")
|
||||
|
||||
|
||||
class RuntimeInfo(NamedTuple):
|
||||
runtime_dirs: list[Path]
|
||||
qt_dir: Path | None
|
||||
|
||||
|
||||
def find_qt_dir(install_root: Path, qt6_version: str, qt6_install_root: str | None) -> Path | None:
|
||||
search_root = Path(qt6_install_root).parent if qt6_install_root else install_root
|
||||
|
||||
for qt_candidate in search_root.glob(f"qt6-{qt6_version}-*/{qt6_version}/*"):
|
||||
if (qt_candidate / "lib").is_dir():
|
||||
return qt_candidate
|
||||
return None
|
||||
|
||||
|
||||
def get_runtime_info(install_root: Path, qt6_version: str) -> RuntimeInfo:
|
||||
install_dirs_path = install_root / "install_dirs.json"
|
||||
install_dirs: dict[str, str] = json.loads(install_dirs_path.read_text()) if install_dirs_path.is_file() else {}
|
||||
|
||||
# Qt is handled separately via `stage_qt_runtime_payload`.
|
||||
qt6_install_root = None
|
||||
if "qt6" in install_dirs:
|
||||
qt6_install_root = install_dirs.pop("qt6")
|
||||
|
||||
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, qt6_install_root)
|
||||
|
||||
if ARGS.shared:
|
||||
dependencies_to_stage = install_dirs.keys()
|
||||
elif ARGS.occt_shared:
|
||||
dependencies_to_stage = {"occt"}
|
||||
else:
|
||||
return RuntimeInfo([], qt_dir)
|
||||
|
||||
runtime_dirs = []
|
||||
for name in dependencies_to_stage:
|
||||
runtime_dir = Path(install_dirs[name])
|
||||
assert "-shared-" in runtime_dir.name, f"Expected a shared build, found: {runtime_dir}"
|
||||
runtime_dirs.append(runtime_dir)
|
||||
return RuntimeInfo(runtime_dirs, qt_dir)
|
||||
|
||||
|
||||
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
|
||||
# Currently there's an overlap between dependencies installations.
|
||||
# E.g. libraries from occt are installed to both `ifcopenshell/lib`
|
||||
# (as part of `ifcopenshell_deploy_qt_runtime`)
|
||||
# and to `occt-shared/lib`. So we skip previously installed binaries.
|
||||
if dest_file.exists():
|
||||
continue
|
||||
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,
|
||||
runtime_dirs: list[Path],
|
||||
) -> 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)
|
||||
|
||||
for runtime_dir in runtime_dirs:
|
||||
stage_runtime_payload(runtime_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,
|
||||
runtime_dirs: list[Path],
|
||||
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"))
|
||||
|
||||
for runtime_dir in runtime_dirs:
|
||||
stage_runtime_payload(runtime_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
|
||||
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.")
|
||||
parser.add_argument("--occt-shared", action="store_true", help="OCCT was built as shared libraries.")
|
||||
parser.add_argument("--shared", action="store_true", help="Build was made with 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,
|
||||
shared=args.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")
|
||||
runtime_dirs, qt_dir = get_runtime_info(install_root, qt6_version)
|
||||
|
||||
# Iterate over all built Python wrappers in `install/ifcopenshell/python-x.y.z`
|
||||
# and zip them, bundling all dynamic libs from `lib`.
|
||||
github_sha = 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, runtime_dirs)
|
||||
|
||||
# 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,
|
||||
runtime_dirs,
|
||||
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,3 +1,13 @@
|
||||
# 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
|
||||
@@ -27,11 +37,13 @@ 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 { namespace tr1
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
+ #define COLLADABU_HASH_NAMESPACE_OPEN std
|
||||
+ #define COLLADABU_HASH_NAMESPACE_CLOSE
|
||||
#define COLLADABU_HASH_FUN hash
|
||||
@@ -107,12 +107,12 @@
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE
|
||||
@@ -45,11 +57,13 @@ 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 { namespace tr1
|
||||
#define COLLADABU_HASH_NAMESPACE_CLOSE }
|
||||
+ #define COLLADABU_HASH_NAMESPACE_OPEN std
|
||||
+ #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
|
||||
|
||||
+10
-2
@@ -28,5 +28,13 @@ since it's pure cmake without any additional moving parts.
|
||||
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
|
||||
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
|
||||
- it will produce Python package in `IfcOpenShell/ifcopenshell`
|
||||
- run `pyodide build`
|
||||
- it will produce a wheel in `IfcOpenShell/dist`
|
||||
- run `python pyodide/build-all-pack-wheel-local.py`, it will
|
||||
- clean up previous wheels
|
||||
- run `pyodide build`
|
||||
- prepare standalone and modular wheels
|
||||
- produce final wheels in `IfcOpenShell/dist` and `IfcOpenshell/dist-modular`
|
||||
- testing:
|
||||
- ensure you're in pyodide environment
|
||||
- `cd IfcOpenshell/pyodide`
|
||||
- `./run_pytest.py setup`
|
||||
- `./run_pytest.py run`
|
||||
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Intended to be run after nix/build-all.py has finished the wasm build."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
output = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True)
|
||||
return Path(output.strip())
|
||||
|
||||
|
||||
def run(cmd: list[str], **kwargs) -> None:
|
||||
print("$", " ".join(cmd))
|
||||
subprocess.check_call(cmd, **kwargs)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
repo_root = get_repo_root()
|
||||
|
||||
shutil.rmtree(repo_root / "dist", ignore_errors=True)
|
||||
shutil.rmtree(repo_root / "dist_modular", ignore_errors=True)
|
||||
run(["pyodide", "build"], cwd=repo_root)
|
||||
shutil.rmtree(repo_root / "ifcopenshell", ignore_errors=True)
|
||||
(repo_root / "setup.py").unlink(missing_ok=True)
|
||||
run(["git", "restore", "pyproject.toml"], cwd=repo_root)
|
||||
|
||||
wheel = next((repo_root / "dist").glob("ifcopenshell-*.whl"))
|
||||
|
||||
run(["uv", "run", "pyodide/order_pyodide_wheel_shared_objects.py", str(wheel)], cwd=repo_root)
|
||||
run(
|
||||
["uv", "run", "pyodide/split_pyodide_ifcopenshell_wheel.py", str(wheel), "dist-modular/"],
|
||||
cwd=repo_root,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
PYODIDE_VERSION=0.29.3
|
||||
PYODIDE_BUILD_VERSION=0.33.0
|
||||
PYODIDE_XBUILDENV_ROOT="${HOME}/.cache/.pyodide-xbuildenv-${PYODIDE_BUILD_VERSION}"
|
||||
PYODIDE_XBUILDENV="${PYODIDE_XBUILDENV_ROOT}/${PYODIDE_VERSION}"
|
||||
PYODIDE_VERSION=0.29.4
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
# Script is assuming that it will be possible to execute it multiple times
|
||||
# therefore we're clearing venv each time and ignoring existing 'emsdk' folder.
|
||||
@@ -16,12 +14,14 @@ source .venv/bin/activate
|
||||
|
||||
# Install pyodide cross build environment.
|
||||
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
|
||||
uv pip install "pyodide-build==${PYODIDE_BUILD_VERSION}"
|
||||
uv pip install -r "${SCRIPT_DIR}/requirements.txt"
|
||||
# `uv run` is required, so xbuildenv would skip using `pip`.
|
||||
uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
|
||||
uv run pyodide xbuildenv install-emscripten
|
||||
|
||||
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
|
||||
# Cache path includes a hash segment that varies by pyodide-build version,
|
||||
# so query it instead of constructing it manually.
|
||||
EMSDK_ROOT=$(uv run pyodide config get emsdk_dir)
|
||||
[ -f "${EMSDK_ROOT}/emsdk_env.sh" ] && source "${EMSDK_ROOT}/emsdk_env.sh"
|
||||
[ -f "${EMSDK_ROOT}/../../emsdk_env.sh" ] && source "${EMSDK_ROOT}/../../emsdk_env.sh"
|
||||
which emcc
|
||||
@@ -29,8 +29,10 @@ emcc --version
|
||||
|
||||
mkdir -p packages/ifcopenshell
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
# Normalize to the canonical PEP 440 form (e.g. 0.9.0alpha0 -> 0.9.0a0).
|
||||
VERSION=`python3 -c "from packaging.version import Version; print(Version('$VERSION'))"`
|
||||
cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell
|
||||
sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
|
||||
sed -i s/9.9.9/$VERSION/g packages/ifcopenshell/meta.yaml
|
||||
|
||||
# Use custom build ifcopenshell directory in build-all to make caching simpler
|
||||
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
package:
|
||||
name: ifcopenshell
|
||||
version: 0.8.0
|
||||
# Placeholder, replaced by build_pyodide.sh with the actual version from VERSION file.
|
||||
version: 9.9.9
|
||||
|
||||
source:
|
||||
# meta.yaml is placed as `packages/ifcopenshell/meta.yaml`.
|
||||
@@ -8,7 +9,7 @@ source:
|
||||
|
||||
build:
|
||||
script: |
|
||||
BUILD_CFG=Release python nix/build-all.py -v --wasm --py313
|
||||
BUILD_CFG=Release python nix/build-all.py -v --wasm
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# ///
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
"""Order Pyodide wheel shared objects so wasm side modules load safely."""
|
||||
"""Order Pyodide wheel shared objects so wasm side modules load safely.
|
||||
|
||||
Pyodide's package loader loads a wheel's bundled ``.so`` files in the order
|
||||
they appear in the wheel's zip.
|
||||
If a ``.so`` that depends on symbols from another ``.so`` is loaded first,
|
||||
loading fails with errors like
|
||||
- "Failed to load dynamic library"
|
||||
- "Dynamic linking error: cannot resolve symbol"
|
||||
|
||||
This is a known issue upstream - https://github.com/pyodide/pyodide/issues/6020.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -22,10 +34,10 @@ SCHEMA_ORDER = {
|
||||
}
|
||||
|
||||
MAIN_SHARED_OBJECT_RE = re.compile(r"^_ifcopenshell_wrapper(?:\.|$)")
|
||||
SCHEMA_PLUGIN_RE = re.compile(r"^ifcopenshell\.parse\.schema\.([^.]+)\.so$")
|
||||
MAPPING_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.mapping\.([^.]+)\.so$")
|
||||
DOCUMENT_PLUGIN_RE = re.compile(r"^ifcopenshell\.document\.[^.]+\.([^.]+)\.so$")
|
||||
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.serialization\.([^.]+)\.so$")
|
||||
SCHEMA_PLUGIN_RE = re.compile(r"^ifcopenshell_parse_schema_(.+)\.so$")
|
||||
MAPPING_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_mapping_(.+)\.so$")
|
||||
DOCUMENT_PLUGIN_RE = re.compile(r"^ifcopenshell_document_[a-z0-9]+(?:_(.+))?\.so$")
|
||||
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_writer_(.+)\.so$")
|
||||
|
||||
|
||||
def schema_key(schema: str) -> tuple[int, str]:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pyodide-build==0.39.0
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
|
||||
DIST_DIRS = (
|
||||
SCRIPT_DIR / "test/pyodide",
|
||||
SCRIPT_DIR / "test/pyodide-modular",
|
||||
)
|
||||
WHEEL_SRCS = (
|
||||
SCRIPT_DIR / "../dist",
|
||||
SCRIPT_DIR / "../dist-modular",
|
||||
)
|
||||
|
||||
|
||||
def run(cmd: list, **kwargs) -> None:
|
||||
print("$", shlex.join(str(part) for part in cmd))
|
||||
subprocess.check_call(cmd, **kwargs)
|
||||
|
||||
|
||||
def setup() -> None:
|
||||
run(["uv", "pip", "install", "pytest-pyodide"])
|
||||
|
||||
# Copy pyodide installation so we can modify it locally just for tests.
|
||||
pyodide_root = subprocess.check_output(["pyodide", "config", "get", "pyodide_root"], text=True).strip()
|
||||
pyodide_root_dist = Path(pyodide_root) / "dist"
|
||||
for dist_dir in DIST_DIRS:
|
||||
if dist_dir.exists():
|
||||
shutil.rmtree(dist_dir)
|
||||
shutil.copytree(pyodide_root_dist, dist_dir)
|
||||
|
||||
|
||||
def run_tests() -> None:
|
||||
for dist_dir, wheel_src in zip(DIST_DIRS, WHEEL_SRCS):
|
||||
if not wheel_src.exists():
|
||||
raise RuntimeError(f"error: {wheel_src} does not exist")
|
||||
|
||||
# Clean up previous wheels.
|
||||
for whl in dist_dir.glob("ifcopenshell*.whl"):
|
||||
whl.unlink()
|
||||
|
||||
# Symlink new ones.
|
||||
for whl in wheel_src.glob("ifcopenshell*.whl"):
|
||||
(dist_dir / whl.name).symlink_to(whl.resolve())
|
||||
|
||||
for dist_dir in DIST_DIRS:
|
||||
run(["pytest", f"--dist-dir={dist_dir}", "--capture=no"], cwd=SCRIPT_DIR)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("command", choices=["setup", "run"])
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "setup":
|
||||
setup()
|
||||
else:
|
||||
run_tests()
|
||||
@@ -1,5 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Split optional IfcOpenShell Pyodide payloads into separate wheels."""
|
||||
# /// script
|
||||
# ///
|
||||
"""Split optional IfcOpenShell Pyodide payloads into separate wheels.
|
||||
|
||||
The main wheel bundles per-schema plugin ``.so`` files and pure Python
|
||||
subpackages that most browser sessions probably don't need.
|
||||
|
||||
This splits each of those out into its own installable wheel,
|
||||
so a Pyodide app can fetch just the base wheel plus whichever schema/plugin wheels it actually needs.
|
||||
|
||||
Resulting wheels (roughly):
|
||||
- ifcopenshell.whl (main ifcopenshell.py files + _ifcopenshell_wrapper)
|
||||
- ifcopenshell_pure_python.whl (api, express, python files only)
|
||||
- splitted wheels with a single .so binary - e.g. `ifcopenshell_parse_schema_ifc4.whl`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -159,6 +173,7 @@ def build_wheel(
|
||||
|
||||
entries[record_name] = None
|
||||
write_record(zf, entries, record_name)
|
||||
print(f"Splitting wheel to '{out}'.")
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pyodide
|
||||
pyodide-modular
|
||||
+22
-11
@@ -1,18 +1,30 @@
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
WHEEL_FILENAME = next(
|
||||
p.name for p in (Path.cwd() / "pyodide").iterdir() if p.name.startswith("ifcopenshell-") and p.suffix == ".whl"
|
||||
)
|
||||
from ..order_pyodide_wheel_shared_objects import shared_object_sort_key
|
||||
|
||||
|
||||
def test_ifcopenshell_import(selenium):
|
||||
def _first_so_name(wheel_path: Path) -> str:
|
||||
with zipfile.ZipFile(wheel_path) as zf:
|
||||
for name in zf.namelist():
|
||||
if name.endswith(".so"):
|
||||
return Path(name).name
|
||||
return wheel_path.name
|
||||
|
||||
|
||||
def test_ifcopenshell_import(selenium, request):
|
||||
dist_dir = Path(request.config.getoption("--dist-dir"))
|
||||
wheel_paths = list(dist_dir.glob("ifcopenshell*.whl"))
|
||||
wheel_paths.sort(key=lambda path: shared_object_sort_key(_first_so_name(path), 0))
|
||||
WHEEL_NAMES = tuple(path.name for path in wheel_paths)
|
||||
|
||||
selenium.load_package("micropip")
|
||||
# Important to test it with `micropip.install`
|
||||
# without any dependencies loaded to ensure micropip will load them automatically.
|
||||
selenium.run_async(
|
||||
f"""
|
||||
selenium.run_async(f"""
|
||||
import micropip
|
||||
await micropip.install(f"./{WHEEL_FILENAME}")
|
||||
wheel_filenames = {WHEEL_NAMES!r}
|
||||
for wheel_filename in wheel_filenames:
|
||||
print(f"Loading {{wheel_filename}}...")
|
||||
await micropip.install(f"./{{wheel_filename}}")
|
||||
import ifcopenshell
|
||||
from pathlib import Path
|
||||
ifcopenshell.set_plugin_search_paths([str(Path(ifcopenshell.__file__).parent)])
|
||||
@@ -24,5 +36,4 @@ def test_ifcopenshell_import(selenium):
|
||||
wall.Name = "Test"
|
||||
assert wall.Name == "Test", f"Entity name wasn't changed: {{wall}}"
|
||||
print(wall)
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
+36
-14
@@ -6,10 +6,6 @@ version = "0.0.0"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
include = '''
|
||||
src/.*.pyi?$
|
||||
|nix/.*.pyi?$
|
||||
'''
|
||||
extend-exclude = '''
|
||||
src/ifcopenshell-python/ifcopenshell/express/rules/*
|
||||
|src/ifcopenshell-python/ifcopenshell/express/express_parser.py
|
||||
@@ -18,6 +14,15 @@ extend-exclude = '''
|
||||
|src/ifc2ca/templates/*
|
||||
|src/svgfill
|
||||
|src/exterior-shell-extractor
|
||||
|choco/bonsai/tools/enable_blenderbim_addon.py
|
||||
|choco/bonsai/tools/disable_blenderbim_addon.py
|
||||
|docs/conf.py
|
||||
|docs/generate_docs.py
|
||||
|aws/lambda/example_handler/__init__.py
|
||||
|conda/update_version_init.py
|
||||
|test/bpy.py
|
||||
|test/tests.py
|
||||
|test/run.py
|
||||
'''
|
||||
|
||||
[tool.pyright]
|
||||
@@ -62,20 +67,30 @@ select = [
|
||||
#
|
||||
"FA", # future annotations
|
||||
"UP", # pyupgrade
|
||||
"RUF015", # next() > list_comprehension[0]
|
||||
"RUF022", # sort __all__
|
||||
"unnecessary-iterable-allocation-for-first-element",
|
||||
"unsorted-dunder-all",
|
||||
"I", # import sorting
|
||||
"unused-noqa",
|
||||
"rule-codes-in-selectors",
|
||||
"noqa-comments",
|
||||
"rule-codes-in-suppression-comments",
|
||||
# General util rules.
|
||||
"invalid-rule-code",
|
||||
"redirected-noqa",
|
||||
"invalid-pyproject-toml",
|
||||
"invalid-suppression-comment",
|
||||
]
|
||||
ignore = [
|
||||
"FA100", # Conflicts with Blender using annotations for props definitions.
|
||||
# Conflicts with Blender using annotations for props definitions.
|
||||
"future-rewritable-type-annotation",
|
||||
# Maybe will enable later:
|
||||
"UP007", # Optional to X | Y
|
||||
"UP045", # Optional to X | None
|
||||
"UP015", # Unnecessary mode argument
|
||||
"UP028", # yield for -> yield from
|
||||
"UP030", # implicit references for positional format fields
|
||||
"UP031", # Replace % with .format
|
||||
"UP032", # Replace .format with f-string
|
||||
"non-pep604-annotation-union", # Union[X,Y] to X | Y
|
||||
"non-pep604-annotation-optional", # Optional to X | None
|
||||
"redundant-open-modes", # Unnecessary mode argument
|
||||
"yield-in-for-loop", # yield for -> yield from
|
||||
"format-literals", # implicit references for positional format fields
|
||||
"printf-string-formatting", # Replace % with .format
|
||||
"f-string", # Replace .format with f-string
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
@@ -98,10 +113,13 @@ 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"
|
||||
@@ -109,6 +127,8 @@ 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 = [
|
||||
@@ -170,6 +190,8 @@ dev-setup.help = "Install repo packages in editable mode"
|
||||
|
||||
ruff = "ruff check"
|
||||
|
||||
check-whitespace = "uv run .github/scripts/check-whitespace.py"
|
||||
|
||||
black = "black ."
|
||||
|
||||
ty.sequence = ["ty-bonsai", "ty-ios"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
black==26.3.1
|
||||
ruff==0.16.0
|
||||
black==26.5.1
|
||||
ruff==0.16.4
|
||||
poethepoet
|
||||
ty==0.0.63
|
||||
ty==0.0.74
|
||||
gersemi==0.28.0
|
||||
|
||||
@@ -10,7 +10,7 @@ name = "bcf-client"
|
||||
# author = "IfcOpenShell"
|
||||
description = "BCF-XML file handler."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.10"
|
||||
keywords = ["IFC", "BCF", "BIM"]
|
||||
dependencies = [
|
||||
"xsdata>=24.4",
|
||||
@@ -65,6 +65,6 @@ commands = pytest --cov --cov-report=term tests
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
lint.select = [
|
||||
"F401", # unused imports
|
||||
lint.extend-select = [
|
||||
"unused-import", # unused imports
|
||||
]
|
||||
|
||||
+17
-34
@@ -42,10 +42,12 @@ endif
|
||||
|
||||
IS_STABLE:=FALSE
|
||||
VERSION:=$(shell cat ../../VERSION)
|
||||
VERSION_MAJOR:=$(shell cat '../../VERSION' | cut -d '.' -f 1)
|
||||
VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
|
||||
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
|
||||
VERSION_BASE:=$(shell sed -E 's/[[:alpha:]]+[0-9]+$$//' ../../VERSION)
|
||||
VERSION_PYTHON:=$(shell sed 's/alpha/a/' ../../VERSION)
|
||||
VERSION_SEMVER:=$(shell sed -E 's/([[:alpha:]]+)([0-9]+)$$/-\\1\\2/' ../../VERSION)
|
||||
VERSION_DATE:=$(shell date '+%y%m%d')
|
||||
VERSION_DAILY:=$(VERSION_BASE)a$(VERSION_DATE)
|
||||
VERSION_SEMVER_DAILY:=$(VERSION_BASE)-alpha$(VERSION_DATE)
|
||||
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
|
||||
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
|
||||
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
|
||||
@@ -67,32 +69,17 @@ endif # def PYVERSION
|
||||
IFCMERGE_VERSION:=2026-04-07
|
||||
|
||||
ifdef PLATFORM
|
||||
SUPPORTED_PLATFORMS := linux macos macosm1 win
|
||||
SUPPORTED_PLATFORMS := linux macosm1 win
|
||||
|
||||
ifeq ($(filter $(PLATFORM),$(SUPPORTED_PLATFORMS)),)
|
||||
$(error Unsupported PLATFORM=$(PLATFORM). Must be one of $(SUPPORTED_PLATFORMS))
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM),macos)
|
||||
ifeq ($(PYVERSION),py313)
|
||||
$(error Blender 5.1 with Python 3.13 doesn't support intel macOS.)
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), linux)
|
||||
PYPI_PLATFORM:=--platform manylinux_2_17_x86_64
|
||||
BLENDER_PLATFORM:=linux-x64
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), macos)
|
||||
ifeq ($(PYVERSION), py311)
|
||||
PYPI_PLATFORM:=--platform macosx_10_10_x86_64
|
||||
else
|
||||
PYPI_PLATFORM:=--platform macosx_10_13_x86_64
|
||||
endif
|
||||
BLENDER_PLATFORM:=macos-x64
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM), macosm1)
|
||||
PYPI_PLATFORM:=--platform macosx_11_0_arm64
|
||||
BLENDER_PLATFORM:=macos-arm64
|
||||
@@ -106,7 +93,7 @@ endif
|
||||
endif # def PLATFORM
|
||||
|
||||
# Current build commit hash.
|
||||
OLD:=3e7b739
|
||||
OLD:=ad113e1
|
||||
.PHONY: bump
|
||||
bump:
|
||||
ifndef NEW
|
||||
@@ -192,10 +179,8 @@ endif
|
||||
# Provides networkx graph analysis for project dependency calculations
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download networkx --dest=./wheels
|
||||
# Required by IFCDiff
|
||||
# Pinned <9.1: deepdiff 9.1.0 adds cachebox<6,>=5.2 which only ships macOS x86_64
|
||||
# wheels for macosx_10_12+ and is incompatible with our macos py311 --platform
|
||||
# macosx_10_10_x86_64 target. Revisit once the macos py311 platform tag is bumped
|
||||
# to 10_13 (matching py312/py313).
|
||||
# Pinned <9.1: deepdiff 9.1.0 adds the compiled dependency cachebox<6,>=5.2,
|
||||
# which this platformless download cannot provide for every target platform.
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download "deepdiff<9.1" --dest=./wheels
|
||||
# Required by IFCCSV and ifcopenshell.util.selector
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download lark --dest=./wheels
|
||||
@@ -213,8 +198,6 @@ endif
|
||||
# pyradiance is using different platform versions than defaults in our makefile.
|
||||
ifeq ($(PLATFORM), linux)
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform manylinux_2_28_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
else ifeq ($(PLATFORM), macos)
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance --platform macosx_10_13_x86_64 --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
else
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PIP) download pyradiance $(PYPI_PLATFORM) --python-version $(PYPI_VERSION) --implementation $(PYPI_IMP) --only-binary=:all: --dest=./wheels
|
||||
endif
|
||||
@@ -260,14 +243,14 @@ endif
|
||||
|
||||
cp pyproject.toml build/
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
$(SED) "s/0.0.0/$(VERSION)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
|
||||
$(SED) "s/0.0.0/$(VERSION_SEMVER)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
|
||||
else
|
||||
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) "s/0.0.0/$(VERSION_SEMVER_DAILY)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
|
||||
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
|
||||
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
|
||||
endif
|
||||
|
||||
# Blender 5.1+ requires Python 3.13.
|
||||
@@ -279,9 +262,9 @@ endif
|
||||
|
||||
# Provides bonsai Add-on functionality
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
|
||||
else
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
|
||||
endif
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build
|
||||
cp build/dist/*.whl build/wheels/
|
||||
@@ -315,9 +298,9 @@ endif
|
||||
rm -rf build/bonsai/libs/
|
||||
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
else
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-alpha$(VERSION_DATE)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER_DAILY)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
endif
|
||||
|
||||
mv build/bonsai*.zip dist/
|
||||
|
||||
@@ -185,13 +185,10 @@ class IfcStore:
|
||||
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
|
||||
IfcStore.cache_path = cache_path
|
||||
cache_path = Path(IfcStore.cache_path)
|
||||
cache_settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
settings = ifcopenshell.geom.settings()
|
||||
cache_preexists = cache_path.exists()
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
|
||||
IfcStore.cache_path, cache_settings, serializer_settings
|
||||
)
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
|
||||
if cache_preexists:
|
||||
print(f"Successfully loaded existing cache: {cache_path.name}.")
|
||||
else:
|
||||
@@ -206,9 +203,7 @@ class IfcStore:
|
||||
|
||||
os.remove(IfcStore.cache_path)
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
|
||||
IfcStore.cache_path, cache_settings, serializer_settings
|
||||
)
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
|
||||
print("New cache was created.")
|
||||
except Exception as e:
|
||||
print(f"Failed to create a cache: {str(e)}.")
|
||||
|
||||
@@ -740,7 +740,7 @@ class IfcImporter:
|
||||
self.update_progress((percent_average / 100 * progress_range) + start_progress)
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
product = self.file.by_id(shape.id)
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
@@ -1079,9 +1079,9 @@ class IfcImporter:
|
||||
def create_curve(
|
||||
self,
|
||||
element: ifcopenshell.entity_instance,
|
||||
shape: Union[W.Triangulation, W.TriangulationElement],
|
||||
shape: Union[W.triangulation, W.triangulation_element],
|
||||
) -> bpy.types.Curve:
|
||||
if isinstance(shape, W.TriangulationElement):
|
||||
if isinstance(shape, W.triangulation_element):
|
||||
geometry = shape.geometry
|
||||
else:
|
||||
geometry = shape
|
||||
@@ -1112,11 +1112,11 @@ class IfcImporter:
|
||||
def create_mesh(
|
||||
self,
|
||||
element: ifcopenshell.entity_instance,
|
||||
shape: Union[W.Triangulation, W.TriangulationElement],
|
||||
shape: Union[W.triangulation, W.triangulation_element],
|
||||
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
|
||||
) -> Union[bpy.types.Mesh, None]:
|
||||
try:
|
||||
if isinstance(shape, W.TriangulationElement):
|
||||
if isinstance(shape, W.triangulation_element):
|
||||
# shape is ShapeElementType
|
||||
geometry = shape.geometry
|
||||
else:
|
||||
|
||||
@@ -678,7 +678,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# Identify all potential building elements
|
||||
# TODO: don't select everything, use AABB culling in Blender
|
||||
building_elements = (
|
||||
building_elements = list(
|
||||
tool.Ifc.get().by_type("IfcWall")
|
||||
+ tool.Ifc.get().by_type("IfcSlab")
|
||||
+ tool.Ifc.get().by_type("IfcVirtualElement")
|
||||
@@ -708,7 +708,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
shapes[shape.id] = {
|
||||
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
|
||||
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
|
||||
|
||||
@@ -348,10 +348,7 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
if self.obj_type == "Object":
|
||||
if context.selected_objects:
|
||||
objects = [o.name for o in context.selected_objects]
|
||||
else:
|
||||
objects = [context.active_object.name]
|
||||
objects = [o.name for o in tool.Blender.get_selected_objects()]
|
||||
else:
|
||||
objects = [self.obj]
|
||||
props = tool.Classification.get_classification_props()
|
||||
|
||||
@@ -516,7 +516,7 @@ def _world_segment_to_screen_pixels(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_GT_box_face_quad(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
|
||||
class BIM_GT_box_face_quad(bpy.types.Gizmo):
|
||||
"""Near-invisible face-quad click target with drag-to-resize modal.
|
||||
|
||||
Geometry: a unit quad in the local XY plane at z=0. The adapter
|
||||
@@ -620,7 +620,7 @@ class BIM_GT_box_face_quad(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class BIM_GT_box_face_outline(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
|
||||
class BIM_GT_box_face_outline(bpy.types.Gizmo):
|
||||
"""Thin non-interactive colored edge outline for one face.
|
||||
|
||||
Drawn as 4 line segments in the face plane. The layout helper
|
||||
|
||||
@@ -160,7 +160,7 @@ def _make_face_set_cb(gz: Any, group: Any, axis: int, is_max: bool):
|
||||
return setter
|
||||
|
||||
|
||||
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup): # noqa: N801 — Blender bl_idname convention
|
||||
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup):
|
||||
"""Face-quad resize handles on the active clip box.
|
||||
|
||||
Renders six near-invisible click-target quads and six colored edge
|
||||
|
||||
@@ -987,7 +987,7 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
try:
|
||||
import typst # noqa: F401
|
||||
import typst # ruff: ignore[unused-import]
|
||||
|
||||
return True
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -313,7 +313,7 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
failures.append(element)
|
||||
print("***** FAILURE *****")
|
||||
if shape:
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
geom = shape.geometry
|
||||
print(
|
||||
f"Success {time.time() - start:.3f}s "
|
||||
|
||||
@@ -28,7 +28,7 @@ operators via ``target_set_operator``; drag handles inherit modal state
|
||||
from ``GizmoMovable``.
|
||||
"""
|
||||
|
||||
__all__ = [ # noqa: RUF022 (unsorted `__all__`)
|
||||
__all__ = [ # ruff: ignore[unsorted-dunder-all]
|
||||
"GizmoColor",
|
||||
"GizmoAxis",
|
||||
"TextAlignment",
|
||||
@@ -5660,7 +5660,7 @@ class BaseParametricGizmoGroup:
|
||||
"""
|
||||
return 0.0
|
||||
|
||||
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None: # noqa: ARG002
|
||||
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None:
|
||||
"""Update overall_width, overall_height, and lining_offset based on view direction.
|
||||
|
||||
This base implementation handles the common pattern for door/window gizmos.
|
||||
@@ -5837,7 +5837,7 @@ class BaseParametricGizmoGroup:
|
||||
self.update_dimension_gizmos(mw, props)
|
||||
self._refresh_element_specific(context, mw, props)
|
||||
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None: # noqa: ARG002
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
|
||||
"""Override for element-specific refresh logic.
|
||||
|
||||
Called from both refresh() (on state change) and draw_prepare() (per frame),
|
||||
@@ -6344,7 +6344,7 @@ class BaseParametricGizmoGroup:
|
||||
"""
|
||||
return (0.0, 0.0)
|
||||
|
||||
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float: # noqa: ARG002
|
||||
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float:
|
||||
"""Get Y offset for icons based on view direction.
|
||||
|
||||
Uses get_icon_y_extent() to determine how far to offset icons based on
|
||||
@@ -6546,9 +6546,7 @@ class BaseParametricGizmoGroup:
|
||||
|
||||
self._refresh_element_specific(context, mw, props)
|
||||
|
||||
def _update_dimension_gizmo_positions(
|
||||
self, context: bpy.types.Context, mw: "Matrix", props # noqa: ARG002
|
||||
) -> None:
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
|
||||
"""Update dimension gizmo positions based on view direction.
|
||||
|
||||
Override this method in subclasses to implement view-dependent
|
||||
|
||||
@@ -1406,31 +1406,28 @@ class CreateDrawing(bpy.types.Operator):
|
||||
# Backwards compatibility with older ifcopenshell builds that don't expose these keys.
|
||||
pass
|
||||
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
|
||||
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
|
||||
self.serialiser_settings.set("svg-without-storeys", True)
|
||||
self.serialiser_settings.set("svg-write-poly", True)
|
||||
self.serialiser_settings.set("svg-poly", True)
|
||||
self.svg_settings.set("svg-without-storeys", True)
|
||||
self.svg_settings.set("svg-write-poly", True)
|
||||
self.svg_settings.set("svg-poly", True)
|
||||
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
|
||||
self.serialiser_settings.set("profile-threshold", 10000)
|
||||
self.serialiser_settings.set("svg-xmlns", True)
|
||||
self.serialiser_settings.set("svg-project", True)
|
||||
self.serialiser_settings.set("auto-elevation", False)
|
||||
self.serialiser_settings.set("auto-section", False)
|
||||
self.serialiser_settings.set("print-space-names", False)
|
||||
self.serialiser_settings.set("print-space-areas", False)
|
||||
self.serialiser_settings.set("door-arcs", False)
|
||||
self.serialiser_settings.set("svg-no-css", True)
|
||||
self.serialiser_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
|
||||
self.serialiser_settings.set("scale", str(self.scale))
|
||||
self.serialiser_settings.set("svg-subtract-before", "always")
|
||||
self.serialiser_settings.set("svg-prefilter", True) # See #3359
|
||||
self.serialiser_settings.set("svg-unify-inputs", True)
|
||||
self.serialiser_settings.set("svg-segment-projection", True)
|
||||
self.svg_settings.set("profile-threshold", 10000)
|
||||
self.svg_settings.set("svg-xmlns", True)
|
||||
self.svg_settings.set("svg-project", True)
|
||||
self.svg_settings.set("auto-elevation", False)
|
||||
self.svg_settings.set("auto-section", False)
|
||||
self.svg_settings.set("print-space-names", False)
|
||||
self.svg_settings.set("print-space-areas", False)
|
||||
self.svg_settings.set("door-arcs", False)
|
||||
self.svg_settings.set("svg-no-css", True)
|
||||
self.svg_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
|
||||
self.svg_settings.set("scale", str(self.scale))
|
||||
self.svg_settings.set("svg-subtract-before", "always")
|
||||
self.svg_settings.set("svg-prefilter", True) # See #3359
|
||||
self.svg_settings.set("svg-unify-inputs", True)
|
||||
self.svg_settings.set("svg-segment-projection", True)
|
||||
if target_view == "REFLECTED_PLAN_VIEW":
|
||||
self.serialiser_settings.set("svg-mirror-y", True)
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(
|
||||
self.svg_buffer, self.svg_settings, self.serialiser_settings
|
||||
)
|
||||
self.svg_settings.set("svg-mirror-y", True)
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
|
||||
# tree = ifcopenshell.geom.tree()
|
||||
# This instructs the tree to explode BReps into faces and return
|
||||
# the style of the face when running tree.select_ray()
|
||||
|
||||
@@ -50,11 +50,13 @@ 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
|
||||
global diagram_scales_enum, diagram_scales_enum_system
|
||||
diagram_scales_enum = []
|
||||
diagram_scales_enum_system = None
|
||||
|
||||
|
||||
def update_target_view_doc(self: "DocProperties", context: bpy.types.Context) -> None:
|
||||
@@ -123,14 +125,12 @@ 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
|
||||
global diagram_scales_enum, diagram_scales_enum_system
|
||||
assert context.scene
|
||||
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":
|
||||
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":
|
||||
diagram_scales_enum = [
|
||||
("CUSTOM", "Custom", ""),
|
||||
("1'=1'-0\"|1/1", "1'=1'-0\"", ""),
|
||||
@@ -144,21 +144,21 @@ def get_diagram_scales(self: "BIMCameraProperties", context: bpy.types.Context)
|
||||
('1/4"=1\'-0"|1/48', '1/4"=1\'-0"', ""),
|
||||
('3/16"=1\'-0"|1/64', '3/16"=1\'-0"', ""),
|
||||
('1/8"=1\'-0"|1/96', '1/8"=1\'-0"', ""),
|
||||
("1\"=10'|1/120", "1\"=10'", ""),
|
||||
('3/32"=1\'-0"|1/128', '3/32"=1\'-0"', ""),
|
||||
('1/16"=1\'-0"|1/192', '1/16"=1\'-0"', ""),
|
||||
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
|
||||
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
|
||||
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
|
||||
("1\"=10'|1/120", "1\"=10'", ""),
|
||||
("1\"=20'|1/240", "1\"=20'", ""),
|
||||
("1\"=30'|1/360", "1\"=30'", ""),
|
||||
('1/32"=1\'-0"|1/384', '1/32"=1\'-0"', ""),
|
||||
("1\"=40'|1/480", "1\"=40'", ""),
|
||||
("1\"=50'|1/600", "1\"=50'", ""),
|
||||
("1\"=60'|1/720", "1\"=60'", ""),
|
||||
('1/64"=1\'-0"|1/768', '1/64"=1\'-0"', ""),
|
||||
("1\"=70'|1/840", "1\"=70'", ""),
|
||||
("1\"=80'|1/960", "1\"=80'", ""),
|
||||
("1\"=90'|1/1080", "1\"=90'", ""),
|
||||
("1\"=100'|1/1200", "1\"=100'", ""),
|
||||
('1/128"=1\'-0"|1/1536', '1/128"=1\'-0"', ""),
|
||||
("1\"=150'|1/1800", "1\"=150'", ""),
|
||||
("1\"=200'|1/2400", "1\"=200'", ""),
|
||||
("1\"=300'|1/3600", "1\"=300'", ""),
|
||||
|
||||
@@ -72,11 +72,10 @@ class ExportOBJ(bpy.types.Operator):
|
||||
# Conversion from IFC to OBJ
|
||||
# Settings for obj
|
||||
settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS)
|
||||
settings.set("apply-default-materials", True)
|
||||
serializer_settings.set("use-element-guids", True)
|
||||
settings.set("use-element-guids", True)
|
||||
settings.set("use-world-coords", True)
|
||||
|
||||
ifc_file: ifcopenshell.file
|
||||
@@ -90,7 +89,7 @@ class ExportOBJ(bpy.types.Operator):
|
||||
obj_file_path = os.path.join(output_dir, "model.obj")
|
||||
mtl_file_path = os.path.join(output_dir, "model.mtl")
|
||||
|
||||
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings)
|
||||
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings)
|
||||
serialiser.setFile(ifc_file)
|
||||
serialiser.setUnitNameAndMagnitude("METER", 1.0)
|
||||
serialiser.writeHeader()
|
||||
@@ -107,7 +106,7 @@ class ExportOBJ(bpy.types.Operator):
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
materials = shape.geometry.materials
|
||||
|
||||
for material in materials:
|
||||
|
||||
@@ -430,7 +430,7 @@ class SverchokData:
|
||||
@classmethod
|
||||
def has_sverchok(cls) -> bool:
|
||||
try:
|
||||
import sverchok # noqa: F401
|
||||
import sverchok # ruff: ignore[unused-import]
|
||||
|
||||
return True
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -560,7 +560,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
update_door_modifier_representation(obj)
|
||||
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
|
||||
continue
|
||||
@@ -638,7 +638,7 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.remove_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -683,7 +683,7 @@ class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
@@ -909,9 +909,7 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
setattr(self, f"gizmo_swing_arc_{cfg.name}", main)
|
||||
setattr(self, f"gizmo_swing_arc_{cfg.name}_flip", flip)
|
||||
|
||||
def _refresh_element_specific(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties" # noqa: ARG002
|
||||
) -> None:
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties") -> None:
|
||||
"""Update door-specific swing arc gizmos."""
|
||||
self.update_swing_gizmos(mw, props)
|
||||
|
||||
|
||||
@@ -765,7 +765,7 @@ class GizmoRoofEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
def is_element_type(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||
return tool.Parametric.is_roof(element)
|
||||
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw, props) -> None: # noqa: ARG002
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw, props) -> None:
|
||||
"""Anchor every dimension gizmo at the object origin. Each gizmo's
|
||||
declared axis (height/slope along +Z, thickness along -Z) separates
|
||||
them in 3D so they don't visually collide despite sharing a
|
||||
@@ -776,7 +776,7 @@ class GizmoRoofEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
self.set_dimension_gizmo_position("angle", mw, origin, (0, 0, 1))
|
||||
self.set_dimension_gizmo_position("roof_thickness", mw, origin, (0, 0, -1))
|
||||
|
||||
def get_element_height(self, props) -> float: # noqa: ARG002
|
||||
def get_element_height(self, props) -> float:
|
||||
"""Object-local Z of the mesh's topmost vertex, so the pen / validate /
|
||||
cancel / cycle row anchors visibly above sloped or stepped roof
|
||||
bodies rather than at the parametric ``props.height`` which may not
|
||||
|
||||
@@ -405,7 +405,7 @@ class SetStairTreads(bpy.types.Operator):
|
||||
bl_label = "Set Number of Treads"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: # noqa: ARG002
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
@@ -658,9 +658,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
self.tread_count_label_gizmo.alpha = 0.8
|
||||
self.tread_count_label_gizmo.target_set_operator("bim.input_stair_treads")
|
||||
|
||||
def _refresh_element_specific(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
|
||||
) -> None:
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties") -> None:
|
||||
"""Update stair-specific lock and tread count gizmos. Lock positioning is
|
||||
handled per-frame in the dimension-positioning hook."""
|
||||
self.update_lock_gizmo(props)
|
||||
@@ -707,7 +705,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
self.update_gizmo_visibility(self.tread_count_label_gizmo, props.is_editing)
|
||||
|
||||
def _update_dimension_gizmo_positions(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties"
|
||||
) -> None:
|
||||
"""Update dimension gizmo positions based on camera view direction."""
|
||||
viewing_from_negative_y, viewing_from_negative_x = self._frame_view_dir
|
||||
|
||||
@@ -2174,7 +2174,7 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
return (far, near)
|
||||
|
||||
def _update_dimension_gizmo_positions(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties" # noqa: ARG002
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties"
|
||||
) -> None:
|
||||
"""Re-position length / height / height_end dimensions to the camera-facing
|
||||
Y-side of the wall every frame. Mirrors the door & stair pattern: when the
|
||||
@@ -2530,7 +2530,7 @@ def _perpendicular_wall_params(
|
||||
return clamped_x, abs(cursor_local_y), side_sign
|
||||
|
||||
|
||||
def _commit_pending_wall_edits_for_selection(context: bpy.types.Context) -> None: # noqa: ARG001
|
||||
def _commit_pending_wall_edits_for_selection(context: bpy.types.Context) -> None:
|
||||
"""Thin wall-scoped alias for ``tool.Parametric.commit_pending_edits_for_selection``.
|
||||
|
||||
Encapsulates the ``names=("wall",)`` filter so the registry name is
|
||||
|
||||
@@ -538,7 +538,7 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Remove Window"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
@@ -2442,7 +2442,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
if iterator.initialize():
|
||||
while True: # Main loop.
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
results.add(self.file.by_id(shape.id))
|
||||
geometry = shape.geometry
|
||||
|
||||
@@ -2518,7 +2518,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
||||
print("Finished", time.time() - start)
|
||||
return {"FINISHED"}
|
||||
|
||||
def process_occurrence(self, shape: W.TriangulationElement) -> None:
|
||||
def process_occurrence(self, shape: W.triangulation_element) -> None:
|
||||
element = self.file.by_id(shape.id)
|
||||
|
||||
mat = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||
|
||||
@@ -558,7 +558,7 @@ class IntegerInputDialogMixin:
|
||||
return None
|
||||
return props
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: # noqa: ARG002
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
props = self._resolve_props(context)
|
||||
if props is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -142,8 +142,35 @@ def assign_material(
|
||||
else:
|
||||
element_material_type = material_type
|
||||
|
||||
ifc.run("material.assign_material", products=[element], type=element_material_type, material=material)
|
||||
assigned_material = material_tool.get_material(element)
|
||||
# TODO: this whole dance is a stopgap and wants rewriting.
|
||||
#
|
||||
# material.assign_material creates material sets with no items in them,
|
||||
# ignoring the material it was handed -- an IfcMaterialLayerSet with no
|
||||
# MaterialLayers is not valid IFC, since the list is mandatory and
|
||||
# [1:?]. So we repair it below, after the fact. Worse, the API rejects a
|
||||
# plain IfcMaterial outright when asked for a usage, which is exactly
|
||||
# what the Object Materials dropdown gives us, so we cannot even pass it
|
||||
# on and have to let the API invent an empty set and then fill it in.
|
||||
#
|
||||
# The fix is for assign_material to build the set around the material it
|
||||
# is given, rather than leaving an invalid one behind for its callers to
|
||||
# patch up. That is a wider change than it looks: add_material_set has
|
||||
# the same behaviour, and the create-empty-then-add-items idiom is
|
||||
# spread through the API's own docstrings, examples and tests. Until
|
||||
# that is untangled, keep the repair here where it is at least visible.
|
||||
|
||||
# Only a usage refuses a plain IfcMaterial; every other type still wants
|
||||
# it, and IfcMaterial and IfcMaterialList cannot be created without it.
|
||||
pass_material = material_tool.is_a_material_set(material) or not element_material_type.endswith("Usage")
|
||||
ifc.run(
|
||||
"material.assign_material",
|
||||
products=[element],
|
||||
type=element_material_type,
|
||||
material=material if pass_material else None,
|
||||
)
|
||||
# A usage points at the set rather than being one, and it is the set
|
||||
# that needs an item adding to it below.
|
||||
assigned_material = material_tool.get_material(element, should_skip_usage=True)
|
||||
assert assigned_material # Type checker.
|
||||
|
||||
if material_tool.is_a_material_set(material):
|
||||
|
||||
@@ -651,7 +651,7 @@ class Material:
|
||||
def get_default_material(cls): pass
|
||||
def get_elements_by_material(cls, material): pass
|
||||
def get_material_attributes(cls): pass
|
||||
def get_material(cls, element, should_inherit: bool = False): pass
|
||||
def get_material(cls, element, should_inherit: bool = False, should_skip_usage: bool = False): pass
|
||||
def get_object_ui_active_material(cls): pass
|
||||
def get_object_ui_material_type(cls): pass
|
||||
def get_style(cls, material): pass
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Ignore unused imports.
|
||||
# ruff: noqa: F401
|
||||
# ruff: file-ignore[unused-import]
|
||||
|
||||
from bonsai.tool.aggregate import Aggregate
|
||||
from bonsai.tool.array import Array
|
||||
|
||||
@@ -25,6 +25,7 @@ import importlib
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -1756,6 +1757,7 @@ class Blender(bonsai.core.tool.Blender):
|
||||
repo_path = repo.working_tree_dir
|
||||
assert repo_path
|
||||
version_ = (Path(repo_path) / "VERSION").read_text().strip()
|
||||
version_ = re.sub(r"[A-Za-z]+\d+$", "", version_)
|
||||
commit_date = bonsai.get_last_commit_date()
|
||||
assert commit_date
|
||||
commit_date = datetime.fromisoformat(commit_date)
|
||||
|
||||
@@ -1187,7 +1187,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
if iterator and iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
element = tool.Ifc.get().by_id(shape.id)
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
# It's possible that there will be multiple shapes for the same context,
|
||||
@@ -2179,7 +2179,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
item = tool.Ifc.get().by_id(props.ifc_definition_id)
|
||||
allowed_attributes = [
|
||||
a.name()
|
||||
for a in item.declaration().as_entity.all_attributes()
|
||||
for a in item.declaration.as_entity().all_attributes()
|
||||
if a.type_of_attribute()._is("IfcLengthMeasure")
|
||||
]
|
||||
|
||||
|
||||
@@ -872,7 +872,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
cls,
|
||||
element: ifcopenshell.entity_instance,
|
||||
representation: ifcopenshell.entity_instance,
|
||||
shape: W.TriangulationElement,
|
||||
shape: W.triangulation_element,
|
||||
) -> bpy.types.Camera:
|
||||
"""Create camera data.
|
||||
|
||||
@@ -1026,7 +1026,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
@classmethod
|
||||
def convert_geometry_to_mesh(
|
||||
cls,
|
||||
geometry: W.Triangulation,
|
||||
geometry: W.triangulation,
|
||||
mesh: bpy.types.Mesh,
|
||||
verts: Optional[npt.NDArray[np.float64]] = None,
|
||||
*,
|
||||
|
||||
@@ -220,9 +220,14 @@ class Material(bonsai.core.tool.Material):
|
||||
|
||||
@classmethod
|
||||
def get_material(
|
||||
cls, element: ifcopenshell.entity_instance, should_inherit: bool = False
|
||||
cls,
|
||||
element: ifcopenshell.entity_instance,
|
||||
should_inherit: bool = False,
|
||||
should_skip_usage: bool = False,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return ifcopenshell.util.element.get_material(element, should_inherit=should_inherit)
|
||||
return ifcopenshell.util.element.get_material(
|
||||
element, should_inherit=should_inherit, should_skip_usage=should_skip_usage
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_a_material_set(cls, material: ifcopenshell.entity_instance) -> bool:
|
||||
@@ -326,6 +331,10 @@ 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:
|
||||
|
||||
@@ -2459,7 +2459,7 @@ class Model(bonsai.core.tool.Model):
|
||||
polygons = {}
|
||||
for curve in curves:
|
||||
geometry = ifcopenshell.geom.create_shape(settings, curve)
|
||||
assert isinstance(geometry, W.Triangulation)
|
||||
assert isinstance(geometry, W.triangulation)
|
||||
v = ifcopenshell.util.shape.get_vertices(geometry, is_2d=True)
|
||||
v = np.round(v, 4) # Round to nearest 0.1mm, otherwise things like circles don't polygonise reliably
|
||||
edges = ifcopenshell.util.shape.get_edges(geometry)
|
||||
|
||||
@@ -53,7 +53,7 @@ class Profile(bonsai.core.tool.Profile):
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||
|
||||
assert isinstance(shape, W.Triangulation)
|
||||
assert isinstance(shape, W.triangulation)
|
||||
verts = ifcopenshell.util.shape.get_vertices(shape)
|
||||
if verts.size == 0:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
|
||||
|
||||
@@ -76,6 +76,10 @@ Release
|
||||
Notes:
|
||||
|
||||
- Typically all packages are released at once using the same version schema
|
||||
- ``VERSION`` uses Python/PEP 440-compatible spelling. For example, an alpha
|
||||
release may be ``0.9.0alpha0`` (canonicalized to ``0.9.0a0``); build scripts
|
||||
derive numeric-only and SemVer forms such as ``0.9.0`` and
|
||||
``0.9.0-alpha0`` where required.
|
||||
- The ``README.md`` badges can serve as a visual reference for what versions have been released
|
||||
- Corrective Release (if needed after a standard release):
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ exclude = ["test*"]
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
lint.extend-select = [
|
||||
"F401", # unused imports
|
||||
"unused-import", # unused imports
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
|
||||
@@ -42,6 +42,7 @@ markers =
|
||||
type
|
||||
unit
|
||||
void
|
||||
wall
|
||||
web
|
||||
|
||||
# Provide plugins explicitly, so it will be possible run tests with PYTEST_DISABLE_PLUGIN_AUTOLOAD.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pytest
|
||||
pytest-blender
|
||||
pytest-bdd
|
||||
fake-bpy-module-latest
|
||||
|
||||
@@ -45,10 +45,10 @@ for dep in dependencies:
|
||||
subprocess.check_call(command + [dep])
|
||||
|
||||
try:
|
||||
import pygments # noqa: F401
|
||||
import pytest # noqa: F401
|
||||
import pytest_bdd # noqa: F401
|
||||
import pytest_blender # noqa: F401
|
||||
import pygments # ruff: ignore[unused-import]
|
||||
import pytest # ruff: ignore[unused-import]
|
||||
import pytest_bdd # ruff: ignore[unused-import]
|
||||
import pytest_blender # ruff: ignore[unused-import]
|
||||
|
||||
print("Test dependency installation was successful!")
|
||||
except Exception as e:
|
||||
|
||||
@@ -163,32 +163,29 @@ class Drawer:
|
||||
|
||||
# self.svg_settings.set_deflection_tolerance(0.0001)
|
||||
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
|
||||
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
|
||||
self.serialiser_settings.set("svg-without-storeys", True)
|
||||
self.serialiser_settings.set("svg-write-poly", True)
|
||||
self.serialiser_settings.set("svg-poly", True)
|
||||
self.svg_settings.set("svg-without-storeys", True)
|
||||
self.svg_settings.set("svg-write-poly", True)
|
||||
self.svg_settings.set("svg-poly", True)
|
||||
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
|
||||
self.serialiser_settings.set("profile-threshold", 10000)
|
||||
self.serialiser_settings.set("svg-xmlns", True)
|
||||
self.serialiser_settings.set("svg-project", True)
|
||||
self.serialiser_settings.set("auto-elevation", False)
|
||||
self.serialiser_settings.set("auto-section", False)
|
||||
self.serialiser_settings.set("print-space-names", False)
|
||||
self.serialiser_settings.set("print-space-areas", False)
|
||||
self.serialiser_settings.set("door-arcs", False)
|
||||
self.serialiser_settings.set("svg-no-css", True)
|
||||
self.serialiser_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
|
||||
self.serialiser_settings.set("scale", "1/50")
|
||||
self.serialiser_settings.set("svg-subtract-before", "always")
|
||||
self.serialiser_settings.set("svg-prefilter", True) # See #3359
|
||||
# self.serialiser_settings.set("svg-prefilter", False) # See #3359
|
||||
self.serialiser_settings.set("svg-unify-inputs", True)
|
||||
self.serialiser_settings.set("svg-segment-projection", True)
|
||||
self.svg_settings.set("profile-threshold", 10000)
|
||||
self.svg_settings.set("svg-xmlns", True)
|
||||
self.svg_settings.set("svg-project", True)
|
||||
self.svg_settings.set("auto-elevation", False)
|
||||
self.svg_settings.set("auto-section", False)
|
||||
self.svg_settings.set("print-space-names", False)
|
||||
self.svg_settings.set("print-space-areas", False)
|
||||
self.svg_settings.set("door-arcs", False)
|
||||
self.svg_settings.set("svg-no-css", True)
|
||||
self.svg_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
|
||||
self.svg_settings.set("scale", "1/50")
|
||||
self.svg_settings.set("svg-subtract-before", "always")
|
||||
self.svg_settings.set("svg-prefilter", True) # See #3359
|
||||
# self.svg_settings.set("svg-prefilter", False) # See #3359
|
||||
self.svg_settings.set("svg-unify-inputs", True)
|
||||
self.svg_settings.set("svg-segment-projection", True)
|
||||
if target_view == "REFLECTED_PLAN_VIEW":
|
||||
self.serialiser_settings.set("svg-mirror-y", True)
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(
|
||||
self.svg_buffer, self.svg_settings, self.serialiser_settings
|
||||
)
|
||||
self.svg_settings.set("svg-mirror-y", True)
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(self.svg_buffer, self.svg_settings)
|
||||
self.serialiser.setFile(ifc)
|
||||
|
||||
|
||||
|
||||
@@ -72,12 +72,12 @@ Scenario: Add classification reference - object
|
||||
And I press "bim.add_classification"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.change_classification_level(parent_id={classification})"
|
||||
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
When I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
|
||||
When I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Change classification level
|
||||
@@ -88,8 +88,8 @@ Scenario: Change classification level
|
||||
And I press "bim.add_classification"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.change_classification_level(parent_id={classification})"
|
||||
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
@@ -104,8 +104,8 @@ Scenario: Disable editing classification references
|
||||
And I press "bim.add_classification"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.change_classification_level(parent_id={classification})"
|
||||
When I press "bim.disable_editing_classification_references"
|
||||
@@ -119,12 +119,12 @@ Scenario: Enable editing classification reference
|
||||
And I press "bim.add_classification"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.change_classification_level(parent_id={classification})"
|
||||
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
|
||||
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
When I press "bim.enable_editing_classification_reference(reference={reference})"
|
||||
Then nothing happens
|
||||
@@ -137,12 +137,12 @@ Scenario: Disable editing classification reference
|
||||
And I press "bim.add_classification"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.change_classification_level(parent_id={classification})"
|
||||
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
|
||||
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
And I press "bim.enable_editing_classification_reference(reference={reference})"
|
||||
When I press "bim.disable_editing_classification_reference"
|
||||
@@ -156,15 +156,15 @@ Scenario: Remove classification reference - object
|
||||
And I press "bim.add_classification"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.change_classification_level(parent_id={classification})"
|
||||
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
|
||||
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
And I press "bim.enable_editing_classification_reference(reference={reference})"
|
||||
When I press "bim.remove_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
|
||||
When I press "bim.remove_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Edit classification reference
|
||||
@@ -175,12 +175,12 @@ Scenario: Edit classification reference
|
||||
And I press "bim.add_classification"
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I set "scene.BIMRootProperties.ifc_product" to "IfcElement"
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.change_classification_level(parent_id={classification})"
|
||||
And the variable "reference" is "{classification_ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWallType/Cube', obj_type='Object')"
|
||||
And I press "bim.add_classification_reference(reference={reference}, obj='IfcWall/Cube', obj_type='Object')"
|
||||
And the variable "reference" is "{ifc}.by_type('IfcClassificationReference')[0].id()"
|
||||
And I press "bim.enable_editing_classification_reference(reference={reference})"
|
||||
When I press "bim.edit_classification_reference"
|
||||
|
||||
@@ -185,6 +185,7 @@ Scenario: Update representation - updating a layered extrusion
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
@@ -213,6 +214,7 @@ Scenario: Update representation - updating a profiled extrusion
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
@@ -416,6 +418,7 @@ Scenario: Override duplicate move - copying a layered extrusion
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
@@ -447,6 +450,7 @@ Scenario: Override duplicate move - copying a profiled extrusion
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
|
||||
@@ -121,6 +121,7 @@ Scenario: Assign material - material layer set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
Then the object "IfcWallType/Empty" does not have the material "Default"
|
||||
@@ -134,6 +135,7 @@ Scenario: Unassign material - material layer set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
When I press "bim.unassign_material"
|
||||
@@ -155,6 +157,7 @@ Scenario: Unassign material - removing inherited material
|
||||
And I press "bim.assign_class"
|
||||
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
|
||||
@@ -181,6 +184,7 @@ Scenario: Enable editing assigned material - material layer set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
When I press "bim.enable_editing_assigned_material"
|
||||
@@ -200,6 +204,7 @@ Scenario: Disable editing assigned material - material layer set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
@@ -220,6 +225,7 @@ Scenario: Edit assigned material - material layer set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
@@ -235,6 +241,7 @@ Scenario: Assign material - material profile set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
When I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
|
||||
And I press "bim.assign_material"
|
||||
Then the object "IfcWallType/Empty" does not have the material "Default"
|
||||
@@ -248,6 +255,7 @@ Scenario: Unassign material - material profile set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
|
||||
And I press "bim.assign_material"
|
||||
When I press "bim.unassign_material"
|
||||
@@ -267,6 +275,7 @@ Scenario: Enable editing assigned material - material profile set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
|
||||
And I press "bim.assign_material"
|
||||
When I press "bim.enable_editing_assigned_material"
|
||||
@@ -286,6 +295,7 @@ Scenario: Disable editing assigned material - material profile set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
@@ -306,6 +316,7 @@ Scenario: Edit assigned material - material profile set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
@@ -454,6 +465,7 @@ Scenario: Add material set layer
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
@@ -477,6 +489,7 @@ Scenario: Remove material set layer
|
||||
And I press "bim.assign_class"
|
||||
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
|
||||
|
||||
@@ -314,6 +314,12 @@ Scenario: Load project elements - auto offset of cartesian points
|
||||
Then the object "IfcBuildingElementProxy/NAME" is at "0,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations - disabled false origin mode
|
||||
# D, G and J have their geometry far from their placement, so each is
|
||||
# shifted onto one of its own verts to keep its precision. Which vert that
|
||||
# is comes from the geometry kernel and has changed before, so these assert
|
||||
# that the origin is on a vert rather than which one, and name verts rather
|
||||
# than origins. In automatic mode the model origin is picked the same way
|
||||
# and everything moves with it, so there they are relative to it.
|
||||
Given an empty Blender session
|
||||
And I press "bim.load_project(filepath='{cwd}/test/files/geolocation.ifc', is_advanced=True)"
|
||||
When I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED"
|
||||
@@ -326,13 +332,19 @@ Scenario: Load project elements - all georeferencing coordinate situations - dis
|
||||
And the object "IfcActuator/A" is at "7,3,0"
|
||||
And the object "IfcActuator/B" is at "6,1,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "13,4,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "13000,4000,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "15000,2000,1000"
|
||||
And the object "IfcActuator/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "15000,6000,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "17000,4000,1000"
|
||||
And the object "IfcActuator/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "11000,3000,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "13000,1000,1000"
|
||||
And the object "IfcActuator/K" is at "10,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations - automatic false origin mode
|
||||
@@ -342,24 +354,27 @@ Scenario: Load project elements - all georeferencing coordinate situations - aut
|
||||
When I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||
And I press "bim.load_project_elements"
|
||||
Then "scene.BIMGeoreferenceProperties.has_blender_offset" is "True"
|
||||
And "scene.BIMGeoreferenceProperties.model_origin" is "13000.0,4000.0,-1000.0"
|
||||
And "scene.BIMGeoreferenceProperties.blender_offset_x" is "13000.0"
|
||||
And "scene.BIMGeoreferenceProperties.blender_offset_y" is "4000.0"
|
||||
And "scene.BIMGeoreferenceProperties.blender_offset_z" is "-1000.0"
|
||||
And the model origin is on an object vertex
|
||||
And the object "IfcSite/My Site" is at "0,0,0"
|
||||
And the object "IfcBuilding/My Building" is at "0,0,0"
|
||||
And the object "IfcBuildingStorey/My Storey" is at "0,0,0"
|
||||
And the object "IfcActuator/A" is at "-6,-1,1"
|
||||
And the object "IfcActuator/B" is at "-7,-3,1"
|
||||
And the object "IfcActuator/A" is at "7,3,0" relative to the model origin at map coordinates "7000,3000,0"
|
||||
And the object "IfcActuator/B" is at "6,1,0" relative to the model origin at map coordinates "6000,1000,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "0,0,0"
|
||||
And the object "IfcActuator/E" is at "-7,-1,1"
|
||||
And the object "IfcActuator/F" is at "-10,-1,1"
|
||||
And the object "IfcActuator/G" is at "2,2,0"
|
||||
And the object "IfcActuator/H" is at "-4,-2,1"
|
||||
And the object "IfcActuator/I" is at "-10,-1,1"
|
||||
And the object "IfcActuator/J" is at "-2,-1,0"
|
||||
And the object "IfcActuator/K" is at "-3,-4,1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "13,4,-1" relative to the model origin at map coordinates "13000,4000,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "15,2,1" relative to the model origin at map coordinates "15000,2000,1000"
|
||||
And the object "IfcActuator/E" is at "6,3,0" relative to the model origin at map coordinates "6000,3000,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0" relative to the model origin at map coordinates "3000,3000,0"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "15,6,-1" relative to the model origin at map coordinates "15000,6000,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "17,4,1" relative to the model origin at map coordinates "17000,4000,1000"
|
||||
And the object "IfcActuator/H" is at "9,2,0" relative to the model origin at map coordinates "9000,2000,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0" relative to the model origin at map coordinates "3000,3000,0"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "11,3,-1" relative to the model origin at map coordinates "11000,3000,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "13,1,1" relative to the model origin at map coordinates "13000,1000,1000"
|
||||
And the object "IfcActuator/K" is at "10,0,0" relative to the model origin at map coordinates "10000,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations - manual false origin mode
|
||||
Given an empty Blender session
|
||||
@@ -379,23 +394,20 @@ Scenario: Load project elements - all georeferencing coordinate situations - man
|
||||
And the object "IfcActuator/A" is at "-3,3,0"
|
||||
And the object "IfcActuator/B" is at "-4,1,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "3,4,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "3,4,-1" at map coordinates "13000,4000,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "5,2,1" at map coordinates "15000,2000,1000"
|
||||
And the object "IfcActuator/E" is at "-4,3,0"
|
||||
And the object "IfcActuator/F" is at "-7,3,0"
|
||||
And the object "IfcActuator/G" is at "5,6,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "5,6,-1" at map coordinates "15000,6000,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "7,4,1" at map coordinates "17000,4000,1000"
|
||||
And the object "IfcActuator/H" is at "-1,2,0"
|
||||
And the object "IfcActuator/I" is at "-7,3,0"
|
||||
And the object "IfcActuator/J" is at "1,3,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "1,3,-1" at map coordinates "11000,3000,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "3,1,1" at map coordinates "13000,1000,1000"
|
||||
And the object "IfcActuator/K" is at "0,0,0"
|
||||
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
|
||||
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
|
||||
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
|
||||
And the object "IfcActuator/D" has a vertex at "3,2,-1"
|
||||
And the object "IfcActuator/D" has a vertex at "5,2,-1"
|
||||
And the object "IfcActuator/G" has a vertex at "5,4,-1"
|
||||
And the object "IfcActuator/G" has a vertex at "7,4,-1"
|
||||
And the object "IfcActuator/J" has a vertex at "1,1,-1"
|
||||
And the object "IfcActuator/J" has a vertex at "3,1,-1"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - disabled false origin mode
|
||||
Given an empty Blender session
|
||||
@@ -410,13 +422,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "5.985,14.71,0"
|
||||
And the object "IfcActuator/B" is at "5.5367,12.519,0"
|
||||
And the object "IfcActuator/C" is at "0,10,0"
|
||||
And the object "IfcActuator/D" is at "11.522,17.228,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "11.5218,17.2284,-1" at map coordinates "11521.758,17228.35,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "13.9712,15.8141,1" at map coordinates "13971.246,15814.136,1000"
|
||||
And the object "IfcActuator/E" is at "5.0191,14.451,0"
|
||||
And the object "IfcActuator/F" is at "2.1213,13.674,0"
|
||||
And the object "IfcActuator/G" is at "12.936,19.678,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "12.936,19.6778,-1" at map coordinates "12935.975,19677.841,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "15.3855,18.2636,1" at map coordinates "15385.465,18263.627,1000"
|
||||
And the object "IfcActuator/H" is at "8.1757,14.261,0"
|
||||
And the object "IfcActuator/I" is at "2.1213,13.674,0"
|
||||
And the object "IfcActuator/J" is at "9.8487,15.745,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "9.8487,15.7448,-1" at map coordinates "9848.726,15744.786,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "12.2982,14.3306,1" at map coordinates "12298.216,14330.573,1000"
|
||||
And the object "IfcActuator/K" is at "9.6593,12.588,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - automatic false origin mode
|
||||
@@ -436,13 +454,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "7,3,0"
|
||||
And the object "IfcActuator/B" is at "6,1,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "13,4,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "11521.758,17228.35,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "13971.246,15814.136,1000"
|
||||
And the object "IfcActuator/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "12935.975,19677.841,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "15385.465,18263.627,1000"
|
||||
And the object "IfcActuator/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "9848.726,15744.786,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "12298.216,14330.573,1000"
|
||||
And the object "IfcActuator/K" is at "10,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - manual false origin mode
|
||||
@@ -463,23 +487,20 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "5.985,4.71,0"
|
||||
And the object "IfcActuator/B" is at "5.5367,2.519,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "11.522,7.228,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "11.5218,7.2284,-1" at map coordinates "11521.758,17228.35,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "13.9712,5.8141,1" at map coordinates "13971.246,15814.136,1000"
|
||||
And the object "IfcActuator/E" is at "5.0191,4.451,0"
|
||||
And the object "IfcActuator/F" is at "2.1213,3.674,0"
|
||||
And the object "IfcActuator/G" is at "12.936,9.678,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "12.936,9.6778,-1" at map coordinates "12935.975,19677.841,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "15.3855,8.2636,1" at map coordinates "15385.465,18263.627,1000"
|
||||
And the object "IfcActuator/H" is at "8.1757,4.261,0"
|
||||
And the object "IfcActuator/I" is at "2.1213,3.674,0"
|
||||
And the object "IfcActuator/J" is at "9.8487,5.745,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "9.8487,5.7448,-1" at map coordinates "9848.726,15744.786,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "12.2982,4.3306,1" at map coordinates "12298.216,14330.573,1000"
|
||||
And the object "IfcActuator/K" is at "9.6593,2.588,0"
|
||||
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
|
||||
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
|
||||
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
|
||||
And the object "IfcActuator/D" has a vertex at "12.039,5.296,-1"
|
||||
And the object "IfcActuator/D" has a vertex at "13.971,5.814,-1"
|
||||
And the object "IfcActuator/G" has a vertex at "13.454,7.746,-1"
|
||||
And the object "IfcActuator/G" has a vertex at "15.385,8.264,-1"
|
||||
And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1"
|
||||
And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with an offset site - manual false origin mode - with custom project north
|
||||
Given an empty Blender session
|
||||
@@ -500,13 +521,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "7,3,0"
|
||||
And the object "IfcActuator/B" is at "6,1,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "13,4,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "11521.758,17228.35,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "13971.246,15814.136,1000"
|
||||
And the object "IfcActuator/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "12935.975,19677.841,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "15385.465,18263.627,1000"
|
||||
And the object "IfcActuator/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "9848.726,15744.786,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "12298.216,14330.573,1000"
|
||||
And the object "IfcActuator/K" is at "10,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - disabled false origin mode (this should be identical to the situation with no map conversion)
|
||||
@@ -522,13 +549,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "7,3,0"
|
||||
And the object "IfcActuator/B" is at "6,1,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "13,4,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "28000,4000,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "30000,2000,1000"
|
||||
And the object "IfcActuator/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "30000,6000,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "32000,4000,1000"
|
||||
And the object "IfcActuator/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "26000,3000,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "28000,1000,1000"
|
||||
And the object "IfcActuator/K" is at "10,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - automatic false origin mode (this should affect the Blender eastings and northings, which is now different to the Blender offset XYZ, but is otherwise identical to the non-map conversion variant)
|
||||
@@ -538,24 +571,27 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
When I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||
And I press "bim.load_project_elements"
|
||||
Then "scene.BIMGeoreferenceProperties.has_blender_offset" is "True"
|
||||
And "scene.BIMGeoreferenceProperties.model_origin" is "28000.0,4000.0,-1000.0"
|
||||
And "scene.BIMGeoreferenceProperties.blender_offset_x" is "13000.0"
|
||||
And "scene.BIMGeoreferenceProperties.blender_offset_y" is "4000.0"
|
||||
And "scene.BIMGeoreferenceProperties.blender_offset_z" is "-1000.0"
|
||||
And the model origin is on an object vertex
|
||||
And the object "IfcSite/My Site" is at "0,0,0"
|
||||
And the object "IfcBuilding/My Building" is at "0,0,0"
|
||||
And the object "IfcBuildingStorey/My Storey" is at "0,0,0"
|
||||
And the object "IfcActuator/A" is at "-6,-1,1"
|
||||
And the object "IfcActuator/B" is at "-7,-3,1"
|
||||
And the object "IfcActuator/A" is at "22,3,0" relative to the model origin at map coordinates "22000,3000,0"
|
||||
And the object "IfcActuator/B" is at "21,1,0" relative to the model origin at map coordinates "21000,1000,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "0,0,0"
|
||||
And the object "IfcActuator/E" is at "-7,-1,1"
|
||||
And the object "IfcActuator/F" is at "-10,-1,1"
|
||||
And the object "IfcActuator/G" is at "2,2,0"
|
||||
And the object "IfcActuator/H" is at "-4,-2,1"
|
||||
And the object "IfcActuator/I" is at "-10,-1,1"
|
||||
And the object "IfcActuator/J" is at "-2,-1,0"
|
||||
And the object "IfcActuator/K" is at "-3,-4,1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "28,4,-1" relative to the model origin at map coordinates "28000,4000,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "30,2,1" relative to the model origin at map coordinates "30000,2000,1000"
|
||||
And the object "IfcActuator/E" is at "21,3,0" relative to the model origin at map coordinates "21000,3000,0"
|
||||
And the object "IfcActuator/F" is at "18,3,0" relative to the model origin at map coordinates "18000,3000,0"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "30,6,-1" relative to the model origin at map coordinates "30000,6000,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "32,4,1" relative to the model origin at map coordinates "32000,4000,1000"
|
||||
And the object "IfcActuator/H" is at "24,2,0" relative to the model origin at map coordinates "24000,2000,0"
|
||||
And the object "IfcActuator/I" is at "18,3,0" relative to the model origin at map coordinates "18000,3000,0"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "26,3,-1" relative to the model origin at map coordinates "26000,3000,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "28,1,1" relative to the model origin at map coordinates "28000,1000,1000"
|
||||
And the object "IfcActuator/K" is at "25,0,0" relative to the model origin at map coordinates "25000,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with a map conversion - manual false origin mode (this should affect the Blender eastings and northings, which is now different to the Blender offset XYZ, but is otherwise identical to the non-map conversion variant)
|
||||
Given an empty Blender session
|
||||
@@ -575,23 +611,20 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "-3,3,0"
|
||||
And the object "IfcActuator/B" is at "-4,1,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "3,4,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "3,4,-1" at map coordinates "28000,4000,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "5,2,1" at map coordinates "30000,2000,1000"
|
||||
And the object "IfcActuator/E" is at "-4,3,0"
|
||||
And the object "IfcActuator/F" is at "-7,3,0"
|
||||
And the object "IfcActuator/G" is at "5,6,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "5,6,-1" at map coordinates "30000,6000,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "7,4,1" at map coordinates "32000,4000,1000"
|
||||
And the object "IfcActuator/H" is at "-1,2,0"
|
||||
And the object "IfcActuator/I" is at "-7,3,0"
|
||||
And the object "IfcActuator/J" is at "1,3,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "1,3,-1" at map coordinates "26000,3000,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "3,1,1" at map coordinates "28000,1000,1000"
|
||||
And the object "IfcActuator/K" is at "0,0,0"
|
||||
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
|
||||
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
|
||||
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
|
||||
And the object "IfcActuator/D" has a vertex at "3,2,-1"
|
||||
And the object "IfcActuator/D" has a vertex at "5,2,-1"
|
||||
And the object "IfcActuator/G" has a vertex at "5,4,-1"
|
||||
And the object "IfcActuator/G" has a vertex at "7,4,-1"
|
||||
And the object "IfcActuator/J" has a vertex at "1,1,-1"
|
||||
And the object "IfcActuator/J" has a vertex at "3,1,-1"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - disabled false origin mode
|
||||
Given an empty Blender session
|
||||
@@ -606,13 +639,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "5.985,14.71,0"
|
||||
And the object "IfcActuator/B" is at "5.5367,12.519,0"
|
||||
And the object "IfcActuator/C" is at "0,10,0"
|
||||
And the object "IfcActuator/D" is at "11.522,17.228,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "11.5218,17.2284,-1" at map coordinates "26521.758,17228.35,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "13.9712,15.8141,1" at map coordinates "28971.246,15814.136,1000"
|
||||
And the object "IfcActuator/E" is at "5.0191,14.451,0"
|
||||
And the object "IfcActuator/F" is at "2.1213,13.674,0"
|
||||
And the object "IfcActuator/G" is at "12.936,19.678,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "12.936,19.6778,-1" at map coordinates "27935.975,19677.841,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "15.3855,18.2636,1" at map coordinates "30385.465,18263.627,1000"
|
||||
And the object "IfcActuator/H" is at "8.1757,14.261,0"
|
||||
And the object "IfcActuator/I" is at "2.1213,13.674,0"
|
||||
And the object "IfcActuator/J" is at "9.8487,15.745,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "9.8487,15.7448,-1" at map coordinates "24848.726,15744.786,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "12.2982,14.3306,1" at map coordinates "27298.216,14330.573,1000"
|
||||
And the object "IfcActuator/K" is at "9.6593,12.588,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - automatic false origin mode
|
||||
@@ -632,13 +671,19 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "7,3,0"
|
||||
And the object "IfcActuator/B" is at "6,1,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "13,4,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "13,4,-1" at map coordinates "26521.758,17228.35,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "15,2,1" at map coordinates "28971.246,15814.136,1000"
|
||||
And the object "IfcActuator/E" is at "6,3,0"
|
||||
And the object "IfcActuator/F" is at "3,3,0"
|
||||
And the object "IfcActuator/G" is at "15,6,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "15,6,-1" at map coordinates "27935.975,19677.841,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "17,4,1" at map coordinates "30385.465,18263.627,1000"
|
||||
And the object "IfcActuator/H" is at "9,2,0"
|
||||
And the object "IfcActuator/I" is at "3,3,0"
|
||||
And the object "IfcActuator/J" is at "11,3,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "11,3,-1" at map coordinates "24848.726,15744.786,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "13,1,1" at map coordinates "27298.216,14330.573,1000"
|
||||
And the object "IfcActuator/K" is at "10,0,0"
|
||||
|
||||
Scenario: Load project elements - all georeferencing coordinate situations with map conversion and an offset site - manual false origin mode
|
||||
@@ -659,23 +704,20 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
||||
And the object "IfcActuator/A" is at "5.985,4.71,0"
|
||||
And the object "IfcActuator/B" is at "5.5367,2.519,0"
|
||||
And the object "IfcActuator/C" is at "0,0,0"
|
||||
And the object "IfcActuator/D" is at "11.522,7.228,-1"
|
||||
And the object "IfcActuator/D" has its origin on a vertex
|
||||
And the object "IfcActuator/D" has a vert at "11.5218,7.2284,-1" at map coordinates "26521.758,17228.35,-1000"
|
||||
And the object "IfcActuator/D" has a vert at "13.9712,5.8141,1" at map coordinates "28971.246,15814.136,1000"
|
||||
And the object "IfcActuator/E" is at "5.0191,4.451,0"
|
||||
And the object "IfcActuator/F" is at "2.1213,3.674,0"
|
||||
And the object "IfcActuator/G" is at "12.936,9.678,-1"
|
||||
And the object "IfcActuator/G" has its origin on a vertex
|
||||
And the object "IfcActuator/G" has a vert at "12.936,9.6778,-1" at map coordinates "27935.975,19677.841,-1000"
|
||||
And the object "IfcActuator/G" has a vert at "15.3855,8.2636,1" at map coordinates "30385.465,18263.627,1000"
|
||||
And the object "IfcActuator/H" is at "8.1757,4.261,0"
|
||||
And the object "IfcActuator/I" is at "2.1213,3.674,0"
|
||||
And the object "IfcActuator/J" is at "9.8487,5.745,-1"
|
||||
And the object "IfcActuator/J" has its origin on a vertex
|
||||
And the object "IfcActuator/J" has a vert at "9.8487,5.7448,-1" at map coordinates "24848.726,15744.786,-1000"
|
||||
And the object "IfcActuator/J" has a vert at "12.2982,4.3306,1" at map coordinates "27298.216,14330.573,1000"
|
||||
And the object "IfcActuator/K" is at "9.6593,2.588,0"
|
||||
And the object "IfcActuator/D" has a cartesian point offset of "31,4,-1"
|
||||
And the object "IfcActuator/G" has a cartesian point offset of "-25,6,-1"
|
||||
And the object "IfcActuator/J" has a cartesian point offset of "11,3,-1"
|
||||
And the object "IfcActuator/D" has a vertex at "12.039,5.296,-1"
|
||||
And the object "IfcActuator/D" has a vertex at "13.971,5.814,-1"
|
||||
And the object "IfcActuator/G" has a vertex at "13.454,7.746,-1"
|
||||
And the object "IfcActuator/G" has a vertex at "15.385,8.264,-1"
|
||||
And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1"
|
||||
And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1"
|
||||
|
||||
Scenario: Link IFC - from an empty IFC project
|
||||
Given an empty IFC project
|
||||
|
||||
@@ -81,6 +81,7 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()"
|
||||
@@ -102,6 +103,7 @@ Scenario: Assign type - assign to a type with a material layer set, which automa
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()"
|
||||
@@ -125,6 +127,7 @@ Scenario: Assign type - assign to a different type with a LAYER2 material layer
|
||||
And I press "bim.assign_class"
|
||||
And the variable "type" is "{ifc}.by_type('IfcWallType')[-1].id()"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet"
|
||||
And I press "bim.assign_material"
|
||||
And I add an empty
|
||||
@@ -180,6 +183,7 @@ Scenario: Assign type - assign to a type with a material profile set
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
|
||||
And I press "bim.assign_class"
|
||||
And I press "bim.add_material()"
|
||||
And the object "IfcWallType/Empty" is selected
|
||||
And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialProfileSet"
|
||||
And I press "bim.assign_material"
|
||||
And I press "bim.enable_editing_assigned_material"
|
||||
|
||||
@@ -45,14 +45,14 @@ def test_text_formatter_defaults_to_none():
|
||||
|
||||
|
||||
def test_text_formatter_field_stores_callable():
|
||||
formatter = lambda props, value: f"{value:.2f}m" # noqa: E731
|
||||
formatter = lambda props, value: f"{value:.2f}m"
|
||||
config = DimensionGizmoConfig(attr_name="length", axis=(1, 0, 0), text_formatter=formatter)
|
||||
assert config.text_formatter is not None
|
||||
assert callable(config.text_formatter)
|
||||
|
||||
|
||||
def test_text_formatter_receives_props_and_value():
|
||||
formatter = lambda props, value: f"{props.label}={value}" # noqa: E731
|
||||
formatter = lambda props, value: f"{props.label}={value}"
|
||||
config = DimensionGizmoConfig(attr_name="length", axis=(1, 0, 0), text_formatter=formatter)
|
||||
props = SimpleNamespace(label="L")
|
||||
assert config.text_formatter(props, 3.14) == "L=3.14"
|
||||
|
||||
@@ -104,7 +104,7 @@ class TestParametricGizmoPollsHideDuringTransformModal:
|
||||
continue
|
||||
try:
|
||||
result = poll(bpy.context)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
|
||||
continue
|
||||
if result:
|
||||
|
||||
@@ -98,7 +98,7 @@ class TestWallGizmoGroupsHideDuringPreview:
|
||||
continue
|
||||
try:
|
||||
result = poll(bpy.context)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
|
||||
continue
|
||||
if result:
|
||||
|
||||
@@ -119,7 +119,7 @@ class TestWallGizmoGroupsHideOnArrayChildSelection:
|
||||
for name, cls in groups:
|
||||
try:
|
||||
result = cls.poll(bpy.context)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
|
||||
continue
|
||||
if result:
|
||||
@@ -159,7 +159,7 @@ class TestWallOperatorsRejectArrayChildSelection:
|
||||
for name, cls in ops:
|
||||
try:
|
||||
result = cls.poll(bpy.context)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
offenders.append((name, f"poll raised: {type(exc).__name__}: {exc}"))
|
||||
continue
|
||||
if result:
|
||||
|
||||
@@ -36,6 +36,7 @@ import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import numpy as np
|
||||
import pytest
|
||||
from mathutils import Vector
|
||||
@@ -1000,6 +1001,7 @@ def i_click_button_and_expect_error_error_msg(button, error_msg):
|
||||
|
||||
@given(parsers.parse('I evaluate expression "{expression}"'))
|
||||
@when(parsers.parse('I evaluate expression "{expression}"'))
|
||||
@then(parsers.parse('I evaluate expression "{expression}"'))
|
||||
def i_evaluate_expression(expression):
|
||||
expression = replace_variables(expression)
|
||||
exec(expression)
|
||||
@@ -1680,6 +1682,111 @@ def the_object_name_has_a_vertex_at_location(name, location):
|
||||
assert is_pass, f"No verts found at {location}: {verts}"
|
||||
|
||||
|
||||
def get_model_origin() -> Vector:
|
||||
"""Where the model was shifted to, in Blender units.
|
||||
|
||||
Geometry far from the origin is moved next to it so it keeps its precision,
|
||||
and the shift is recorded as the model origin. Which vert of which object it
|
||||
lands on is not something to depend on, so anything measured from it stays
|
||||
put even when that choice changes.
|
||||
"""
|
||||
props = bpy.context.scene.BIMGeoreferenceProperties
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(an_ifc_file_exists())
|
||||
return Vector([float(co) for co in props.model_origin.split(",")]) * unit_scale
|
||||
|
||||
|
||||
def get_world_verts(obj: bpy.types.Object) -> list[Vector]:
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh) and len(mesh.vertices), f"Object {obj.name} has no mesh"
|
||||
return [obj.matrix_world @ v.co for v in mesh.vertices]
|
||||
|
||||
|
||||
def assert_vert_at_map_coordinates(obj: bpy.types.Object, vert: Vector, coordinates: str) -> None:
|
||||
# Same conversion as the georeferencing calculator, which works in project
|
||||
# units rather than Blender ones.
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(an_ifc_file_exists())
|
||||
enh = Vector(tool.Georeference.xyz2enh(tuple(co / unit_scale for co in vert)))
|
||||
expected = Vector([float(co) for co in coordinates.split(",")])
|
||||
assert (enh - expected).length < 0.05, f"Vert {vert} is at map coordinates {enh[:]} instead of {coordinates}"
|
||||
|
||||
|
||||
@then(
|
||||
parsers.parse(
|
||||
'the object "{name}" is at "{location}" relative to the model origin at map coordinates "{coordinates}"'
|
||||
)
|
||||
)
|
||||
def the_object_name_is_at_location_relative_to_the_model_origin_at_map_coordinates(name, location, coordinates):
|
||||
"""For objects with no geometry to name a vert on.
|
||||
|
||||
The Blender location is only meaningful next to the origin everything was
|
||||
shifted by, since the two move together, but the map coordinates hold still
|
||||
either way.
|
||||
"""
|
||||
obj = the_object_name_exists(name)
|
||||
obj_location = obj.location + get_model_origin()
|
||||
assert (
|
||||
obj_location - Vector([float(co) for co in location.split(",")])
|
||||
).length < 0.05, f"Object is at {obj_location} relative to the model origin instead of {location}"
|
||||
assert_vert_at_map_coordinates(obj, obj.matrix_world.translation, coordinates)
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" has a vert at "{location}" at map coordinates "{coordinates}"'))
|
||||
def the_object_name_has_a_vert_at_location_at_map_coordinates(name, location, coordinates):
|
||||
"""Check where a vert sits in Blender and where it is in the world.
|
||||
|
||||
Both matter: the Blender location is what the user sees, and checking only
|
||||
the map coordinates would pass just as happily if the georeferencing maths
|
||||
or the offsets it reads were wrong, since the same maths produces both.
|
||||
"""
|
||||
obj = the_object_name_exists(name)
|
||||
target = Vector([float(co) for co in location.split(",")])
|
||||
verts = get_world_verts(obj)
|
||||
vert = next((v for v in verts if (v - target).length < 0.001), None)
|
||||
assert vert is not None, f"No vert found at {location}: {verts}"
|
||||
assert_vert_at_map_coordinates(obj, vert, coordinates)
|
||||
|
||||
|
||||
@then(
|
||||
parsers.parse(
|
||||
'the object "{name}" has a vert at "{location}" relative to the model origin at map coordinates "{coordinates}"'
|
||||
)
|
||||
)
|
||||
def the_object_name_has_a_vert_at_location_relative_to_the_model_origin_at_map_coordinates(name, location, coordinates):
|
||||
"""As above, for when the whole model has been shifted onto the origin.
|
||||
|
||||
Blender locations are then only meaningful relative to that origin, since
|
||||
everything moves together with it.
|
||||
"""
|
||||
obj = the_object_name_exists(name)
|
||||
target = Vector([float(co) for co in location.split(",")]) - get_model_origin()
|
||||
verts = get_world_verts(obj)
|
||||
vert = next((v for v in verts if (v - target).length < 0.001), None)
|
||||
assert vert is not None, f"No vert found at {location} relative to the model origin: {verts}"
|
||||
assert_vert_at_map_coordinates(obj, vert, coordinates)
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" has its origin on a vertex'))
|
||||
def the_object_name_has_its_origin_on_a_vertex(name):
|
||||
"""Far away geometry is shifted onto one of its own verts, which keeps the
|
||||
origin on the geometry and the local coordinates small enough to keep their
|
||||
precision. Which vert that is does not matter."""
|
||||
obj = the_object_name_exists(name)
|
||||
mesh = obj.data
|
||||
assert isinstance(mesh, bpy.types.Mesh) and len(mesh.vertices), f"Object {obj.name} has no mesh"
|
||||
nearest = min(v.co.length for v in mesh.vertices)
|
||||
assert nearest < 0.001, f"Object origin is {nearest} away from its nearest vert"
|
||||
|
||||
|
||||
@then("the model origin is on an object vertex")
|
||||
def the_model_origin_is_on_an_object_vertex():
|
||||
for obj in bpy.data.objects:
|
||||
if not isinstance(obj.data, bpy.types.Mesh):
|
||||
continue
|
||||
if any(v.length < 0.001 for v in get_world_verts(obj)):
|
||||
return
|
||||
assert False, "No object has a vert at the model origin"
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" has no scale'))
|
||||
def the_object_name_has_no_scale(name):
|
||||
assert the_object_name_exists(name).scale == Vector(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tool.ruff]
|
||||
extend = "../pyproject.toml"
|
||||
lint.ignore = [
|
||||
"F401", # unused imports
|
||||
"unused-import", # unused imports
|
||||
]
|
||||
|
||||
@@ -168,7 +168,7 @@ ifcopenshell_deploy_qt_runtime(BonsaiViewer)
|
||||
# them explicitly. (In a static build these are absent from lib/
|
||||
# and the glob just no-ops, so this rule is safe in both modes.)
|
||||
#
|
||||
# 2. Plug-ins (ifcopenshell.*.dylib, no `lib` prefix) — dlopen-only
|
||||
# 2. Plug-ins (ifcopenshell_*.dylib, no `lib` prefix) — dlopen-only
|
||||
# deps the plug-in loader resolves at runtime. macdeployqt has
|
||||
# no way to know about these.
|
||||
#
|
||||
@@ -177,7 +177,7 @@ ifcopenshell_deploy_qt_runtime(BonsaiViewer)
|
||||
# inside the bundle), so plug-ins and core libs both find each other
|
||||
# on the first probe.
|
||||
#
|
||||
# The geometry-writer filter drops ifcopenshell.geometry.writer.*.dylib
|
||||
# The geometry-writer filter drops ifcopenshell_geometry_writer_*.dylib
|
||||
# (the per-schema OBJ / glTF / DAE / STP / IGS / SVG / TTL export
|
||||
# converters — heavy, viewer-irrelevant). Mirrors the Rocky workflow's
|
||||
# filter in `stage_runtime_payload` (see 27249770e).
|
||||
@@ -195,7 +195,7 @@ if(APPLE)
|
||||
install(CODE [[
|
||||
set(_fw "${CMAKE_INSTALL_PREFIX}/BonsaiViewer.app/Contents/Frameworks")
|
||||
file(GLOB _ifc_dylibs "${CMAKE_INSTALL_PREFIX}/lib/*.dylib")
|
||||
list(FILTER _ifc_dylibs EXCLUDE REGEX "ifcopenshell\\.geometry\\.writer\\.")
|
||||
list(FILTER _ifc_dylibs EXCLUDE REGEX "ifcopenshell_geometry_writer_")
|
||||
if(_ifc_dylibs)
|
||||
message(STATUS "Staging IfcOpenShell dylibs (linked core + plug-ins) into BonsaiViewer.app/Contents/Frameworks")
|
||||
file(COPY ${_ifc_dylibs} DESTINATION "${_fw}")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user