mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-22 04:55:59 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 253a6c56a1 | |||
| 99f95bee6a | |||
| 6cae194610 | |||
| 8c3bc3b1ca | |||
| cb775b71fc | |||
| 053d8e9901 | |||
| a6f2396476 | |||
| c699c01e91 | |||
| 12c3dabc34 | |||
| a27c9ef01c | |||
| 8c5d33f9e5 | |||
| f63d02b2b4 | |||
| 44bf8527f6 | |||
| 6be06bb6c5 | |||
| 904a4df651 | |||
| ca866b3669 | |||
| 28219973f6 | |||
| 63fb5635ee | |||
| 4be91c26c4 | |||
| ca652f4534 | |||
| ecd90e45b7 | |||
| f0482e7f0c | |||
| 3faa192db8 | |||
| 5d5be1ea65 | |||
| 86844eedf7 | |||
| 6ab935216f | |||
| 2a5685a7d0 | |||
| 5309b4614c | |||
| ffe7296973 | |||
| c92451e0fc | |||
| 38dc24336e | |||
| d2d47efdd4 | |||
| 8a186b3d01 | |||
| 288b716574 | |||
| 95025ad4b1 | |||
| b0aa54b37b | |||
| 2d8c9561b8 | |||
| fd343188b4 | |||
| a9790a3f18 | |||
| 40d1c20bcc | |||
| 2fcb8c17c7 | |||
| ad9192027c | |||
| b787d76ad6 | |||
| 49ecfbf494 | |||
| 22a17cd287 | |||
| c35023de88 | |||
| 9b39dd629b | |||
| 11543f19ca | |||
| 0874c4e59c | |||
| d2f18709cb | |||
| 101c4716ea | |||
| 22c8aa1960 |
@@ -1,4 +1,5 @@
|
||||
Checks: 'bugprone-*,cert-*,clang-analyzer-*,readability-*'
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
AnalyzeTemporaryDtors: false
|
||||
FormatStyle: none
|
||||
+2
-4
@@ -19,8 +19,6 @@
|
||||
|
||||
|
||||
# normalize the line endings of the following files
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.cpp text
|
||||
*.css text
|
||||
*.csv text
|
||||
@@ -30,20 +28,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
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "pytest",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""Check (and by default fix) whitespace issues in tracked source files:
|
||||
- stray CR, e.g. 'hello\\rworld' -> 'helloworld'
|
||||
- line ending mismatch, e.g. 'hello\\r\\n' -> 'hello\\n' (or vice versa)
|
||||
- missing newline at end of file
|
||||
- extra newline(s) at end of file
|
||||
- trailing whitespace at end of line
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO, Literal, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class C:
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
CR = b"\r"
|
||||
CRLF = b"\r\n"
|
||||
LF = b"\n"
|
||||
|
||||
LineSeparator = Literal[b"\r\n", b"\n"]
|
||||
SYSTEM_LINE_SEPARATOR = cast(LineSeparator, os.linesep.encode())
|
||||
|
||||
|
||||
class Checker:
|
||||
def __init__(self, newline: LineSeparator = SYSTEM_LINE_SEPARATOR) -> None:
|
||||
self.newline = newline
|
||||
self.issues = 0
|
||||
|
||||
def report(self, label: str, issue: str) -> None:
|
||||
self.issues += 1
|
||||
print(f"{label}: {C.RED}{issue}{C.RESET}")
|
||||
|
||||
def check_stray_cr(self, filepath: Path, check: bool) -> None:
|
||||
with filepath.open("r+b") as f:
|
||||
self._check_stray_cr(f, str(filepath), check)
|
||||
|
||||
def _check_stray_cr(self, f: BinaryIO, label: str, check: bool) -> None:
|
||||
# a CR is "stray" if it isn't immediately followed by a LF, i.e. not part of a CRLF pair
|
||||
# CRLF/CR mismatch will be reported separately.
|
||||
stray_cr = re.compile(rb"\r(?!\n)")
|
||||
|
||||
content = f.read()
|
||||
matches = list(stray_cr.finditer(content))
|
||||
if not matches:
|
||||
return
|
||||
|
||||
line_numbers = dict.fromkeys(content.count(b"\n", 0, m.start()) + 1 for m in matches)
|
||||
for line_number in line_numbers:
|
||||
self.report(f"{label}:{line_number}", "stray carriage return")
|
||||
if check:
|
||||
return
|
||||
|
||||
f.seek(0)
|
||||
f.write(stray_cr.sub(b"", content))
|
||||
f.truncate()
|
||||
|
||||
def check_line_endings_mismatch(self, filepath: Path, check: bool) -> None:
|
||||
with filepath.open("r+b") as f:
|
||||
self._check_line_endings_mismatch(f, str(filepath), check)
|
||||
|
||||
def _check_line_endings_mismatch(self, f: BinaryIO, label: str, check: bool) -> None:
|
||||
NEWLINE = self.newline
|
||||
|
||||
def get_line_ending(line: bytes) -> LineSeparator | None:
|
||||
if line.endswith(CRLF):
|
||||
return CRLF
|
||||
if line.endswith(LF):
|
||||
return LF
|
||||
# last line with no trailing newline at all; check_eof_newline handles that
|
||||
return None
|
||||
|
||||
changed = False
|
||||
fixed_lines = []
|
||||
for line_number, line in enumerate(f, start=1):
|
||||
found = get_line_ending(line)
|
||||
if found in (NEWLINE, None):
|
||||
fixed_lines.append(line)
|
||||
continue
|
||||
|
||||
self.report(f"{label}:{line_number}", f"line ending mismatch (expected {NEWLINE!r}, found {found!r})")
|
||||
changed = True
|
||||
content = line[: -len(found)]
|
||||
fixed_lines.append(content + NEWLINE)
|
||||
|
||||
if changed and not check:
|
||||
f.seek(0)
|
||||
f.write(b"".join(fixed_lines))
|
||||
f.truncate()
|
||||
|
||||
def check_eof_newline(self, filepath: Path, check: bool) -> None:
|
||||
with filepath.open("r+b") as f:
|
||||
self._check_eof_newline(f, str(filepath), check)
|
||||
|
||||
def _check_eof_newline(self, f: BinaryIO, label: str, check: bool) -> None:
|
||||
NEWLINE = self.newline
|
||||
NEWLINE_SIZE = len(NEWLINE)
|
||||
|
||||
size = f.seek(0, os.SEEK_END)
|
||||
if size == 0:
|
||||
return
|
||||
|
||||
trailing_newlines = 0
|
||||
while True:
|
||||
pos = f.seek((-trailing_newlines - 1) * NEWLINE_SIZE, os.SEEK_END)
|
||||
if f.read(NEWLINE_SIZE) != NEWLINE:
|
||||
break
|
||||
trailing_newlines += 1
|
||||
if pos == 0:
|
||||
break
|
||||
|
||||
if trailing_newlines == 0:
|
||||
self.report(label, "missing newline at end of file")
|
||||
if check:
|
||||
return
|
||||
f.seek(0, os.SEEK_END)
|
||||
f.write(NEWLINE)
|
||||
elif trailing_newlines > 1:
|
||||
self.report(label, f"{trailing_newlines} trailing newlines at end of file")
|
||||
if check:
|
||||
return
|
||||
f.truncate(size - (trailing_newlines - 1) * NEWLINE_SIZE)
|
||||
|
||||
def check_trailing_whitespaces(self, filepath: Path, check: bool) -> None:
|
||||
with filepath.open("r+b") as f:
|
||||
self._check_trailing_whitespaces(f, str(filepath), check)
|
||||
|
||||
def _check_trailing_whitespaces(self, f: BinaryIO, label: str, check: bool) -> None:
|
||||
NEWLINE = self.newline
|
||||
NEWLINE_SIZE = len(NEWLINE)
|
||||
|
||||
changed = False
|
||||
fixed_lines = []
|
||||
for line_number, line in enumerate(f, start=1):
|
||||
has_newline = line.endswith(NEWLINE)
|
||||
content = line[:-NEWLINE_SIZE] if has_newline else line
|
||||
stripped = content.rstrip()
|
||||
if stripped != content:
|
||||
self.report(f"{label}:{line_number}", "trailing whitespace")
|
||||
changed = True
|
||||
fixed_lines.append(stripped + (NEWLINE if has_newline else b""))
|
||||
|
||||
if changed and not check:
|
||||
f.seek(0)
|
||||
f.write(b"".join(fixed_lines))
|
||||
f.truncate()
|
||||
|
||||
|
||||
CheckMethod = Callable[[Checker, BinaryIO, str, bool], None]
|
||||
|
||||
|
||||
class TestChecker:
|
||||
def _assert_check(
|
||||
self,
|
||||
method: CheckMethod,
|
||||
content: bytes,
|
||||
expected_issues: int,
|
||||
fixed: bytes,
|
||||
check: bool,
|
||||
line_ending: LineSeparator,
|
||||
*,
|
||||
transform: bool = True,
|
||||
) -> None:
|
||||
checker = Checker(line_ending)
|
||||
if line_ending == CRLF and transform:
|
||||
content = content.replace(LF, CRLF)
|
||||
fixed = fixed.replace(LF, CRLF)
|
||||
buffer = io.BytesIO(content)
|
||||
method(checker, buffer, "test", check)
|
||||
assert buffer.getvalue() == (content if check else fixed)
|
||||
assert checker.issues == expected_issues
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected_issues", "fixed"),
|
||||
(
|
||||
# OK
|
||||
(b"", 0, b""),
|
||||
(b"hello\n", 0, b"hello\n"),
|
||||
(b"line1\r\nline2\n", 0, b"line1\r\nline2\n"),
|
||||
# ERR
|
||||
(b"hello\rworld\n", 1, b"helloworld\n"),
|
||||
(b"a\rb\rc\n", 1, b"abc\n"),
|
||||
(b"hello\r", 1, b"hello"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("check", [False, True])
|
||||
def test_check_stray_cr(self, content: bytes, expected_issues: int, fixed: bytes, check: bool) -> None:
|
||||
# Don't parametrize by line endings, since in this case it doesn't matter.
|
||||
self._assert_check(Checker._check_stray_cr, content, expected_issues, fixed, check, LF)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected_issues", "fixed", "line_ending"),
|
||||
(
|
||||
# OK
|
||||
(b"", 0, b"", LF),
|
||||
(b"hello\n", 0, b"hello\n", LF),
|
||||
(b"hello\r\n", 0, b"hello\r\n", CRLF),
|
||||
# ERR
|
||||
(b"hello\r\n", 1, b"hello\n", LF),
|
||||
(b"a\nb\r\nc\n", 1, b"a\nb\nc\n", LF),
|
||||
(b"a\r\nb\r\n", 2, b"a\nb\n", LF),
|
||||
(b"hello\n", 1, b"hello\r\n", CRLF),
|
||||
(b"a\r\nb\nc\r\n", 1, b"a\r\nb\r\nc\r\n", CRLF),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("check", [False, True])
|
||||
def test_check_line_endings_mismatch(
|
||||
self, content: bytes, expected_issues: int, fixed: bytes, line_ending: LineSeparator, check: bool
|
||||
) -> None:
|
||||
self._assert_check(
|
||||
Checker._check_line_endings_mismatch, content, expected_issues, fixed, check, line_ending, transform=False
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected_issues", "fixed"),
|
||||
(
|
||||
# OK
|
||||
(b"", 0, b""),
|
||||
(b"hello\n", 0, b"hello\n"),
|
||||
# ERR
|
||||
(b"hello", 1, b"hello\n"),
|
||||
(b"hello\n\n\n", 1, b"hello\n"),
|
||||
(b"\n\n\n", 1, b"\n"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("check", [False, True])
|
||||
@pytest.mark.parametrize("line_ending", [LF, CRLF])
|
||||
def test_check_eof_newline(
|
||||
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
|
||||
) -> None:
|
||||
self._assert_check(Checker._check_eof_newline, content, expected_issues, fixed, check, line_ending)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected_issues", "fixed"),
|
||||
(
|
||||
# OK
|
||||
(b"", 0, b""),
|
||||
(b"hello\n", 0, b"hello\n"),
|
||||
(b"hello", 0, b"hello"),
|
||||
# ERR
|
||||
(b" ", 1, b""),
|
||||
(b"hello ", 1, b"hello"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("check", [False, True])
|
||||
@pytest.mark.parametrize("line_ending", [LF, CRLF])
|
||||
def test_check_trailing_whitespaces(
|
||||
self, content: bytes, expected_issues: int, fixed: bytes, check: bool, line_ending: LineSeparator
|
||||
) -> None:
|
||||
self._assert_check(Checker._check_trailing_whitespaces, content, expected_issues, fixed, check, line_ending)
|
||||
|
||||
@staticmethod
|
||||
def run_tests(extra_args: list[str] | None = None) -> None:
|
||||
pytest.main([__file__, *(extra_args or [])])
|
||||
|
||||
|
||||
def existing_path(value: str) -> Path:
|
||||
path = Path(value)
|
||||
if not path.exists():
|
||||
raise argparse.ArgumentTypeError(f"path not found: {value}")
|
||||
return path
|
||||
|
||||
|
||||
# Python files are covered by `black`.
|
||||
PATTERNS = (
|
||||
"*.cpp",
|
||||
"*.h",
|
||||
"*.i",
|
||||
)
|
||||
|
||||
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",)
|
||||
|
||||
|
||||
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())
|
||||
@@ -1,46 +0,0 @@
|
||||
# Lint/test gate for the bonsaiviewer-autodesk crate — nothing here ships.
|
||||
# The connector binary that reaches users is built by the platform pipelines
|
||||
# (build_rocky.yml, build_rocky_arm.yml, build_win.yml, build_osx.yml), each
|
||||
# of which runs packaging/build.py itself and bundles dist/autodesk/ into the
|
||||
# Bonsai Viewer archive.
|
||||
name: Test Bonsai Viewer Autodesk Connector
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'src/bonsaiviewer-autodesk/**'
|
||||
- '.github/workflows/build-bonsaiviewer-autodesk.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/bonsaiviewer-autodesk/**'
|
||||
- '.github/workflows/build-bonsaiviewer-autodesk.yml'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: cargo-test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/bonsaiviewer-autodesk
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# cargo-target reuse across runs. Massive cold-build speedup,
|
||||
# cheap on the GitHub Actions cache budget.
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src/bonsaiviewer-autodesk
|
||||
|
||||
- name: cargo fmt --check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: cargo test
|
||||
run: cargo test --all-features
|
||||
@@ -10,11 +10,10 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# x64 (Intel cross-compile) dropped while wgpu Qt is required:
|
||||
# the runner is arm64 so `brew --prefix qt` returns the arm64
|
||||
# prefix; we'd need a separate x86_64 Qt install under
|
||||
# /usr/local to cross-build BonsaiViewer. Revisit if Intel-Mac
|
||||
# demand resurfaces.
|
||||
- os: macos
|
||||
runner: macos-14
|
||||
arch: x64
|
||||
oldarch:
|
||||
- os: macos
|
||||
runner: macos-14
|
||||
arch: arm64
|
||||
@@ -35,28 +34,15 @@ 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
|
||||
# preinstalled: xz, cmake
|
||||
brew install git bison autoconf automake libffi findutils
|
||||
# qt brings in Qt6 + Svg; nix/build-all.py honours pre-set
|
||||
# QT_DIR so BonsaiViewer doesn't try to aqtinstall (which is
|
||||
# Linux-only).
|
||||
brew install qt
|
||||
echo "$(brew --prefix findutils)/libexec/gnubin" >> $GITHUB_PATH
|
||||
# Mac is using bison 2.5 by default, but we need 3.5+ for swig.
|
||||
echo "$(brew --prefix bison)/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: |
|
||||
python -m pip install awscli
|
||||
@@ -64,7 +50,7 @@ jobs:
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py unpack
|
||||
python ../nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
@@ -91,21 +77,8 @@ jobs:
|
||||
/usr/local/bin/brew install gettext openssl
|
||||
fi
|
||||
set -o pipefail
|
||||
export QT_DIR="$(brew --prefix qt)"
|
||||
# --shared mirrors build_rocky.yml after 27249770e: builds
|
||||
# IfcOpenShell as shared libs so each plug-in dylib references
|
||||
# libIfcParse / libIfcGeom via @rpath instead of statically
|
||||
# embedding them — the dominant size win for BonsaiViewer.app
|
||||
# (per-plugin libs go from ~30-50 MB to a few MB).
|
||||
#
|
||||
# IfcOpenShell-Python is back on after the ifcwrap rpath fix:
|
||||
# INSTALL_RPATH "$ORIGIN" is a Linux-ism that macOS dyld bakes
|
||||
# in as a literal string, so `@rpath/ifcopenshell.document.rdb
|
||||
# .dylib` failed to resolve at import time. ifcwrap now sets
|
||||
# INSTALL_RPATH to "@loader_path" on Apple.
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \
|
||||
BUILD_BONSAIVIEWER=ON QT_DIR="${QT_DIR}" \
|
||||
uv run ./nix/build-all.py -v --diskcleanup --ifcopenshell-shared ${MAC_INTEL} \
|
||||
python3 ./nix/build-all.py -v --diskcleanup ${MAC_INTEL} \
|
||||
| tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
@@ -122,7 +95,7 @@ jobs:
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd build
|
||||
uv run ../nix/cache_dependencies.py pack
|
||||
python ../nix/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
@@ -136,28 +109,8 @@ 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.
|
||||
uv run src/bonsaiviewer-autodesk/packaging/build.py
|
||||
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
|
||||
test -d "$autodesk_connector_dir"
|
||||
|
||||
cd ./build/`uname`/*/10.15/install/ifcopenshell
|
||||
mkdir -p ~/output
|
||||
install_root="$PWD"
|
||||
|
||||
stage_runtime_payload() {
|
||||
dest="$1"
|
||||
while IFS= read -r runtime_file; do
|
||||
cp -L "$runtime_file" "$dest/"
|
||||
done < <(
|
||||
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
|
||||
[ -d "$runtime_dir" ] || continue
|
||||
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
|
||||
done
|
||||
)
|
||||
}
|
||||
mkdir ~/output
|
||||
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
@@ -172,42 +125,18 @@ jobs:
|
||||
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
|
||||
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell/*
|
||||
mv *.zip ~/output
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
|
||||
exe=`basename "$exe_path"`
|
||||
package_dir="$install_root/.package-${exe}"
|
||||
rm -rf "$package_dir"
|
||||
mkdir -p "$package_dir"
|
||||
cp "$exe_path" "$package_dir/"
|
||||
stage_runtime_payload "$package_dir"
|
||||
pushd "$package_dir" > /dev/null
|
||||
zip -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" .
|
||||
popd > /dev/null
|
||||
rm -rf "$package_dir"
|
||||
done
|
||||
|
||||
# .app bundles (e.g. BonsaiViewer.app) live at the install-prefix
|
||||
# root because their install rule uses `BUNDLE DESTINATION "."` —
|
||||
# that's the layout Qt's macdeployqt expects. macdeployqt has
|
||||
# already embedded the Qt frameworks inside each bundle during
|
||||
# install/strip, so the only thing left to stage is the connector.
|
||||
find "$install_root" -maxdepth 1 -type d -name "*.app" | while read app_path; do
|
||||
app=`basename "$app_path" .app`
|
||||
if [ "$app" = "BonsaiViewer" ]; then
|
||||
# ConnectorDiscovery looks in applicationDirPath()/connectors,
|
||||
# which for a bundle is Contents/MacOS.
|
||||
mkdir -p "$app_path/Contents/MacOS/connectors"
|
||||
cp -a "$autodesk_connector_dir" "$app_path/Contents/MacOS/connectors/"
|
||||
fi
|
||||
pushd "$install_root" > /dev/null
|
||||
zip -qq -r "$HOME/output/${app}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" "$(basename "$app_path")"
|
||||
popd > /dev/null
|
||||
cd bin
|
||||
rm *.zip || true
|
||||
ls | while read exe; do
|
||||
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip $exe
|
||||
done
|
||||
mv *.zip ~/output
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -8,13 +8,6 @@ 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:
|
||||
@@ -33,7 +26,7 @@ jobs:
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
uv run ../IfcOpenShell/nix/cache_dependencies.py unpack
|
||||
python ../IfcOpenShell/nix/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
@@ -56,10 +49,23 @@ 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
|
||||
uv run ../IfcOpenShell/nix/cache_dependencies.py pack
|
||||
python ../IfcOpenShell/nix/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
@@ -70,28 +76,6 @@ 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:
|
||||
@@ -102,4 +86,3 @@ jobs:
|
||||
- name: Upload .zip archives to S3
|
||||
run: |
|
||||
aws s3 cp dist s3://ifcopenshell-builds/ --recursive --exclude "*" --include "*.whl"
|
||||
aws s3 cp dist-modular s3://ifcopenshell-builds/ --recursive --exclude "*" --include "*.zip"
|
||||
|
||||
@@ -19,29 +19,13 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
dnf update -y
|
||||
dnf install -y epel-release
|
||||
# --enablerepo=crb: libstdc++-static (libsupc++.a, needed by the
|
||||
# bundled FLTK link) lives in Rocky's CodeReady Builder repo, which
|
||||
# is disabled by default.
|
||||
dnf install -y --enablerepo=crb gcc gcc-c++ git autoconf automake bison make zip cmake \
|
||||
dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils xz byacc patchelf libxkbcommon-devel \
|
||||
dbus-devel \
|
||||
libXext-devel libXinerama-devel libXcursor-devel libXrender-devel \
|
||||
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
|
||||
findutils xz byacc
|
||||
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"
|
||||
|
||||
- name: Install aws cli
|
||||
run: |
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
||||
@@ -78,10 +62,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3" ADD_COMMIT_SHA=1 BUILD_CFG=Release BUILD_BONSAIVIEWER=ON \
|
||||
uv run --with aqtinstall ./nix/build-all.py \
|
||||
-v --diskcleanup --ifcopenshell-shared 2>&1 \
|
||||
| tee build.log
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
@@ -108,131 +89,11 @@ jobs:
|
||||
git push || true
|
||||
|
||||
- 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}"
|
||||
mkdir ~/output
|
||||
|
||||
if [ -z "${QT_DIR:-}" ]; then
|
||||
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
|
||||
if [ -d "$qt_candidate/lib" ]; then
|
||||
QT_DIR="$qt_candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Ensure that all shared libraries in provided dest `$1`
|
||||
# are present using their SONAMEs (at least as symlinks).
|
||||
ensure_soname_links() {
|
||||
dest="$1"
|
||||
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
|
||||
# TODO: actual pattern is "Library soname" instead of "Shared library"?
|
||||
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
|
||||
[ -n "$soname" ] || continue
|
||||
[ -e "$dest/$soname" ] && continue
|
||||
ln -s "$(basename "$shared_object")" "$dest/$soname"
|
||||
done
|
||||
}
|
||||
|
||||
# Copy all libs from `install/ifcopenshell` to the provided `$1`.
|
||||
# Set `$2` to `0` to skip including geometry writers.
|
||||
stage_runtime_payload() {
|
||||
dest="$1"
|
||||
include_geometry_writers="${2:-1}"
|
||||
while IFS= read -r runtime_file; do
|
||||
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
|
||||
continue
|
||||
fi
|
||||
cp -P "$runtime_file" "$dest/"
|
||||
done < <(
|
||||
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
|
||||
[ -d "$runtime_dir" ] || continue
|
||||
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
|
||||
done
|
||||
)
|
||||
ensure_soname_links "$dest"
|
||||
}
|
||||
|
||||
# Copy all libs from `QT_DIR` to the provided `$2`.
|
||||
stage_qt_runtime_payload() {
|
||||
exe_path="$1"
|
||||
dest="$2"
|
||||
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
|
||||
|
||||
# Skip executables that don't depend on QT (don't have `libQt6` referenced).
|
||||
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Copy all QT libs to `dest`.
|
||||
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
|
||||
ensure_soname_links "$dest"
|
||||
|
||||
# Copy QT plugins.
|
||||
if [ -d "$QT_DIR/plugins" ]; then
|
||||
pushd "$QT_DIR/plugins" > /dev/null
|
||||
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
|
||||
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
|
||||
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
|
||||
done
|
||||
popd > /dev/null
|
||||
# Point plugins rpath to `$dest`.
|
||||
if [ -d "$dest/plugins" ]; then
|
||||
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
|
||||
fi
|
||||
fi
|
||||
|
||||
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
|
||||
|
||||
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
|
||||
}
|
||||
|
||||
# Check all binaries in the dest `$1`
|
||||
# and report if they're still missing dependencies or are static.
|
||||
check_runtime_dependencies() {
|
||||
package_dir="$1"
|
||||
missing=0
|
||||
# Iterate over all .so files.
|
||||
while IFS= read -r binary_file; do
|
||||
# Skip non-binaries.
|
||||
readelf -h "$binary_file" >/dev/null 2>&1 || continue
|
||||
# Report non-dynamic binaries.
|
||||
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
|
||||
echo "ldd failed for $binary_file"
|
||||
cat "$package_dir/.ldd.out"
|
||||
missing=1
|
||||
continue
|
||||
fi
|
||||
# Report missing dependencies.
|
||||
if grep -q "not found" "$package_dir/.ldd.out"; then
|
||||
echo "Missing runtime dependencies for $binary_file"
|
||||
grep "not found" "$package_dir/.ldd.out"
|
||||
missing=1
|
||||
fi
|
||||
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
|
||||
rm -f "$package_dir/.ldd.out"
|
||||
# TODO: should error?
|
||||
if [ "$missing" -ne 0 ]; then
|
||||
echo "Runtime dependency check found issues; continuing packaging."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Iterate over all built Python wrappers in `install/ifcopenshell/python-x.y.z`.
|
||||
# and zip them, bundling all dynamic libs from `lib`.
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
|
||||
@@ -246,34 +107,18 @@ jobs:
|
||||
fi
|
||||
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
|
||||
find ifcopenshell -name "*.pyc" -delete
|
||||
# TODO: packs qt libs also?
|
||||
stage_runtime_payload ifcopenshell
|
||||
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell
|
||||
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/*
|
||||
mv *.zip ~/output
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
# Iterate over all executables in `install/ifcopenshell/bin` and zip them.
|
||||
# Each zip bundles dynamic libs from `lib` and also qt libs.
|
||||
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
|
||||
exe=`basename "$exe_path"`
|
||||
package_dir="$install_root/.package-${exe}"
|
||||
rm -rf "$package_dir"
|
||||
mkdir -p "$package_dir"
|
||||
cp "$exe_path" "$package_dir/"
|
||||
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
|
||||
stage_runtime_payload "$package_dir" 0
|
||||
stage_qt_runtime_payload "$exe_path" "$package_dir"
|
||||
if [ "$exe" = "BonsaiViewer" ]; then
|
||||
mkdir -p "$package_dir/connectors"
|
||||
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
|
||||
fi
|
||||
check_runtime_dependencies "$package_dir"
|
||||
pushd "$package_dir" > /dev/null
|
||||
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip" .
|
||||
popd > /dev/null
|
||||
rm -rf "$package_dir"
|
||||
cd bin
|
||||
rm *.zip || true
|
||||
ls | while read exe; do
|
||||
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe
|
||||
done
|
||||
mv *.zip ~/output
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -6,19 +6,7 @@ on:
|
||||
jobs:
|
||||
build_ifcopenshell:
|
||||
runs-on: ubuntu-22.04-arm
|
||||
# Rocky 10 (glibc 2.39) — aqt's official Qt6 ARM binaries are built
|
||||
# against glibc 2.38, which Rocky 9 (glibc 2.34) can't run (moc fails to
|
||||
# load). The legacy arm64v8/rockylinux Docker image stopped at 9; Rocky 10
|
||||
# is published under the rockylinux/rockylinux namespace.
|
||||
container:
|
||||
image: rockylinux/rockylinux:10
|
||||
# The community rockylinux image omits PATH from its config (the old
|
||||
# Docker Official arm64v8/rockylinux set it), so GitHub Actions `run:`
|
||||
# steps fail with `exec: "sh": not found` — docker exec has no /usr/bin
|
||||
# to resolve the shell. Restore a standard PATH; GITHUB_PATH prepends
|
||||
# (uv, cargo) are still layered on top by the runner.
|
||||
env:
|
||||
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
container: arm64v8/rockylinux:9
|
||||
|
||||
steps:
|
||||
- name: Set up uv
|
||||
@@ -31,29 +19,13 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
dnf update -y
|
||||
dnf install -y epel-release
|
||||
# --enablerepo=crb: libstdc++-static (libsupc++.a, needed by the
|
||||
# bundled FLTK link) lives in Rocky's CodeReady Builder repo, which
|
||||
# is disabled by default.
|
||||
dnf install -y --enablerepo=crb gcc gcc-c++ git autoconf automake bison make zip cmake \
|
||||
dnf install -y gcc gcc-c++ git autoconf automake bison make zip cmake python3 python3-pip \
|
||||
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
||||
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
||||
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
||||
findutils xz byacc patchelf libxkbcommon-devel \
|
||||
dbus-devel \
|
||||
libXext-devel libXinerama-devel libXcursor-devel libXrender-devel \
|
||||
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
|
||||
findutils xz byacc
|
||||
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"
|
||||
|
||||
- name: Install aws cli
|
||||
run: |
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
|
||||
@@ -84,16 +56,13 @@ jobs:
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux10
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
||||
|
||||
- name: Run Build Script
|
||||
shell: bash
|
||||
run: |
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3" ADD_COMMIT_SHA=1 BUILD_CFG=Release BUILD_BONSAIVIEWER=ON \
|
||||
uv run --with aqtinstall ./nix/build-all.py \
|
||||
-v --diskcleanup --ifcopenshell-shared 2>&1 \
|
||||
| tee build.log
|
||||
CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release uv run ./nix/build-all.py -v --diskcleanup 2>&1 | tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
@@ -120,111 +89,10 @@ jobs:
|
||||
git push || true
|
||||
|
||||
- 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
|
||||
}
|
||||
mkdir ~/output
|
||||
|
||||
ls -d python-* | while read py_version; do
|
||||
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
||||
@@ -239,31 +107,18 @@ jobs:
|
||||
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
|
||||
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell/*
|
||||
mv *.zip ~/output
|
||||
popd > /dev/null
|
||||
done
|
||||
|
||||
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
|
||||
exe=`basename "$exe_path"`
|
||||
package_dir="$install_root/.package-${exe}"
|
||||
rm -rf "$package_dir"
|
||||
mkdir -p "$package_dir"
|
||||
cp "$exe_path" "$package_dir/"
|
||||
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
|
||||
stage_runtime_payload "$package_dir" 0
|
||||
stage_qt_runtime_payload "$exe_path" "$package_dir"
|
||||
if [ "$exe" = "BonsaiViewer" ]; then
|
||||
mkdir -p "$package_dir/connectors"
|
||||
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
|
||||
fi
|
||||
check_runtime_dependencies "$package_dir"
|
||||
pushd "$package_dir" > /dev/null
|
||||
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip" .
|
||||
popd > /dev/null
|
||||
rm -rf "$package_dir"
|
||||
cd bin
|
||||
rm *.zip || true
|
||||
ls | while read exe; do
|
||||
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip $exe
|
||||
done
|
||||
mv *.zip ~/output
|
||||
cd ..
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
|
||||
@@ -48,11 +48,7 @@ jobs:
|
||||
run: |
|
||||
cd ${{ matrix.deps_dir }}
|
||||
Get-ChildItem -Path . -Filter 'cache-*.zip' | ForEach-Object {
|
||||
Write-Host "Extracting $($_.Name)"
|
||||
7z x -bso0 -bsp0 $_.FullName
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to extract $($_.Name) with 7z exit code $LASTEXITCODE."
|
||||
}
|
||||
7z x $_.FullName
|
||||
}
|
||||
|
||||
- name: ccache
|
||||
@@ -63,20 +59,6 @@ jobs:
|
||||
# and with default 500MB some cache gets deleted, leading to misses.
|
||||
max-size: 5000MB
|
||||
|
||||
- name: Set up Python for connector build
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
# Build the Autodesk connector before the C++ build: build-all-win.py
|
||||
# bundles it next to BonsaiViewer.exe while archiving the executables.
|
||||
# bonsaiviewer-autodesk is a Rust connector; packaging/build.py runs
|
||||
# `cargo build --release` and stages the binary + connector.json into
|
||||
# dist/autodesk/.
|
||||
- name: Build Autodesk connector
|
||||
working-directory: src/bonsaiviewer-autodesk
|
||||
run: python packaging/build.py
|
||||
|
||||
- name: Run Build Script And Pack .zip Archives
|
||||
shell: cmd
|
||||
env:
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
- name: Compile
|
||||
run: |
|
||||
cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }}
|
||||
|
||||
@@ -27,9 +27,7 @@ jobs:
|
||||
|
||||
- name: Get current version
|
||||
id: version
|
||||
# Strip any trailing prerelease label and number; the dated alpha
|
||||
# suffix is added below.
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
sudo apt-get install --no-install-recommends \
|
||||
git cmake gcc g++ libboost-all-dev python3-all-dev swig libpcre3-dev libxml2-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
libcgal-dev nlohmann-json3-dev libeigen3-dev
|
||||
libhdf5-dev libcgal-dev nlohmann-json3-dev libeigen3-dev
|
||||
|
||||
-
|
||||
name: ccache
|
||||
@@ -60,6 +60,7 @@ jobs:
|
||||
-DMPFR_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DJSON_INCLUDE_DIR=/usr/include \
|
||||
-DEIGEN_DIR=/usr/include/eigen3 \
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- name: Get current version
|
||||
id: version
|
||||
run: echo "version=$(sed -E 's/[[:alpha:]]+[0-9]+$//' VERSION)" >> $GITHUB_OUTPUT
|
||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%y%m%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -43,7 +43,6 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt-get install --no-install-recommends -y \
|
||||
cmake \
|
||||
bison \
|
||||
gcc \
|
||||
g++ \
|
||||
libboost-date-time-dev \
|
||||
@@ -62,23 +61,13 @@ jobs:
|
||||
libocct-ocaf-dev \
|
||||
libocct-visualization-dev \
|
||||
libpcre3-dev \
|
||||
libpcre2-dev \
|
||||
libtbb-dev \
|
||||
libxml2-dev \
|
||||
libxi-dev \
|
||||
occt-misc \
|
||||
tcl-dev \
|
||||
tk-dev
|
||||
|
||||
- name: Build SWIG
|
||||
# IfcOpenShell requires SWIG 4.1+, ubuntu-22.04 ships 4.0.2.
|
||||
run: |
|
||||
sudo apt-get remove --purge -y swig swig4.0
|
||||
git clone https://github.com/swig/swig --branch v4.2.1 --depth 1
|
||||
cmake -S swig -B swig/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build swig/build -j "$(nproc)"
|
||||
sudo cmake --install swig/build
|
||||
swig -version
|
||||
tk-dev \
|
||||
swig
|
||||
|
||||
- name: Configure minimal IfcOpenShell
|
||||
run: |
|
||||
|
||||
+25
-11
@@ -18,6 +18,7 @@ on:
|
||||
- 'src/ifcparse/**'
|
||||
- 'src/ifcquery/**'
|
||||
- 'src/ifcwrap/**'
|
||||
- 'src/qtviewer/**'
|
||||
- 'src/svgfill/**'
|
||||
- 'src/serializers/**'
|
||||
- 'conda/**'
|
||||
@@ -37,10 +38,6 @@ jobs:
|
||||
compile-and-test:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build_shared_libs: [ON, OFF]
|
||||
env:
|
||||
# Colored output for cmake.
|
||||
CLICOLOR_FORCE: "1"
|
||||
@@ -83,7 +80,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 \
|
||||
${OCCT_CMAKE_DEPS} \
|
||||
libcgal-dev libeigen3-dev
|
||||
libhdf5-dev libcgal-dev libeigen3-dev
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
@@ -121,7 +118,7 @@ jobs:
|
||||
cd OpenCOLLADA
|
||||
git checkout v1.6.68
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/pr622_and_disable_subdirs.patch
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/config_select_libs_by_use_shared.patch
|
||||
patch -p1 --batch --forward -i ../nix/patches/opencollada/allow_static_libraries_config_on_unix.patch
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
@@ -163,7 +160,7 @@ jobs:
|
||||
# Remove default swig to avoid conflicts.
|
||||
sudo apt remove --purge swig swig4.0
|
||||
sudo apt-get install -y libpcre2-dev bison
|
||||
git clone https://github.com/swig/swig --branch v4.2.1 --depth 1
|
||||
git clone https://github.com/swig/swig --branch v4.1.0 --depth 1
|
||||
cd swig
|
||||
mkdir build && cd build
|
||||
cmake .. \
|
||||
@@ -185,7 +182,6 @@ jobs:
|
||||
-DPYTHON_EXECUTABLE:FILEPATH=${{ env.pythonLocation }}/bin/python \
|
||||
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
|
||||
-DUSE_MMAP=On \
|
||||
-DBUILD_SHARED_LIBS=${{ matrix.build_shared_libs }} \
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DWITH_ROCKSDB=On \
|
||||
@@ -221,11 +217,29 @@ jobs:
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
cmake --build .
|
||||
./arbitrary_open_profile_def && test -f arbitrary_open_profile_def.ifc
|
||||
./composite_profile_def && test -f composite_profile_def.ifc
|
||||
./csg_primitive && test -f csg_primitive.ifc
|
||||
./ellipse_pies && test -f ellipse_pies.ifc
|
||||
./faces && test -f faces.ifc
|
||||
./ifc_curve_rebar && test -f ifc_curve_rebar.ifc
|
||||
./profiles
|
||||
test -f IfcUShapeProfileDef.ifc
|
||||
test -f IfcTShapeProfileDef.ifc
|
||||
test -f IfcZShapeProfileDef.ifc
|
||||
test -f IfcEllipseProfileDef.ifc
|
||||
test -f IfcIShapeProfileDef.ifc
|
||||
test -f IfcLShapeProfileDef.ifc
|
||||
test -f IfcCShapeProfileDef.ifc
|
||||
test -f IfcCircleProfileDef.ifc
|
||||
test -f IfcRectangleProfileDef.ifc
|
||||
test -f IfcTrapeziumProfileDef.ifc
|
||||
./IfcParseExamples "../IfcParseExamples_test.ifc"
|
||||
./IfcOpenHouse && test -f IfcOpenHouse.ifc
|
||||
./IfcParseExamples IfcOpenHouse.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
|
||||
./IfcAlignment && test -f IfcAlignment.ifc
|
||||
./IfcSimplifiedAlignment && test -f IfcSimplifiedAlignment.ifc
|
||||
./triangulated_faceset && test -f triangulated_faceset.ifc
|
||||
|
||||
- name: Test ifcopenshell-python
|
||||
run: |
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
name: Publish C++ API documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v0.9.0
|
||||
paths:
|
||||
- '.github/workflows/publish-cpp-api-docs.yml'
|
||||
- 'docs/cpp-api/**'
|
||||
- 'src/ifcgeom/**'
|
||||
- 'src/ifcparse/**'
|
||||
- 'src/serializers/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-cpp-api-docs
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: github.repository == 'IfcOpenShell/IfcOpenShell'
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout IfcOpenShell
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install documentation dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes doxygen graphviz
|
||||
python -m pip install --requirement docs/cpp-api/requirements.txt
|
||||
|
||||
- name: Build C++ API documentation
|
||||
working-directory: docs/cpp-api
|
||||
run: |
|
||||
export PROJECT_NUMBER="$(git rev-parse --short HEAD)"
|
||||
python -m sphinx -M html . output -W --keep-going
|
||||
|
||||
- name: Checkout documentation repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/cpp_docs
|
||||
ref: master
|
||||
path: published-docs
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
- name: Replace published documentation
|
||||
run: |
|
||||
publish_tree="${RUNNER_TEMP}/published-docs-tree"
|
||||
mkdir -p "${publish_tree}/v0.9.0-latest"
|
||||
|
||||
rsync --archive docs/cpp-api/output/html/ "${publish_tree}/v0.9.0-latest/"
|
||||
touch "${publish_tree}/.nojekyll"
|
||||
|
||||
if [[ -f published-docs/CNAME ]]; then
|
||||
cp published-docs/CNAME "${publish_tree}/CNAME"
|
||||
fi
|
||||
|
||||
rsync --archive --delete --exclude='.git/' "${publish_tree}/" published-docs/
|
||||
|
||||
- name: Commit and push if changed
|
||||
working-directory: published-docs
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
git config user.name 'IfcOpenBot'
|
||||
git config user.email 'IfcOpenBot@users.noreply.github.com'
|
||||
|
||||
git add --all
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "Update C++ API docs from ${SOURCE_SHA:0:7}"
|
||||
git push origin master
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
swig libpcre3-dev libxml2-dev \
|
||||
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
|
||||
libhdf5-dev libcgal-dev opencollada-dev
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
@@ -64,6 +64,7 @@ jobs:
|
||||
-DMPFR_INCLUDE_DIR=/usr/include \
|
||||
-DGMP_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DMPFR_LIBRARY_DIR=/usr/lib/x86_64-linux-gnu \
|
||||
-DHDF5_INCLUDE_DIR=/usr/include/hdf5/serial \
|
||||
-DOPENCOLLADA_INCLUDE_DIR=/usr/include/opencollada \
|
||||
-DOPENCOLLADA_LIBRARY_DIR=/usr/lib/opencollada/ \
|
||||
../cmake
|
||||
|
||||
+1
-18
@@ -15,10 +15,8 @@
|
||||
/src/examples/out/
|
||||
/src/ifcmax/out/
|
||||
/src/ifcwrap/out/
|
||||
/src/qtviewer/out/
|
||||
/src/ifctester/webapp/public/pyodide/
|
||||
# pyodide wheels
|
||||
/dist/
|
||||
/dist-modular/
|
||||
|
||||
/win/BuildDepsCache*.txt
|
||||
|
||||
@@ -110,15 +108,6 @@ src/bonsai/bonsai/bim/data/webui/running_pid.json
|
||||
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/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
|
||||
|
||||
@@ -126,9 +115,6 @@ src/ifcopenshell-python/ifcopenshell/libifcopenshell.plugin.so
|
||||
.clangd
|
||||
# clangd cache
|
||||
.cache
|
||||
# Useful for symlinking json compilation database from cmake,
|
||||
# allowing clang commands without `-p path/to/build`.
|
||||
/compile_commands.json
|
||||
|
||||
# Brickschema
|
||||
src/bonsai/bonsai/bim/schema/Brick.ttl
|
||||
@@ -148,6 +134,3 @@ CLAUDE.local.md
|
||||
*.py.tmp*
|
||||
*.json.tmp*
|
||||
|
||||
# bonsaiviewer-autodesk connector build artifacts
|
||||
/src/bonsaiviewer-autodesk/build/
|
||||
/src/bonsaiviewer-autodesk/dist/
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
|
||||
path = src/ifcopenshell-python/test/Sample-BIM-Files
|
||||
url = https://github.com/IfcOpenShell/ids-test-files
|
||||
[submodule "docs/cpp-api/assets/doxygen-awesome-css"]
|
||||
path = docs/cpp-api/assets/doxygen-awesome-css
|
||||
url = https://github.com/jothepro/doxygen-awesome-css.git
|
||||
[submodule "src/ifcopenshell-python/ifcopenshell/simple_spf"]
|
||||
path = src/ifcopenshell-python/ifcopenshell/simple_spf
|
||||
url = https://github.com/IfcOpenShell/step-file-parser
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ RUN echo "deb http://archive.ubuntu.com/ubuntu focal-proposed main restricted" |
|
||||
libboost-all-dev \
|
||||
libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev \
|
||||
libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
|
||||
python3-pytest ; \
|
||||
libhdf5-serial-dev python3-pytest ; \
|
||||
rm -rf /var/lib/apt/lists/* ;
|
||||
|
||||
COPY . /home/IfcOpenShell/
|
||||
|
||||
@@ -70,6 +70,7 @@ The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
||||
| ifcjni | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| ifcparse | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| ifcwrap | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| qtviewer | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
| serializers | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
|
||||
|
||||
[LGPL]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/COPYING.LESSER "LGPL-3.0-or-later"
|
||||
|
||||
+199
-249
@@ -18,10 +18,10 @@
|
||||
################################################################################
|
||||
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
if (NOT DEFINED CMAKE_CXX_STANDARD)
|
||||
if(NOT DEFINED CMAKE_CXX_STANDARD)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
endif()
|
||||
if (CMAKE_CXX_STANDARD LESS 17)
|
||||
if(CMAKE_CXX_STANDARD LESS 17)
|
||||
message(FATAL_ERROR "C++17 or newer is required.")
|
||||
endif()
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
|
||||
@@ -36,18 +36,11 @@ 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+
|
||||
cmake_policy(SET CMP0141 NEW) # Support for `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT`.
|
||||
# Has to be set before `project` to take effect.
|
||||
cmake_policy(SET CMP0141 NEW) # Support for `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT`.
|
||||
endif()
|
||||
if(POLICY CMP0144) # 3.27
|
||||
cmake_policy(SET CMP0144 NEW) # find_package() uses upper-case <PACKAGENAME>_ROOT variables.
|
||||
@@ -56,6 +49,8 @@ if(POLICY CMP0167) # 3.30
|
||||
cmake_policy(SET CMP0167 OLD)
|
||||
endif()
|
||||
|
||||
project(IfcOpenShell VERSION ${RELEASE_VERSION})
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE "Release")
|
||||
endif()
|
||||
@@ -63,106 +58,75 @@ endif()
|
||||
# Include utility macros and functions
|
||||
include(utilities.cmake)
|
||||
|
||||
# Use a SemVer-compatible spelling for CPack artifact names. A trailing
|
||||
# alphabetic label and number is separated from the numeric version by a
|
||||
# hyphen: for example, 0.9.0alpha0 becomes 0.9.0-alpha0.
|
||||
# use extra version to make pre-release using eg semver
|
||||
if(NOT DEFINED EXTRA_VERSION)
|
||||
if(RELEASE_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+([A-Za-z]+)([0-9]+)$")
|
||||
set(EXTRA_VERSION "-${CMAKE_MATCH_1}${CMAKE_MATCH_2}")
|
||||
else()
|
||||
set(EXTRA_VERSION "")
|
||||
endif()
|
||||
set(EXTRA_VERSION "-alpha.3")
|
||||
endif()
|
||||
|
||||
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
|
||||
option(WASM_BUILD "Build a WebAssembly binary." OFF)
|
||||
|
||||
option(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF)
|
||||
option(BUILD_SHARED_LIBS "Build IfcOpenShell as shared libraries (required)." ON)
|
||||
option(
|
||||
ENABLE_BUILD_OPTIMIZATIONS
|
||||
"Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds."
|
||||
OFF
|
||||
)
|
||||
option(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
|
||||
option(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
|
||||
option(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
|
||||
option(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
|
||||
option(NO_WARN "Disable all warnings" OFF)
|
||||
option(CREATE_BUNDLE "Copy .so files and don't create RPATHS or SOVERSION symlinks" )
|
||||
|
||||
option(BUILD_IFCGEOM "Build IfcGeom." ON)
|
||||
option(BUILD_IFCPYTHON "Build IfcPython." ON)
|
||||
option(BUILD_IFCPARSE_EXPERIMENTAL_WRAPPER "Build the experimental Clang-generated ifcparse Python wrapper." OFF)
|
||||
option(BUILD_CONVERT "Build IfcConvert executable." ON)
|
||||
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
|
||||
option(BUILD_EXAMPLES "Build example applications." ON)
|
||||
option(BUILD_EXAMPLES "Build example applications." OFF)
|
||||
option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." ON)
|
||||
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
|
||||
option(BUILD_IFCMODEL_UI "Build minimal Qt IFC model UI prototype" OFF)
|
||||
option(BUILD_BONSAIVIEWER "Build Bonsai Viewer" OFF) # Requires Qt6 + OpenGL 4.5
|
||||
option(BUILD_IFCOPENSHELL_PARSE_TESTS "Build C++ unit tests for IfcParse (fetches Catch2 v3)" OFF)
|
||||
option(BUILD_IFCOPENSHELL_GEOMETRY_TESTS "Build C++ unit tests for IfcGeom (fetches Catch2 v3)" OFF)
|
||||
option(BUILD_BONSAIVIEWER_TESTS "Build unit tests for Bonsai Viewer core (fetches Catch2 v3)" OFF)
|
||||
option(BUILD_BONSAIVIEWER_WGPU "Build the experimental wgpu backend (fetches wgpu-native binary release)" OFF)
|
||||
# IfcViewer (the GL static lib) now links against IfcViewerWgpu because
|
||||
# SceneLoader drives the wgpu viewport. Auto-enable the wgpu subproject
|
||||
# whenever BUILD_BONSAIVIEWER is on so the link target exists.
|
||||
if(BUILD_BONSAIVIEWER AND NOT BUILD_BONSAIVIEWER_WGPU)
|
||||
message(STATUS "BUILD_BONSAIVIEWER implies BUILD_BONSAIVIEWER_WGPU "
|
||||
"(SceneLoader uses ViewportWindow); auto-enabling.")
|
||||
set(BUILD_BONSAIVIEWER_WGPU ON)
|
||||
endif()
|
||||
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
|
||||
option(BUILD_PACKAGE "" OFF)
|
||||
|
||||
# Most users probably need just common schemas,
|
||||
# but we're keeping it `OFF` by default to avoid disruption
|
||||
# (e.g. all Python distribution would need to adapt this option to be set).
|
||||
option(
|
||||
IFCOPENSHELL_DEPLOY_QT_RUNTIME
|
||||
"Deploy Qt runtime dependencies for installed Qt applications."
|
||||
ON
|
||||
)
|
||||
option(
|
||||
IFCOPENSHELL_DEPLOY_QT_TRANSLATIONS
|
||||
"Deploy Qt translation catalogs with installed Qt applications."
|
||||
BUILD_ONLY_COMMON_SCHEMAS
|
||||
"Build only common IFC schemas (2x3, 4, 4x3_add2). By default all schemas will be built."
|
||||
OFF
|
||||
)
|
||||
option(SCHEMA_VERSIONS "Explicitly specify schemas to build." "")
|
||||
|
||||
option(WITH_OPENCASCADE "Enable geometry interpretation using Open CASCADE" ON)
|
||||
option(WITH_CGAL "Enable geometry interpretation using CGAL" ON)
|
||||
option(WITH_MANIFOLD "Enable geometry interpretation using Manifold" OFF)
|
||||
option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
|
||||
option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
|
||||
option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
|
||||
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
|
||||
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
|
||||
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
|
||||
option(WITH_RELATIONSHIP_VALIDATION "Build IfcConvert with option to validate geometrical relationships." OFF)
|
||||
option(WITH_ROCKSDB "Support a RocksDB key-value store as a file backend in IfcOpenShell" OFF)
|
||||
option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
|
||||
|
||||
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
|
||||
option(USE_DEBUG_PYTHON "Use debug binaries when building Debug IfcPython on Windows." OFF)
|
||||
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
|
||||
option(VERSION_OVERRIDE "Use VERSION as the branch label when commit information is embedded" OFF)
|
||||
option(
|
||||
VERSION_OVERRIDE
|
||||
"Override the version defined in buildinfo.cpp with the file VERSION in the repository root"
|
||||
OFF
|
||||
)
|
||||
option(USE_CCACHE "Enable use of ccache if it's available from PATH." ON)
|
||||
|
||||
set(
|
||||
PYTHON_MODULE_INSTALL_DIR
|
||||
"" CACHE PATH
|
||||
set(PYTHON_MODULE_INSTALL_DIR
|
||||
""
|
||||
CACHE PATH
|
||||
"Directory to install IfcPython package to. By default package is installed in found Python's site-packages."
|
||||
)
|
||||
|
||||
project(IfcOpenShell VERSION ${PROJECT_VERSION_NUMERIC})
|
||||
|
||||
# Make sure CMake modules in this project are found first
|
||||
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
|
||||
|
||||
# Catch2 is fetched only when an explicit C++ test family is enabled, so the
|
||||
# default build remains offline-capable.
|
||||
if(BUILD_IFCOPENSHELL_PARSE_TESTS OR BUILD_IFCOPENSHELL_GEOMETRY_TESTS OR BUILD_BONSAIVIEWER_TESTS)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
Catch2
|
||||
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
||||
GIT_TAG v3.5.4
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(Catch2)
|
||||
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
|
||||
include(CTest)
|
||||
include(Catch)
|
||||
enable_testing()
|
||||
endif()
|
||||
|
||||
if(MINIMAL_BUILD)
|
||||
message(STATUS "Setting options for minimal build")
|
||||
set(BUILD_GEOMSERVER OFF)
|
||||
@@ -170,16 +134,18 @@ if(MINIMAL_BUILD)
|
||||
set(WITH_CGAL OFF)
|
||||
set(COLLADA_SUPPORT OFF)
|
||||
set(GLTF_SUPPORT OFF)
|
||||
set(HDF5_SUPPORT OFF)
|
||||
set(IFCXML_SUPPORT OFF)
|
||||
set(USD_SUPPORT OFF)
|
||||
endif()
|
||||
|
||||
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM))
|
||||
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND (NOT BUILD_IFCGEOM))
|
||||
message(STATUS "'IfcGeom' is required with current outputs")
|
||||
set(BUILD_IFCGEOM ON)
|
||||
endif()
|
||||
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
if(USE_CCACHE AND CCACHE_FOUND)
|
||||
message(STATUS "`ccache` is found, using it as a compiler launcher.")
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
|
||||
if(MSVC)
|
||||
@@ -188,17 +154,15 @@ if(CCACHE_FOUND)
|
||||
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"
|
||||
)
|
||||
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()
|
||||
mark_as_advanced(CCACHE_FOUND)
|
||||
|
||||
# Variable to accumulate swig definitions from various submodules.
|
||||
set(SWIG_DEFINES "")
|
||||
|
||||
if(MSVC AND MSVC_PARALLEL_BUILD)
|
||||
add_definitions("/MP")
|
||||
@@ -216,23 +180,26 @@ include(GNUInstallDirs)
|
||||
|
||||
set(IFCOPENSHELL_EXPORT_TARGETS "${PROJECT_NAME}Targets")
|
||||
|
||||
if(NOT INCLUDEDIR)
|
||||
set(INCLUDEDIR include)
|
||||
# On Windows Release and Debug binaries are not compatible.
|
||||
# So we add a postfix to avoid issues and allow release and debug installations to coexist.
|
||||
if(WIN32)
|
||||
set(CMAKE_DEBUG_POSTFIX "_d")
|
||||
endif()
|
||||
if(NOT IS_ABSOLUTE ${INCLUDEDIR})
|
||||
set(INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
endif()
|
||||
message(STATUS "INCLUDEDIR: ${INCLUDEDIR}")
|
||||
|
||||
if(MSVC)
|
||||
message(WARNING "Building DLLs against the static VC run-time. This is not recommended if the DLLs are to be redistributed.")
|
||||
# C4521: 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2'
|
||||
# There will be couple hundreds of these so suppress them away, https://msdn.microsoft.com/en-us/library/esew7y1w.aspx
|
||||
add_definitions(-wd4251)
|
||||
if(BUILD_SHARED_LIBS)
|
||||
add_definitions(-DIFC_SHARED_BUILD)
|
||||
if(MSVC)
|
||||
message(
|
||||
WARNING
|
||||
"Building DLLs against the static VC run-time. This is not recommended if the DLLs are to be redistributed."
|
||||
)
|
||||
# C4521: 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2'
|
||||
# There will be couple hundreds of these so suppress them away, https://msdn.microsoft.com/en-us/library/esew7y1w.aspx
|
||||
add_definitions(-wd4251)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
|
||||
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
|
||||
|
||||
if(NOT MINIMAL_BUILD)
|
||||
UNIFY_ENVVARS_AND_CACHE(PYTHON_INCLUDE_DIR)
|
||||
@@ -248,64 +215,47 @@ foreach(option_flag IN LISTS option_flags)
|
||||
convert_env_var_to_bool("${option_flag}")
|
||||
endforeach()
|
||||
|
||||
if(WITH_CGAL)
|
||||
find_package(CGAL REQUIRED)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
|
||||
list(APPEND GEOMETRY_KERNELS cgal)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM AND WITH_OPENCASCADE)
|
||||
find_package(OpenCASCADE REQUIRED)
|
||||
# Map OpenCASCADE_LIBRARIES variable from OpenCASCADEConfig.cmake to OpenCASCADE_LIBRARIES used by kernel generic cmake file
|
||||
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES})
|
||||
add_definitions(-DIFOPSH_WITH_OPENCASCADE)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_OPENCASCADE)
|
||||
list(APPEND GEOMETRY_KERNELS opencascade)
|
||||
endif()
|
||||
|
||||
message(STATUS "BUILD_IFCGEOM WITH_MANIFOLD: ${BUILD_IFCGEOM} ${WITH_MANIFOLD}")
|
||||
|
||||
if(BUILD_IFCGEOM AND WITH_MANIFOLD)
|
||||
find_package(manifold CONFIG REQUIRED)
|
||||
if(TARGET manifold::manifold)
|
||||
set(MANIFOLD_LIBRARIES manifold::manifold)
|
||||
elseif(TARGET manifold)
|
||||
set(MANIFOLD_LIBRARIES manifold)
|
||||
else()
|
||||
message(FATAL_ERROR "Unable to determine manifold target")
|
||||
endif()
|
||||
list(APPEND GEOMETRY_KERNELS manifold)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
list(APPEND GEOMETRY_KERNELS passthrough)
|
||||
endif()
|
||||
|
||||
set(GLTF_LIBRARIES "")
|
||||
if(GLTF_SUPPORT)
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
set(GLTF_LIBRARIES nlohmann_json::nlohmann_json)
|
||||
add_definitions(-DWITH_GLTF)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_GLTF)
|
||||
endif()
|
||||
|
||||
# Add USD support to serializers
|
||||
set(USD_LIBRARIES "")
|
||||
if(USD_SUPPORT)
|
||||
find_package(USD REQUIRED)
|
||||
set(USD_LIBRARIES pxr::USD)
|
||||
endif(USD_SUPPORT)
|
||||
|
||||
if (WITH_ROCKSDB)
|
||||
set(ROCKSDB_LIBRARIES "")
|
||||
if(WITH_ROCKSDB)
|
||||
# Temporaily mess with CMAKE_FIND_PACKAGE_PREFER_CONFIG to help RocksDB
|
||||
# find it's zstd dependency on Windows.
|
||||
# Only do it on Windows, otherwise it might create problems as
|
||||
# findzstd and zstd-config target names do not match.
|
||||
# https://github.com/facebook/rocksdb/pull/13975
|
||||
if(WIN32)
|
||||
set(TEMP CMAKE_FIND_PACKAGE_PREFER_CONFIG)
|
||||
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)
|
||||
endif()
|
||||
find_package(RocksDB CONFIG REQUIRED)
|
||||
mark_as_advanced(RocksDB_DIR)
|
||||
if(WIN32)
|
||||
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ${TEMP})
|
||||
endif()
|
||||
|
||||
message(STATUS "RocksDB: found at '${RocksDB_DIR}'.")
|
||||
add_library(IFCOPENSHELL_RocksDB INTERFACE)
|
||||
set(IFCOPENSHELL_ROCKSDB_TARGET IFCOPENSHELL_RocksDB)
|
||||
set(ROCKSDB_LIBRARIES "IFCOPENSHELL_RocksDB")
|
||||
target_compile_definitions(IFCOPENSHELL_RocksDB INTERFACE IFOPSH_WITH_ROCKSDB)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB)
|
||||
@@ -318,12 +268,14 @@ if (WITH_ROCKSDB)
|
||||
message(FATAL_ERROR "RocksDB found but neither RocksDB::rocksdb nor RocksDB::rocksdb-shared target exists")
|
||||
endif()
|
||||
|
||||
if (WITH_ZSTD)
|
||||
if(WITH_ZSTD)
|
||||
# @todo do we actually need the zstd include dir or rather just pass
|
||||
# the libzstd.a along with the rocksdb library when needed and feature
|
||||
# detect based on rocksdb API?
|
||||
find_package(zstd CONFIG REQUIRED)
|
||||
mark_as_advanced(zstd_DIR)
|
||||
message(STATUS "zstd: found at '${zstd_DIR}'.")
|
||||
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE zstd::libzstd_static)
|
||||
endif()
|
||||
|
||||
install(TARGETS IFCOPENSHELL_RocksDB EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
|
||||
@@ -331,7 +283,7 @@ endif()
|
||||
|
||||
# Find Boost: On win32 the (hardcoded) default is to use static libraries and
|
||||
# runtime, when doing running conda-build we pick what conda prepared for us.
|
||||
if(WIN32 AND("$ENV{CONDA_BUILD}" STREQUAL ""))
|
||||
if(WIN32 AND NOT DEFINED ENV{CONDA_BUILD})
|
||||
set(Boost_USE_STATIC_LIBS ON)
|
||||
set(Boost_USE_STATIC_RUNTIME OFF)
|
||||
set(Boost_USE_MULTITHREADED ON)
|
||||
@@ -362,8 +314,18 @@ if(WASM_BUILD)
|
||||
else()
|
||||
# @todo review this, shouldn't this be all possible header-only now?
|
||||
# ... or rewritten using C++17 features?
|
||||
# set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
|
||||
set(BOOST_COMPONENTS program_options regex thread date_time iostreams)
|
||||
# Boost.System has been header-only since 1.69 and its compiled stub library
|
||||
# was dropped in newer Boost, so requesting it as a component makes
|
||||
# find_package fail on Boost 1.70 and up (for example Boost 1.90). It is
|
||||
# still pulled in transitively by thread / iostreams where needed, so do not
|
||||
# request it explicitly.
|
||||
set(BOOST_COMPONENTS
|
||||
program_options
|
||||
regex
|
||||
thread
|
||||
date_time
|
||||
iostreams
|
||||
)
|
||||
endif()
|
||||
|
||||
if(USE_MMAP)
|
||||
@@ -373,6 +335,17 @@ if(USE_MMAP)
|
||||
else()
|
||||
set(BOOST_COMPONENTS ${BOOST_COMPONENTS} iostreams)
|
||||
endif()
|
||||
|
||||
add_definitions(-DUSE_MMAP)
|
||||
endif()
|
||||
|
||||
# Handle CGAL after Boost settings are set, since CGAL will use them too.
|
||||
# Do `find_package(Boost)` only after this, to make sure `FindBoost` finds correct components.
|
||||
# Otherwise it will find components needed for CGAL and we might some libraries.
|
||||
if(WITH_CGAL)
|
||||
find_package(CGAL REQUIRED)
|
||||
set(CGAL_LIBRARIES IFCOPENSHELL_CGAL)
|
||||
list(APPEND GEOMETRY_KERNELS cgal)
|
||||
endif()
|
||||
|
||||
find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
|
||||
@@ -381,8 +354,17 @@ message(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
|
||||
|
||||
if(COLLADA_SUPPORT)
|
||||
find_package(OpenCOLLADA REQUIRED)
|
||||
add_definitions(-DWITH_OPENCOLLADA)
|
||||
endif()
|
||||
|
||||
if(HDF5_SUPPORT)
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} hdf5::hdf5_cpp)
|
||||
|
||||
add_definitions(-DWITH_HDF5)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
|
||||
endif(HDF5_SUPPORT)
|
||||
|
||||
if(ENABLE_BUILD_OPTIMIZATIONS)
|
||||
if(MSVC)
|
||||
# NOTE: RelWithDebInfo and Release use O2 (= /Ox /Gl /Gy/ = Og /Oi /Ot /Oy /Ob2 /Gs /GF /Gy) by default,
|
||||
@@ -395,12 +377,20 @@ if(ENABLE_BUILD_OPTIMIZATIONS)
|
||||
|
||||
# Linker
|
||||
# /OPT:REF enables also /OPT:ICF and disables INCREMENTAL
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF")
|
||||
|
||||
set(LINKER_FLAGS_RELEASE "/LTCG /OPT:REF")
|
||||
# /OPT:NOICF is recommended when /DEBUG is used (http://msdn.microsoft.com/en-us/library/xe4t6fc1.aspx)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF")
|
||||
set(LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /OPT:NOICF")
|
||||
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO
|
||||
"${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}"
|
||||
)
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}")
|
||||
set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO
|
||||
"${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}"
|
||||
)
|
||||
else()
|
||||
# GCC-like: Release should use O3 but RelWithDebInfo 02 so enforce 03. Anything other useful that could be added here?
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
||||
@@ -449,7 +439,7 @@ if(MSVC)
|
||||
endif()
|
||||
|
||||
# Enforce standards-conformance on VS > 2015, older Boost versions fail to compile with this
|
||||
if(MSVC_VERSION GREATER 1900 AND(Boost_MAJOR_VERSION GREATER 1 OR Boost_MINOR_VERSION GREATER 66))
|
||||
if(MSVC_VERSION GREATER 1900 AND (Boost_MAJOR_VERSION GREATER 1 OR Boost_MINOR_VERSION GREATER 66))
|
||||
add_definitions(-permissive-)
|
||||
endif()
|
||||
|
||||
@@ -468,11 +458,11 @@ if(MSVC)
|
||||
# endforeach()
|
||||
# endif()
|
||||
|
||||
add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE)
|
||||
# See #5158.
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.40)
|
||||
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
|
||||
endif()
|
||||
add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE)
|
||||
# See #5158.
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.40)
|
||||
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
|
||||
endif()
|
||||
else()
|
||||
add_definitions(-Wall -Wextra)
|
||||
|
||||
@@ -482,7 +472,10 @@ else()
|
||||
add_definitions(-Wno-maybe-uninitialized)
|
||||
endif()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 9.0))
|
||||
if(
|
||||
CMAKE_CXX_COMPILER_ID MATCHES "GNU"
|
||||
AND (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 9.0)
|
||||
)
|
||||
# OpenCascade spews a lot of deprecated-copy warnings
|
||||
add_definitions(-Wno-deprecated-copy)
|
||||
endif()
|
||||
@@ -493,28 +486,45 @@ else()
|
||||
endif()
|
||||
endif(MSVC)
|
||||
|
||||
include_directories(${INCLUDE_DIRECTORIES}
|
||||
${Boost_INCLUDE_DIRS}
|
||||
${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR}
|
||||
)
|
||||
include_directories(${OPENCOLLADA_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} ${HDF5_INCLUDE_DIR})
|
||||
|
||||
if(NOT SCHEMA_VERSIONS)
|
||||
if(WASM_BUILD)
|
||||
# super arbitrarily try to keep size down at least a little bit
|
||||
# `WASM_BUILD` - super arbitrarily try to keep size down at least a little bit
|
||||
if(BUILD_ONLY_COMMON_SCHEMAS OR WASM_BUILD)
|
||||
set(SCHEMA_VERSIONS "2x3" "4" "4x3_add2")
|
||||
else()
|
||||
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3" "4x3_tc1" "4x3_add1" "4x3_add2")
|
||||
set(SCHEMA_VERSIONS
|
||||
"2x3"
|
||||
"4"
|
||||
"4x1"
|
||||
"4x2"
|
||||
"4x3"
|
||||
"4x3_tc1"
|
||||
"4x3_add1"
|
||||
"4x3_add2"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
message(STATUS "IFC SCHEMA_VERSIONS that will be used for the build: ${SCHEMA_VERSIONS}.")
|
||||
|
||||
set(SCHEMA_DEFINITIONS "")
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
list(APPEND SCHEMA_DEFINITIONS "-DHAS_SCHEMA_${schema}")
|
||||
endforeach()
|
||||
|
||||
string(REPLACE ";" ")(" schema_version_seq "(${SCHEMA_VERSIONS})")
|
||||
list(APPEND SCHEMA_DEFINITIONS "-DSCHEMA_SEQ=${schema_version_seq}")
|
||||
|
||||
if(COMPILE_SCHEMA)
|
||||
# @todo, this appears to be untested at the moment
|
||||
find_package(PythonInterp)
|
||||
|
||||
if(NOT PYTHONINTERP_FOUND)
|
||||
message(FATAL_ERROR "A Python interpreter is necessary when COMPILE_SCHEMA is enabled. Disable COMPILE_SCHEMA or fix Python paths to proceed.")
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"A Python interpreter is necessary when COMPILE_SCHEMA is enabled. Disable COMPILE_SCHEMA or fix Python paths to proceed."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(IFC_RELEASE_NOT_USED ${SCHEMA_VERSIONS})
|
||||
@@ -534,7 +544,10 @@ if(COMPILE_SCHEMA)
|
||||
|
||||
if("${PYPARSING_FOUND}" STREQUAL "-1")
|
||||
message(STATUS "Installing pyparsing")
|
||||
execute_process(COMMAND ${PYTHON_EXECUTABLE} -m pip "install" --user pyparsing RESULT_VARIABLE SUCCESS)
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} -m pip "install" --user pyparsing
|
||||
RESULT_VARIABLE SUCCESS
|
||||
)
|
||||
|
||||
if(NOT "${SUCCESS}" STREQUAL "0")
|
||||
execute_process(COMMAND pip "install" --user pyparsing RESULT_VARIABLE SUCCESS)
|
||||
@@ -553,7 +566,8 @@ if(COMPILE_SCHEMA)
|
||||
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py
|
||||
WORKING_DIRECTORY ../src/ifcopenshell-python/ifcopenshell/express
|
||||
OUTPUT_FILE express_parser.py
|
||||
RESULT_VARIABLE SUCCESS)
|
||||
RESULT_VARIABLE SUCCESS
|
||||
)
|
||||
|
||||
if(NOT "${SUCCESS}" STREQUAL "0")
|
||||
message(FATAL_ERROR "Failed to bootstrap parser. Make sure pyparsing is installed")
|
||||
@@ -563,7 +577,8 @@ if(COMPILE_SCHEMA)
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} ../ifcopenshell-python/ifcopenshell/express/express_parser.py ../../${COMPILE_SCHEMA}
|
||||
WORKING_DIRECTORY ../src/ifcparse
|
||||
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME)
|
||||
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME
|
||||
)
|
||||
|
||||
# Prevent the schema that had just been compiled from being excluded
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
@@ -578,52 +593,17 @@ if(NOT Boost_VERSION LESS 105800)
|
||||
add_definitions(-DBOOST_OPTIONAL_USE_OLD_DEFINITION_OF_NONE)
|
||||
endif()
|
||||
|
||||
add_subdirectory(../src/plugin plugin)
|
||||
|
||||
add_subdirectory(../src/ifcparse ifcparse)
|
||||
set(IFCOPENSHELL_LIBRARIES IfcParse)
|
||||
if(BUILD_IFCOPENSHELL_PARSE_TESTS)
|
||||
add_subdirectory(../src/ifcparse/tests ifcparse/tests)
|
||||
endif()
|
||||
|
||||
if(BUILD_EXAMPLES OR BUILD_BONSAIVIEWER)
|
||||
add_subdirectory(../src/helpers helpers)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCPARSE_EXPERIMENTAL_WRAPPER)
|
||||
add_subdirectory(../src/wrappergen wrappergen)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
# CGAL::CGAL target already has dependencies resolved.
|
||||
if(WITH_CGAL AND CGAL_DIR)
|
||||
set(CGAL_LIBRARIES CGAL::CGAL)
|
||||
message(STATUS "Using found CGAL package at '${CGAL_DIR}'")
|
||||
elseif(WITH_CGAL AND NOT CGAL_DIR)
|
||||
find_library(libGMP NAMES gmp mpir PATHS ${GMP_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
find_library(libMPFR NAMES mpfr PATHS ${MPFR_LIBRARY_DIR} NO_DEFAULT_PATH)
|
||||
if(NOT libGMP)
|
||||
message(FATAL_ERROR "Unable to find GMP library files, aborting")
|
||||
endif()
|
||||
if(NOT libMPFR)
|
||||
message(FATAL_ERROR "Unable to find MPFR library files, aborting")
|
||||
endif()
|
||||
|
||||
list(APPEND CGAL_LIBRARIES "${libMPFR}")
|
||||
list(APPEND CGAL_LIBRARIES "${libGMP}")
|
||||
endif()
|
||||
|
||||
add_subdirectory(../src/ifcgeom ifcgeom)
|
||||
if(BUILD_IFCOPENSHELL_GEOMETRY_TESTS)
|
||||
add_subdirectory(../src/ifcgeom/tests ifcgeom/tests)
|
||||
endif()
|
||||
elseif(BUILD_IFCOPENSHELL_GEOMETRY_TESTS)
|
||||
message(FATAL_ERROR "BUILD_IFCOPENSHELL_GEOMETRY_TESTS requires BUILD_IFCGEOM=ON.")
|
||||
endif(BUILD_IFCGEOM)
|
||||
|
||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON OR BUILD_BONSAIVIEWER)
|
||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
add_subdirectory(../src/serializers serializers)
|
||||
endif(BUILD_CONVERT OR BUILD_IFCPYTHON OR BUILD_BONSAIVIEWER)
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} ${SERIALIZER_SCHEMA_LIBRARIES})
|
||||
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
|
||||
if(BUILD_CONVERT)
|
||||
add_subdirectory(../src/ifcconvert ifcconvert)
|
||||
@@ -642,8 +622,8 @@ if(ADD_COMMIT_SHA)
|
||||
endif()
|
||||
|
||||
if(GIT_FOUND)
|
||||
if (VERSION_OVERRIDE)
|
||||
set (git_branch ${RELEASE_VERSION})
|
||||
if(VERSION_OVERRIDE)
|
||||
set(git_branch ${RELEASE_VERSION})
|
||||
else()
|
||||
message("git found: ${GIT_EXECUTABLE} with version ${GIT_VERSION_STRING}")
|
||||
execute_process(
|
||||
@@ -655,8 +635,8 @@ if(ADD_COMMIT_SHA)
|
||||
string(REPLACE "\n" ";" git_branch_list "${git_branches}")
|
||||
|
||||
foreach(git_branch_candidate IN ITEMS ${git_branch_list})
|
||||
string(REPLACE "*" "" git_branch_candidate_temp "${git_branch_candidate}")
|
||||
string(STRIP "${git_branch_candidate_temp}" git_branch_candidate_2)
|
||||
string(REPLACE "*" "" git_branch_candidate_temp "${git_branch_candidate}")
|
||||
string(STRIP "${git_branch_candidate_temp}" git_branch_candidate_2)
|
||||
if(NOT git_branch_candidate_2 MATCHES "^HEAD$")
|
||||
string(REPLACE "/" ";" git_branch_candidate_2_list "${git_branch_candidate_2}")
|
||||
list(GET git_branch_candidate_2_list -1 git_branch)
|
||||
@@ -674,13 +654,13 @@ if(ADD_COMMIT_SHA)
|
||||
message(STATUS "IfcOpenShell branch: \"${git_branch}\"")
|
||||
message(STATUS "IfcOpenShell commit: \"${git_sha}\"")
|
||||
|
||||
if ("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
|
||||
if("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
|
||||
message(FATAL_ERROR "Unable to determine commit sha and/or branch")
|
||||
endif()
|
||||
|
||||
target_compile_definitions(IfcParse PRIVATE
|
||||
-DIFCOPENSHELL_BRANCH=${git_branch}
|
||||
-DIFCOPENSHELL_COMMIT=${git_sha}
|
||||
target_compile_definitions(
|
||||
IfcParse
|
||||
PRIVATE -DIFCOPENSHELL_BRANCH=${git_branch} -DIFCOPENSHELL_COMMIT=${git_sha}
|
||||
)
|
||||
endif()
|
||||
endif(ADD_COMMIT_SHA)
|
||||
@@ -699,7 +679,12 @@ endif()
|
||||
|
||||
# Documentation
|
||||
if(BUILD_DOCUMENTATION)
|
||||
add_subdirectory(../docs/cpp-api docs/cpp-api)
|
||||
set(CMAKE_MODULE_PATH "../docs/cmake")
|
||||
add_subdirectory(../docs docs)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCPYTHON)
|
||||
add_subdirectory(../src/ifcwrap ifcwrap)
|
||||
endif()
|
||||
|
||||
if(BUILD_EXAMPLES)
|
||||
@@ -714,48 +699,8 @@ if(BUILD_IFCPYTHON AND WITH_CGAL)
|
||||
add_subdirectory(../src/svgfill svgfill)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCPYTHON)
|
||||
add_subdirectory(../src/ifcwrap ifcwrap)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCMODEL_UI)
|
||||
add_subdirectory(../src/ifcmodel-ui ifcmodel-ui)
|
||||
endif()
|
||||
|
||||
if(BUILD_IFCGEOM)
|
||||
# install(FILES ${IFCGEOM_H_FILES}
|
||||
# DESTINATION ${INCLUDEDIR}/ifcgeom
|
||||
# )
|
||||
|
||||
install(FILES ${SCHEMA_AGNOSTIC_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/ifcgeom
|
||||
)
|
||||
|
||||
file(GLOB SERIALIZATION_H_FILES ../src/ifcgeom/serialization/*.h)
|
||||
install(FILES ${SERIALIZATION_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/ifcgeom/serialization
|
||||
)
|
||||
|
||||
foreach(kernel ${GEOMETRY_KERNELS})
|
||||
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/kernels/${kernel}/*.h)
|
||||
install(FILES ${IFCGEOM_H_FILES}
|
||||
DESTINATION ${INCLUDEDIR}/ifcgeom/kernels/${kernel}
|
||||
)
|
||||
endforeach()
|
||||
|
||||
install(
|
||||
TARGETS ${IFCGEOM_SCHEMA_LIBRARIES} ${kernel_libraries} IfcGeom
|
||||
EXPORT ${IFCOPENSHELL_EXPORT_TARGETS}
|
||||
)
|
||||
endif(BUILD_IFCGEOM)
|
||||
if(BUILD_BONSAIVIEWER)
|
||||
# IfcViewer is the unified scene + render lib since the wgpu/ifcviewer
|
||||
# merge — wgpu-native is fetched inside its CMakeLists.txt.
|
||||
add_subdirectory(../src/ifcviewer ifcviewer)
|
||||
if(BUILD_BONSAIVIEWER_WGPU)
|
||||
add_subdirectory(../src/ifcviewer-minimal ifcviewer-minimal)
|
||||
endif()
|
||||
add_subdirectory(../src/bonsaiviewer bonsaiviewer)
|
||||
if(BUILD_QTVIEWER)
|
||||
add_subdirectory(../src/qtviewer qtviewer)
|
||||
endif()
|
||||
|
||||
# Cmake uninstall target
|
||||
@@ -763,23 +708,23 @@ if(NOT TARGET uninstall)
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake"
|
||||
IMMEDIATE @ONLY)
|
||||
IMMEDIATE
|
||||
@ONLY
|
||||
)
|
||||
|
||||
add_custom_target(uninstall
|
||||
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
|
||||
add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
|
||||
endif()
|
||||
|
||||
# Packaging
|
||||
list(APPEND CPACK_SOURCE_IGNORE_FILES
|
||||
"/\\\\.git"
|
||||
"/build/"
|
||||
"/.pytest_cache/"
|
||||
"/__pycache__/"
|
||||
)
|
||||
list(APPEND CPACK_SOURCE_IGNORE_FILES "/\\\\.git" "/build/" "/.pytest_cache/" "/__pycache__/")
|
||||
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
|
||||
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
|
||||
set(CPACK_PACKAGE_NAME
|
||||
"${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}"
|
||||
)
|
||||
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}${EXTRA_VERSION}")
|
||||
SET(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}")
|
||||
set(CPACK_PACKAGE_FILE_NAME
|
||||
"${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}"
|
||||
)
|
||||
set(CPACK_PACKAGE_DIRECTORY "${PROJECT_BINARY_DIR}/assets")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "IfcOpenShell")
|
||||
set(CPACK_PACKAGE_DESCRIPTION "IfcOpenShell.")
|
||||
@@ -792,6 +737,7 @@ set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
|
||||
set(CPACK_GENERATOR "TGZ;DEB")
|
||||
set(CPACK_SOURCE_GENERATOR "TGZ")
|
||||
|
||||
set(BOOST_DEPS "")
|
||||
foreach(COMPONENT IN ITEMS ${BOOST_COMPONENTS})
|
||||
string(REPLACE "_" "-" COMP ${COMPONENT})
|
||||
set(BOOST_DEPS "${BOOST_DEPS}, libboost-${COMP}-dev")
|
||||
@@ -799,12 +745,16 @@ endforeach(COMPONENT)
|
||||
|
||||
set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS
|
||||
"python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}"
|
||||
)
|
||||
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION_SUMMARY "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
|
||||
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")
|
||||
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "science")
|
||||
set(CPACK_DEBIAN_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}${EXTRA_VERSION}")
|
||||
set(CPACK_DEBIAN_PACKAGE_VERSION
|
||||
"${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}${EXTRA_VERSION}"
|
||||
)
|
||||
set(CPACK_DEBIAN_ARCHITECTURE "${CMAKE_SYSTEM_PROCESSOR}")
|
||||
# set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_SOURCE_DIR}/cmake/debian/postinst")
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"BUILD_CONVERT": "ON",
|
||||
"BUILD_IFCMAX": "OFF",
|
||||
"IFCXML_SUPPORT": "ON",
|
||||
"HDF5_SUPPORT": "ON",
|
||||
"SCHEMA_VERSIONS": "4x3_add2",
|
||||
"CMAKE_GENERATOR_PLATFORM": "",
|
||||
"CMAKE_GENERATOR_TOOLSET": ""
|
||||
@@ -49,6 +50,8 @@
|
||||
"MPFR_LIBRARY_DIR": "$env{LIBRARY_PREFIX}/lib",
|
||||
"Boost_LIBRARY_DIR": "$env{LIBRARY_PREFIX}/lib",
|
||||
"Boost_INCLUDE_DIR": "$env{LIBRARY_PREFIX}/include",
|
||||
"HDF5_INCLUDE_DIR": "$env{LIBRARY_PREFIX}/include",
|
||||
"HDF5_LIBRARY_DIR": "$env{LIBRARY_PREFIX}/lib",
|
||||
"ZLIB_INCLUDE_DIR": "$env{LIBRARY_PREFIX}/include"
|
||||
}
|
||||
},
|
||||
@@ -107,4 +110,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
# - `GMP_LIBRARY_DIR`
|
||||
# - `MPFR_INCLUDE_DIR`
|
||||
# - `MPFR_LIBRARY_DIR`
|
||||
# If input variables are not specified, try to find CGAL config.
|
||||
# If input variables are not specified, try to find HDF5 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output targets:
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#
|
||||
# Input variables:
|
||||
# - `HDF5_INCLUDE_DIR`
|
||||
# - `HDF5_LIBRARY_DIR`
|
||||
# - `HDF5_LIBRARIES`
|
||||
# If input variables are not specified, try to find HDF5 config.
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output variables:
|
||||
# - `HDF5_INCLUDE_DIR`
|
||||
# - `HDF5_LIBRARY_DIR`
|
||||
# - `HDF5_LIBRARIES`
|
||||
#
|
||||
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
|
||||
|
||||
# To avoid cyclic calls to this file
|
||||
list(REMOVE_ITEM CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
|
||||
if(NOT HDF5_INCLUDE_DIR)
|
||||
message(STATUS "No HDF5 include directory specified")
|
||||
else()
|
||||
set(HDF5_INCLUDE_DIR "${HDF5_INCLUDE_DIR}" CACHE FILEPATH "HDF5 header files")
|
||||
endif()
|
||||
|
||||
if(NOT HDF5_LIBRARY_DIR)
|
||||
message(STATUS "No HDF5 library directory specified")
|
||||
else()
|
||||
set(HDF5_LIBRARY_DIR "${HDF5_LIBRARY_DIR}" CACHE FILEPATH "HDF5 library files")
|
||||
endif()
|
||||
|
||||
if(HDF5_LIBRARY_DIR)
|
||||
# result of the HDF5 ctest package
|
||||
# Find zlib using cmake find_library. How should this be implemented?
|
||||
# FIND_LIBRARY(NAMES z libz libz_debug PATHS ... NO_DEFAULT_PATH)
|
||||
if(NOT DEFINED ENV{CONDA_BUILD})
|
||||
# result of the HDF5 ctest package
|
||||
if(WIN32)
|
||||
set(zlib_post lib)
|
||||
set(lib_ext lib)
|
||||
else()
|
||||
set(lib_ext a)
|
||||
endif()
|
||||
|
||||
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
set(debug_postfix "_debug")
|
||||
endif()
|
||||
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libz${zlib_post}${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libsz${debug_postfix}.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libaec${debug_postfix}.${lib_ext}"
|
||||
)
|
||||
else()
|
||||
message(STATUS "Packaging hdf5 and zlib for conda distribution")
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||
# macOS
|
||||
set(zlib_post libz)
|
||||
set(lib_ext dylib)
|
||||
set(HDF5_LIBRARIES
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5_cpp.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/libhdf5.${lib_ext}"
|
||||
"${HDF5_LIBRARY_DIR}/${zlib_post}.${lib_ext}"
|
||||
)
|
||||
else()
|
||||
# linux and windows
|
||||
# Find HDF5 package
|
||||
find_package(HDF5 REQUIRED COMPONENTS C CXX)
|
||||
# Find ZLIB package
|
||||
find_package(ZLIB REQUIRED)
|
||||
# Include directories
|
||||
include_directories(${HDF5_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS})
|
||||
# Link libraries
|
||||
set(HDF5_LIBRARIES ${HDF5_LIBRARIES} ${ZLIB_LIBRARIES})
|
||||
message(STATUS "HDF5 libraries: ${HDF5_LIBRARIES}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT HDF5_INCLUDE_DIR OR NOT HDF5_LIBRARY_DIR)
|
||||
# First try to find it as a config.
|
||||
find_package(HDF5 CONFIG)
|
||||
mark_as_advanced(HDF5_DIR)
|
||||
if(HDF5_DIR)
|
||||
message(STATUS "HDF5: found config at '${HDF5_DIR}'.")
|
||||
if(TARGET hdf5_cpp-static)
|
||||
set(HDF5_LIBRARIES hdf5_cpp-static)
|
||||
elseif(TARGET hdf5_cpp-shared)
|
||||
set(HDF5_LIBRARIES hdf5_cpp-shared)
|
||||
elseif(TARGET hdf5::hdf5_cpp-shared)
|
||||
set(HDF5_LIBRARIES hdf5::hdf5_cpp-shared)
|
||||
else()
|
||||
find_package(HDF5 REQUIRED COMPONENTS CXX)
|
||||
endif()
|
||||
else()
|
||||
# If it failed, still try to find as a module.
|
||||
# E.g. on Ubuntu `libhdf5-dev` doesn't provie hdf5-config.cmake.
|
||||
# Will automatically fill HDF5_LIBRARIES and HDF5_INCLUDE_DIR.
|
||||
find_package(HDF5 COMPONENTS CXX)
|
||||
if(NOT HDF5_INCLUDE_DIR)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"HDF5_INCLUDE_DIR is not provided (current value: '${HDF5_INCLUDE_DIR}'). "
|
||||
"HDF5_LIBRARY_DIR is not provided (current value: '${HDF5_LIBRARY_DIR}'). "
|
||||
"Also could not find HDF5 package (neither module or config)."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Restore module path.
|
||||
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
@@ -139,8 +139,7 @@ if(NOT OpenCOLLADA_DIR)
|
||||
endif()
|
||||
endif(NOT OpenCOLLADA_DIR)
|
||||
|
||||
if(OPENCOLLADA_FOUND AND NOT TARGET OpenCOLLADA::OpenCOLLADA)
|
||||
add_library(OpenCOLLADA::OpenCOLLADA INTERFACE IMPORTED)
|
||||
target_include_directories(OpenCOLLADA::OpenCOLLADA INTERFACE ${OPENCOLLADA_INCLUDE_DIRS})
|
||||
target_link_libraries(OpenCOLLADA::OpenCOLLADA INTERFACE ${OPENCOLLADA_LIBRARIES})
|
||||
if(OPENCOLLADA_FOUND)
|
||||
add_definitions(-DWITH_OPENCOLLADA)
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
|
||||
endif()
|
||||
|
||||
+6
-12
@@ -6,7 +6,7 @@
|
||||
# Input variables could also be provided as environment variables.
|
||||
#
|
||||
# Output targets:
|
||||
# - `proj::proj`
|
||||
# - `PROJ::proj`
|
||||
#
|
||||
|
||||
# To avoid cyclic calls to this file
|
||||
@@ -34,12 +34,10 @@ if((NOT PROJ_INCLUDE_DIR AND NOT PROJ_LIBRARIES))
|
||||
message(FATAL_ERROR "Unable to find PROJ libraries in: ${PROJ_LIBRARY_DIR}")
|
||||
endif()
|
||||
|
||||
if(NOT TARGET proj::proj)
|
||||
add_library(proj::proj INTERFACE IMPORTED)
|
||||
target_include_directories(proj::proj INTERFACE "${PROJ_INCLUDE_DIR}")
|
||||
target_link_libraries(proj::proj INTERFACE ${PROJ_LIBRARIES})
|
||||
target_link_directories(proj::proj INTERFACE "${PROJ_LIBRARY}")
|
||||
endif()
|
||||
add_library(PROJ::proj INTERFACE IMPORTED)
|
||||
target_include_directories(PROJ::proj INTERFACE "${PROJ_INCLUDE_DIR}")
|
||||
target_link_libraries(PROJ::proj INTERFACE ${PROJ_LIBRARIES})
|
||||
target_link_directories(PROJ::proj INTERFACE "${PROJ_LIBRARY}")
|
||||
endif()
|
||||
else()
|
||||
find_library(PROJ_LIBRARY NAMES proj PATHS ${PROJ_LIBRARY_DIR})
|
||||
@@ -52,11 +50,7 @@ else()
|
||||
|
||||
set(PROJ_INCLUDE_DIR ${PROJ_INCLUDE_DIR} CACHE FILEPATH "PROJ header files")
|
||||
message(STATUS "Looking for PROJ include files in: ${PROJ_INCLUDE_DIR}")
|
||||
if(NOT TARGET proj::proj)
|
||||
add_library(proj::proj INTERFACE IMPORTED)
|
||||
target_include_directories(proj::proj INTERFACE "${PROJ_INCLUDE_DIR}")
|
||||
target_link_libraries(proj::proj INTERFACE ${PROJ_LIBRARIES})
|
||||
endif()
|
||||
include_directories(${PROJ_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
|
||||
|
||||
@@ -64,6 +64,7 @@ set(USD_LIBRARIES
|
||||
find_library(USD_LIBRARY NAMES ${USD_LIBRARIES} PATHS ${USD_LIBRARY_DIR})
|
||||
if(USD_LIBRARY)
|
||||
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
|
||||
link_directories(${USD_LIBRARY_DIR})
|
||||
else()
|
||||
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
|
||||
endif()
|
||||
@@ -81,3 +82,5 @@ if(MSVC)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(pxr::USD INTERFACE PXR_STATIC WITH_USD)
|
||||
|
||||
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
set_and_check(IFCOPENSHELL_LIBRARY_DIR "@PACKAGE_CMAKE_INSTALL_LIBDIR@")
|
||||
|
||||
# Variable to inspect installed schema versions.
|
||||
set(IFCOPENSHELL_SCHEMA_VERSIONS @SCHEMA_VERSIONS@)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ configure_package_config_file(
|
||||
${CONFIG_PACKAGE_INPUT}
|
||||
${CONFIG_PACKAGE_OUTPUT}
|
||||
INSTALL_DESTINATION ${CONFIG_PACKAGE_LOCATION}
|
||||
PATH_VARS CMAKE_INSTALL_LIBDIR
|
||||
)
|
||||
|
||||
install(FILES "${CONFIG_PACKAGE_OUTPUT}" "${CONFIG_VERSION_OUTPUT}" DESTINATION ${CONFIG_PACKAGE_LOCATION})
|
||||
|
||||
@@ -41,122 +41,6 @@ macro(SET_INSTALL_RPATHS _target _paths)
|
||||
set_target_properties(${_target} PROPERTIES INSTALL_RPATH "${${_target}_rpaths}")
|
||||
endmacro()
|
||||
|
||||
macro(SET_INSTALL_SELF_RPATH _target)
|
||||
if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}")
|
||||
SET_INSTALL_RPATHS(${_target} "${CMAKE_INSTALL_LIBDIR}")
|
||||
elseif(APPLE)
|
||||
SET_INSTALL_RPATHS(${_target} "@loader_path")
|
||||
else()
|
||||
SET_INSTALL_RPATHS(${_target} "$ORIGIN")
|
||||
endif()
|
||||
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})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(ifcopenshell_wasm_plugin_link_options TARGET REGISTRATION_SYMBOL)
|
||||
ifcopenshell_plugin_target(${TARGET})
|
||||
|
||||
if(NOT WASM_BUILD)
|
||||
return()
|
||||
endif()
|
||||
|
||||
cmake_parse_arguments(PLUGIN "" "OPTIMIZATION" "" ${ARGN})
|
||||
if(NOT PLUGIN_OPTIMIZATION)
|
||||
set(PLUGIN_OPTIMIZATION -O1)
|
||||
endif()
|
||||
|
||||
set(plugin_symbols
|
||||
ifcopenshell_plugin_abi_v1
|
||||
ifcopenshell_plugin_metadata_v1
|
||||
${REGISTRATION_SYMBOL}
|
||||
)
|
||||
|
||||
target_link_options(${TARGET} PRIVATE "SHELL:-s SIDE_MODULE=2" ${PLUGIN_OPTIMIZATION})
|
||||
foreach(symbol IN LISTS plugin_symbols)
|
||||
target_link_options(${TARGET} PRIVATE "LINKER:--export=${symbol}")
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
function(ifcopenshell_deploy_qt_runtime TARGET)
|
||||
if(NOT IFCOPENSHELL_DEPLOY_QT_RUNTIME)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT TARGET ${TARGET})
|
||||
message(FATAL_ERROR "Cannot deploy Qt runtime for unknown target '${TARGET}'.")
|
||||
endif()
|
||||
|
||||
get_target_property(target_type ${TARGET} TYPE)
|
||||
if(NOT target_type STREQUAL "EXECUTABLE")
|
||||
message(FATAL_ERROR "Qt runtime deployment target '${TARGET}' is not an executable.")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED QT_DEFAULT_MAJOR_VERSION)
|
||||
if(DEFINED QT_VERSION)
|
||||
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION})
|
||||
else()
|
||||
set(QT_DEFAULT_MAJOR_VERSION 6)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT TARGET Qt${QT_DEFAULT_MAJOR_VERSION}::Core)
|
||||
set(qt_find_args Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED)
|
||||
if(DEFINED QT_DIR AND NOT QT_DIR STREQUAL "")
|
||||
list(APPEND qt_find_args PATHS ${QT_DIR})
|
||||
endif()
|
||||
find_package(${qt_find_args})
|
||||
endif()
|
||||
|
||||
if(COMMAND _qt_internal_setup_deploy_support)
|
||||
if(NOT DEFINED QT_CMAKE_EXPORT_NAMESPACE AND TARGET Qt${QT_DEFAULT_MAJOR_VERSION}::Core)
|
||||
set(QT_CMAKE_EXPORT_NAMESPACE Qt${QT_DEFAULT_MAJOR_VERSION})
|
||||
endif()
|
||||
|
||||
if(QT_DEFAULT_MAJOR_VERSION EQUAL 6 AND TARGET Qt6::Core)
|
||||
get_target_property(qt_core_type Qt6::Core TYPE)
|
||||
if(qt_core_type STREQUAL "SHARED_LIBRARY")
|
||||
set(QT6_IS_SHARED_LIBS_BUILD ON)
|
||||
else()
|
||||
set(QT6_IS_SHARED_LIBS_BUILD OFF)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
_qt_internal_setup_deploy_support()
|
||||
endif()
|
||||
|
||||
set(deploy_args
|
||||
TARGET ${TARGET}
|
||||
OUTPUT_SCRIPT deploy_script
|
||||
NO_UNSUPPORTED_PLATFORM_ERROR
|
||||
)
|
||||
|
||||
if(NOT IFCOPENSHELL_DEPLOY_QT_TRANSLATIONS)
|
||||
list(APPEND deploy_args NO_TRANSLATIONS)
|
||||
endif()
|
||||
|
||||
list(APPEND deploy_args ${ARGN})
|
||||
|
||||
if(COMMAND qt_generate_deploy_app_script)
|
||||
qt_generate_deploy_app_script(${deploy_args})
|
||||
elseif(COMMAND qt6_generate_deploy_app_script)
|
||||
qt6_generate_deploy_app_script(${deploy_args})
|
||||
else()
|
||||
message(WARNING
|
||||
"Qt runtime deployment requested for '${TARGET}', but this Qt version "
|
||||
"does not provide qt_generate_deploy_app_script()."
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
install(SCRIPT ${deploy_script})
|
||||
endfunction()
|
||||
|
||||
# Get a list of all OPTION flags from the CMakeLists.txt and store in an output LIST
|
||||
function(get_all_option_flags output_list)
|
||||
# Read the contents of the CMakeLists.txt
|
||||
|
||||
@@ -22,6 +22,9 @@ cmake -G "Ninja" ^
|
||||
-D GMP_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D MPFR_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D COLLADA_SUPPORT=OFF ^
|
||||
-D HDF5_SUPPORT=ON ^
|
||||
-D HDF5_INCLUDE_DIR="%LIBRARY_PREFIX%\include" ^
|
||||
-D HDF5_LIBRARY_DIR="%LIBRARY_PREFIX%\lib" ^
|
||||
-D JSON_INCLUDE_DIR="%LIBRARY_PREFIX%\include" ^
|
||||
-D PYTHON_INCLUDE_DIR=%PREFIX%\include ^
|
||||
-D PYTHON_EXECUTABLE:FILEPATH=%PREFIX%\python.exe ^
|
||||
@@ -34,6 +37,7 @@ cmake -G "Ninja" ^
|
||||
-D GLTF_SUPPORT:BOOL=ON ^
|
||||
-D BUILD_CONVERT:BOOL=ON ^
|
||||
-D BUILD_IFCMAX:BOOL=OFF ^
|
||||
-D IFCXML_SUPPORT:BOOL=ON ^
|
||||
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
|
||||
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
|
||||
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
|
||||
|
||||
+5
-1
@@ -24,6 +24,9 @@ cmake ${CMAKE_ARGS} -G Ninja \
|
||||
-DMPFR_LIBRARY_DIR=$PREFIX/lib \
|
||||
-DOCC_INCLUDE_DIR=$PREFIX/include/opencascade \
|
||||
-DOCC_LIBRARY_DIR=$PREFIX/lib \
|
||||
-DHDF5_SUPPORT:BOOL=ON \
|
||||
-DHDF5_INCLUDE_DIR=$PREFIX/include \
|
||||
-DHDF5_LIBRARY_DIR=$PREFIX/lib \
|
||||
-DJSON_INCLUDE_DIR=$PREFIX/include \
|
||||
-DCGAL_INCLUDE_DIR=$PREFIX/include \
|
||||
-DLIBXML2_INCLUDE_DIR=$PREFIX/include/libxml2 \
|
||||
@@ -31,6 +34,7 @@ cmake ${CMAKE_ARGS} -G Ninja \
|
||||
-DEIGEN_DIR:FILEPATH=$PREFIX/include/eigen3 \
|
||||
-DCOLLADA_SUPPORT:BOOL=OFF \
|
||||
-DBUILD_EXAMPLES:BOOL=OFF \
|
||||
-DIFCXML_SUPPORT:BOOL=ON \
|
||||
-DGLTF_SUPPORT:BOOL=ON \
|
||||
-DBUILD_CONVERT:BOOL=ON \
|
||||
-DBUILD_IFCPYTHON:BOOL=ON \
|
||||
@@ -43,4 +47,4 @@ ninja
|
||||
|
||||
ninja install -j 1
|
||||
|
||||
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
|
||||
python "${RECIPE_DIR}/update_version_init.py" "${PKG_VERSION}" "${SP_DIR}/ifcopenshell/__init__.py"
|
||||
@@ -26,6 +26,8 @@ c_stdlib_version:
|
||||
- 2.17 # [linux]
|
||||
- 10.13 # [osx and x86_64]
|
||||
- 11.0 # [osx and arm64]
|
||||
hdf5:
|
||||
- 1.14.6
|
||||
libboost_devel:
|
||||
- '1.86'
|
||||
libxml2:
|
||||
|
||||
@@ -33,6 +33,7 @@ requirements:
|
||||
- occt
|
||||
- libxml2
|
||||
- cgal-cpp
|
||||
- hdf5
|
||||
- eigen
|
||||
- mpfr
|
||||
- nlohmann_json
|
||||
@@ -284,6 +285,11 @@ about:
|
||||
<td>Internal library for IfcOpenShell</td>
|
||||
<td>LGPL-3.0-or-later*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>qtviewer</td>
|
||||
<td>Internal library for IfcOpenShell</td>
|
||||
<td>LGPL-3.0-or-later*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>serializers</td>
|
||||
<td>Internal library for IfcOpenShell</td>
|
||||
|
||||
+34
-14
@@ -1,15 +1,35 @@
|
||||
find_package(Doxygen REQUIRED)
|
||||
find_program(
|
||||
SPHINX_EXECUTABLE
|
||||
NAMES sphinx-build
|
||||
REQUIRED
|
||||
DOC "Path to the sphinx-build executable"
|
||||
)
|
||||
#Look for an executable called sphinx-build
|
||||
find_program(SPHINX_EXECUTABLE NAMES sphinx-build DOC "Path to sphinx-build executable")
|
||||
|
||||
add_custom_target(
|
||||
cpp_api_docs
|
||||
COMMAND ${SPHINX_EXECUTABLE} -M html . output -W --keep-going
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMENT "Generating the IfcOpenShell C++ API documentation"
|
||||
VERBATIM
|
||||
)
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
#Handle standard arguments to find_package like REQUIRED and QUIET
|
||||
find_package_handle_standard_args(Sphinx "Failed to find sphinx-build executable" SPHINX_EXECUTABLE)
|
||||
|
||||
find_package(Doxygen REQUIRED)
|
||||
#find_package(Sphinx REQUIRED)
|
||||
|
||||
set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/docs/sphinx)
|
||||
|
||||
message(STATUS "SPHINX BUILD ${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
file(MAKE_DIRECTORY ./output/doxygen)
|
||||
|
||||
if(DOXYGEN_FOUND)
|
||||
add_custom_target(
|
||||
Sphinx
|
||||
ALL
|
||||
COMMAND ${SPHINX_EXECUTABLE} -v -T -b html ${SPHINX_SOURCE} ${CMAKE_CURRENT_SOURCE_DIR}/output
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/output
|
||||
COMMENT "Generating documentation with Sphinx"
|
||||
)
|
||||
|
||||
# add_custom_target(ifcopenshell_python_docs ALL
|
||||
# COMMAND make html
|
||||
# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../src/ifcblenderexport/docs
|
||||
# OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/../src/ifcblenderexport/docs
|
||||
# COMMENT "Generating documentation with Sphinx")
|
||||
else(DOXYGEN_FOUND)
|
||||
message("Doxygen need to be installed to generate the doxygen documentation")
|
||||
endif(DOXYGEN_FOUND)
|
||||
|
||||
+22
-63
@@ -68,7 +68,7 @@ PROJECT_LOGO =
|
||||
# entered, it will be relative to the location where doxygen was started. If
|
||||
# left blank the current directory will be used.
|
||||
|
||||
OUTPUT_DIRECTORY = ./output/doxygen
|
||||
OUTPUT_DIRECTORY = ./output
|
||||
|
||||
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
|
||||
# sub-directories (in 2 levels) under the output directory of each output format
|
||||
@@ -852,7 +852,7 @@ WARNINGS = YES
|
||||
# will automatically be disabled.
|
||||
# The default value is: YES.
|
||||
|
||||
WARN_IF_UNDOCUMENTED = NO
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
|
||||
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
|
||||
# potential errors in the documentation, such as documenting some parameters in
|
||||
@@ -901,7 +901,7 @@ WARN_IF_UNDOC_ENUM_VAL = NO
|
||||
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
|
||||
# The default value is: NO.
|
||||
|
||||
WARN_AS_ERROR = FAIL_ON_WARNINGS
|
||||
WARN_AS_ERROR = NO
|
||||
|
||||
# The WARN_FORMAT tag determines the format of the warning messages that doxygen
|
||||
# can produce. The string should contain the $file, $line, and $text tags, which
|
||||
@@ -944,6 +944,7 @@ WARN_LOGFILE =
|
||||
# Note: If this tag is empty the current directory is searched.
|
||||
|
||||
INPUT = ../../src/ifcgeom \
|
||||
../../src/ifcgeom_schema_agnostic \
|
||||
../../src/ifcparse \
|
||||
../../src/serializers \
|
||||
|
||||
@@ -1000,7 +1001,7 @@ RECURSIVE = YES
|
||||
# Note that relative paths are relative to the directory from which doxygen is
|
||||
# run.
|
||||
|
||||
EXCLUDE = ../../src/ifcparse/schemas
|
||||
EXCLUDE =
|
||||
|
||||
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
|
||||
# directories that are symbolic links (a Unix file system feature) are excluded
|
||||
@@ -1024,33 +1025,7 @@ EXCLUDE_PATTERNS =
|
||||
# wildcard * is used, a substring. Examples: ANamespace, AClass,
|
||||
# ANamespace::AClass, ANamespace::*Test
|
||||
|
||||
EXCLUDE_SYMBOLS = "ifcopenshell::geom::opaque_number::*" \
|
||||
ifcopenshell::entity::attribute_by_name_cmp \
|
||||
ifcopenshell::impl::rocks_db_file_storage::rocksdb_types_iterator \
|
||||
ifcopenshell::impl::in_memory_file_storage::type_iterator \
|
||||
"util::string_buffer::*_item" \
|
||||
util::string_buffer::item \
|
||||
ifcopenshell::geom::layer_filter::wildcards_match \
|
||||
ifcopenshell::paged_file_impl::entry \
|
||||
ifcopenshell::token \
|
||||
attribute_value::pointer_type \
|
||||
INCLUDE_PARENT_PARENT_DIR \
|
||||
POSTFIX_SCHEMA_ \
|
||||
POSTFIX_SCHEMA__ \
|
||||
STRINGIFY_ \
|
||||
MAKE_INIT_FN_ \
|
||||
MAKE_INIT_FN__ \
|
||||
key_from_string \
|
||||
add_ \
|
||||
subtract_ \
|
||||
multiply_ \
|
||||
divide_ \
|
||||
equals_ \
|
||||
less_than_ \
|
||||
negate_ \
|
||||
ifcopenshell::geom::utils::create_cube \
|
||||
ifcopenshell::geom::utils::create_polyhedron \
|
||||
ifcopenshell::geom::utils::create_nef_polyhedron
|
||||
EXCLUDE_SYMBOLS =
|
||||
|
||||
# The EXAMPLE_PATH tag can be used to specify one or more files or directories
|
||||
# that contain example code fragments that are included (see the \include
|
||||
@@ -1261,7 +1236,7 @@ IGNORE_PREFIX =
|
||||
# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
|
||||
# The default value is: YES.
|
||||
|
||||
GENERATE_HTML = NO
|
||||
GENERATE_HTML = YES
|
||||
|
||||
# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
|
||||
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
|
||||
@@ -1336,7 +1311,7 @@ HTML_STYLESHEET =
|
||||
# documentation.
|
||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||
|
||||
HTML_EXTRA_STYLESHEET =
|
||||
HTML_EXTRA_STYLESHEET = assets/doxygen-awesome-css/doxygen-awesome.css
|
||||
|
||||
# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
|
||||
# other source files which should be copied to the HTML output directory. Note
|
||||
@@ -2191,7 +2166,7 @@ MAN_LINKS = NO
|
||||
# captures the structure of the code including all documentation.
|
||||
# The default value is: NO.
|
||||
|
||||
GENERATE_XML = YES
|
||||
GENERATE_XML = NO
|
||||
|
||||
# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
|
||||
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
|
||||
@@ -2328,7 +2303,7 @@ ENABLE_PREPROCESSING = YES
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
MACRO_EXPANSION = YES
|
||||
MACRO_EXPANSION = NO
|
||||
|
||||
# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
|
||||
# the macro expansion is limited to the macros specified with the PREDEFINED and
|
||||
@@ -2336,7 +2311,7 @@ MACRO_EXPANSION = YES
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
EXPAND_ONLY_PREDEF = YES
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
|
||||
# If the SEARCH_INCLUDES tag is set to YES, the include files in the
|
||||
# INCLUDE_PATH will be searched if a #include is found.
|
||||
@@ -2369,17 +2344,7 @@ INCLUDE_FILE_PATTERNS =
|
||||
# recursively expanded use the := operator instead of the = operator.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
PREDEFINED = IFC_PARSE_API= \
|
||||
IFC_SCHEMA_API= \
|
||||
IFC_GEOM_API= \
|
||||
IFC_GEOMLIBRARY_API= \
|
||||
IFC_GEOMSERIALIZATION_API= \
|
||||
SERIALIZERS_API= \
|
||||
"POSTFIX_SCHEMA(name)=name##_Schema" \
|
||||
"Handle(name):=opencascade::handle<name>" \
|
||||
kernel_=kernel \
|
||||
Simplekernel_=Simplekernel \
|
||||
inline=
|
||||
PREDEFINED =
|
||||
|
||||
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
|
||||
# tag can be used to specify a list of macro names that should be expanded. The
|
||||
@@ -2388,22 +2353,7 @@ PREDEFINED = IFC_PARSE_API= \
|
||||
# definition found in the source code.
|
||||
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
|
||||
|
||||
EXPAND_AS_DEFINED = kernel_ \
|
||||
cgal_shape \
|
||||
cgal_kernel \
|
||||
cgal_placement \
|
||||
cgal_point \
|
||||
cgal_direction \
|
||||
cgal_vector \
|
||||
cgal_plane \
|
||||
cgal_curve \
|
||||
cgal_wire \
|
||||
cgal_face \
|
||||
cgal_polyhedron \
|
||||
cgal_vertex_descriptor \
|
||||
cgal_face_descriptor \
|
||||
create_cube \
|
||||
create_polyhedron
|
||||
EXPAND_AS_DEFINED =
|
||||
|
||||
# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
|
||||
# remove all references to function-like macros that are alone on a line, have
|
||||
@@ -2781,6 +2731,15 @@ DOT_GRAPH_MAX_NODES = 50
|
||||
|
||||
MAX_DOT_GRAPH_DEPTH = 0
|
||||
|
||||
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
||||
# files in one run (i.e. multiple -o and -T options on the command line). This
|
||||
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
|
||||
# this, this feature is disabled by default.
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_MULTI_TARGETS = NO
|
||||
|
||||
# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
|
||||
# explaining the meaning of the various boxes and arrows in the dot generated
|
||||
# graphs.
|
||||
|
||||
+18
-41
@@ -1,56 +1,33 @@
|
||||
# IfcOpenShell C++ API documentation
|
||||
|
||||
This directory contains the Sphinx, Doxygen, Breathe, and Exhale configuration
|
||||
for the IfcOpenShell C++ API reference. During a Sphinx build, Exhale runs
|
||||
Doxygen, Breathe consumes the generated XML, and Exhale creates the API pages.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10 or newer
|
||||
- [Doxygen](https://www.doxygen.nl/)
|
||||
- [Graphviz](https://graphviz.org/)
|
||||
|
||||
Install the Python dependencies from this directory:
|
||||
|
||||
```shell
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Both `doxygen` and `dot` must be available on `PATH`. For the standard Windows
|
||||
install locations, this can be done for the current PowerShell session with:
|
||||
|
||||
```powershell
|
||||
$env:Path = "C:\Program Files\doxygen\bin;C:\Program Files\Graphviz\bin;$env:Path"
|
||||
```
|
||||
This folder contains the setup to build the IfcOpenShell C++ API documentation from the source code.
|
||||
|
||||
## Generating the documentation
|
||||
|
||||
From this directory, run:
|
||||
> Prerequisites:
|
||||
>
|
||||
> Make sure to have [Doxygen](https://www.doxygen.nl) and [Graphviz](https://graphviz.org) installed into your `$PATH` variable.
|
||||
>
|
||||
> The documentation also use the [doxygen-awesome](https://jothepro.github.io/doxygen-awesome-css) theme as a git submodule.
|
||||
|
||||
Build with the command (from within the `/docs/cpp-api` folder):
|
||||
|
||||
```shell
|
||||
python -m sphinx -M html . output -W --keep-going
|
||||
$ doxygen
|
||||
```
|
||||
|
||||
To include the current Git commit in Doxygen's project metadata, set
|
||||
`PROJECT_NUMBER` before building. For example, in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:PROJECT_NUMBER = git rev-parse --short HEAD
|
||||
python -m sphinx -M html . output -W --keep-going
|
||||
```
|
||||
|
||||
Or in a POSIX shell:
|
||||
To include the current git commit hash into the build documentation, use the following command:
|
||||
|
||||
```shell
|
||||
PROJECT_NUMBER=$(git rev-parse --short HEAD) python -m sphinx -M html . output -W --keep-going
|
||||
$ PROJECT_NUMBER=$(git rev-parse --short HEAD) doxygen
|
||||
```
|
||||
|
||||
Alternatively, configure the main CMake project with
|
||||
`-DBUILD_DOCUMENTATION=ON` and build the `cpp_api_docs` target.
|
||||
This will extract the current commit hash in short version and sets the propper ENV variable used by doxygen.
|
||||
|
||||
The generated documentation is written to `output/html/index.html`. The
|
||||
generated Doxygen XML and Exhale sources are kept under `output/` as build
|
||||
artifacts.
|
||||
The generation of the documentation might take a while depending on your systems hardware, as it is configured to generate the Class graphs using .
|
||||
|
||||
The generated headers under `src/ifcparse/schemas` are intentionally excluded
|
||||
from this documentation build.
|
||||
The resulting documentation is located unter `/cpp-api/output/html` and can be directly accessed with your browser:
|
||||
|
||||
```shell
|
||||
$ open ./output/html/index.html
|
||||
```
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
|
||||
from sphinx.deprecation import RemovedInSphinx90Warning
|
||||
|
||||
warnings.filterwarnings("ignore", category=RemovedInSphinx90Warning, module=r"exhale\.configs")
|
||||
|
||||
generated_directories = (
|
||||
Path(__file__).parent / "output" / "api",
|
||||
Path(__file__).parent / "output" / "doxygen",
|
||||
)
|
||||
for generated_directory in generated_directories:
|
||||
if generated_directory.is_dir():
|
||||
rmtree(generated_directory)
|
||||
|
||||
project = "IfcOpenShell"
|
||||
copyright = "2020, IfcOpenShell"
|
||||
|
||||
extensions = [
|
||||
"breathe",
|
||||
"exhale",
|
||||
]
|
||||
|
||||
primary_domain = "cpp"
|
||||
highlight_language = "cpp"
|
||||
html_theme = "alabaster"
|
||||
|
||||
breathe_projects = {
|
||||
"IfcOpenShell": "./output/doxygen/xml",
|
||||
}
|
||||
breathe_default_project = "IfcOpenShell"
|
||||
|
||||
exhale_args = {
|
||||
"containmentFolder": "./output/api",
|
||||
"rootFileName": "library_root.rst",
|
||||
"rootFileTitle": "IfcOpenShell C++ API",
|
||||
"doxygenStripFromPath": "../..",
|
||||
"createTreeView": False,
|
||||
"exhaleExecutesDoxygen": True,
|
||||
"exhaleUseDoxyfile": True,
|
||||
}
|
||||
|
||||
cpp_id_attributes = [
|
||||
"IFC_PARSE_API",
|
||||
"IFC_SCHEMA_API",
|
||||
"IFC_GEOM_API",
|
||||
"IFC_GEOMLIBRARY_API",
|
||||
"IFC_GEOMSERIALIZATION_API",
|
||||
"SERIALIZERS_API",
|
||||
]
|
||||
|
||||
exclude_patterns = [
|
||||
"output/doctrees",
|
||||
"output/doxygen",
|
||||
"output/html",
|
||||
]
|
||||
@@ -1,9 +0,0 @@
|
||||
.. This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
IfcOpenShell C++ API
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
output/api/library_root
|
||||
@@ -1,5 +0,0 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
Sphinx==8.1.3
|
||||
breathe==4.36.0
|
||||
exhale==0.3.7
|
||||
-317
@@ -1,317 +0,0 @@
|
||||
# Build fix: remove `boost_system` from CMake components
|
||||
|
||||
`Boost.System` became header-only in Boost 1.69. Boost 1.90.0 no longer ships a compiled library or CMake config for it, so `find_package(Boost REQUIRED COMPONENTS system ...)` fails.
|
||||
|
||||
## Fix
|
||||
|
||||
`cmake/CMakeLists.txt`:
|
||||
|
||||
```diff
|
||||
- set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
|
||||
+ set(BOOST_COMPONENTS program_options regex thread date_time iostreams)
|
||||
```
|
||||
|
||||
The headers are still available; no linking is needed.
|
||||
|
||||
# Build fix: add `template` keyword for dependent template member calls
|
||||
|
||||
Calling a template member function through a dependent expression (e.g. `storage->has_attribute_value<T>(...)` where `storage`'s type depends on a template parameter) requires the `template` keyword to disambiguate from a less-than comparison.
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
src/ifcparse/IfcParse.cpp:1856:67: error: expected primary-expression before '>' token
|
||||
1856 | if (storage->has_attribute_value<express::Base>(attr_index)) {
|
||||
| ^
|
||||
```
|
||||
|
||||
Six identical errors at lines 1856, 1865, 1896, 1905, 1934, 1943.
|
||||
|
||||
## Fix
|
||||
|
||||
`src/ifcparse/IfcParse.cpp`:
|
||||
|
||||
```diff
|
||||
-storage->has_attribute_value<express::Base>(attr_index)
|
||||
+storage->template has_attribute_value<express::Base>(attr_index)
|
||||
|
||||
-storage->has_attribute_value<Blank>(attr_index)
|
||||
+storage->template has_attribute_value<Blank>(attr_index)
|
||||
```
|
||||
|
||||
Applied at all six call sites in `in_memory_file_storage::read_from_stream`.
|
||||
|
||||
# Linker fix: missing explicit template instantiations for `InstanceStreamer`
|
||||
|
||||
`InstanceStreamer` is a class template with methods defined in `IfcParse.cpp`, not the header. Without explicit instantiations, the linker can't find the symbols when the SWIG wrapper loads.
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPS3_PNS_7IfcFileE
|
||||
(IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcFile*))
|
||||
```
|
||||
|
||||
## Fix
|
||||
|
||||
Cannot use `template class InstanceStreamer<...>` because some constructors have `static_assert` guards that reject certain reader types. Instead, instantiate each member function individually per reader type, only including the constructors valid for that type.
|
||||
|
||||
`src/ifcparse/IfcParse.cpp` (after the last `InstanceStreamer` method definition):
|
||||
|
||||
```cpp
|
||||
// FullBufferImpl
|
||||
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(IfcParse::IfcFile*);
|
||||
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(const std::string&, bool, IfcParse::IfcFile*);
|
||||
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcParse::IfcFile*);
|
||||
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcParse::IfcFile*);
|
||||
// ... plus ensure_header, initialize_header, hasSemicolon, semicolonCount,
|
||||
// pushPage, bypassTypes, readInstance
|
||||
|
||||
// PushedSequentialImpl — same pattern, different valid constructors
|
||||
|
||||
// MMapFileReader (ifdef USE_MMAP) — same pattern
|
||||
```
|
||||
|
||||
# Linker fix: `FullBufferImpl` missing buffer constructor
|
||||
|
||||
SWIG's `stream_from_string` calls `InstanceStreamer<FileReader<FullBufferImpl>>(void*, int, IfcFile*)`, but the `(void*, int)` constructor previously hit a `static_assert` for `FullBufferImpl` — it only allowed `PushedSequentialImpl`.
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPviPNS_7IfcFileE
|
||||
(InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcFile*))
|
||||
```
|
||||
|
||||
## Fix
|
||||
|
||||
Three changes to make `FullBufferImpl` support buffer-based and default construction:
|
||||
|
||||
`src/ifcparse/FileReader.h` — add buffer constructor to `FullBufferImpl`:
|
||||
|
||||
```diff
|
||||
class IFC_PARSE_API FullBufferImpl {
|
||||
public:
|
||||
explicit FullBufferImpl(const std::string& fn);
|
||||
+ FullBufferImpl(void* data, size_t length);
|
||||
```
|
||||
|
||||
`src/ifcparse/FileReader.h` — add `FileReader(void*, size_t)` forwarding constructor:
|
||||
|
||||
```diff
|
||||
+ FileReader(void* data, size_t length)
|
||||
+ : cursor_(0) {
|
||||
+ if constexpr (std::is_same_v<Impl, FullBufferImpl>) {
|
||||
+ impl_ = std::make_shared<Impl>(data, length);
|
||||
+ } else {
|
||||
+ static_assert(...);
|
||||
+ }
|
||||
+ }
|
||||
```
|
||||
|
||||
`src/ifcparse/FileReader.cpp` — implement the constructor:
|
||||
|
||||
```cpp
|
||||
FullBufferImpl::FullBufferImpl(void* data, size_t length)
|
||||
: buf_(static_cast<char*>(data), static_cast<char*>(data) + length)
|
||||
, size_(length) {
|
||||
}
|
||||
```
|
||||
|
||||
`src/ifcparse/IfcParse.cpp` — extend the two `InstanceStreamer` constructors to accept `FullBufferImpl`:
|
||||
|
||||
```diff
|
||||
// InstanceStreamer(IfcFile*):
|
||||
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
|
||||
+ owned_stream_ = std::make_unique<Reader>(nullptr, (size_t)0);
|
||||
|
||||
// InstanceStreamer(void*, int, IfcFile*):
|
||||
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
|
||||
+ owned_stream_ = std::make_unique<Reader>(data, (size_t)length);
|
||||
```
|
||||
|
||||
# Runtime fix: segfault in `parse_context::push()` due to vector reallocation
|
||||
|
||||
`parse_context_pool` stores nodes in a `std::vector<parse_context>`. During parsing, `load()` takes a `parse_context&` parameter and calls `context.push()`, which calls `pool_->make()`. If the pool's vector reallocates (via `emplace_back`), all existing references into the vector — including the `context` reference held by the caller — become dangling. Subsequent access through the dangling reference causes a segfault.
|
||||
|
||||
Triggered by larger IFC files (e.g. `ISSUE_159_kleine_Wohnung_R22.ifc`, 9.5 MB) that cause enough pool growth to trigger reallocation.
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Thread 1 received signal SIGSEGV, Segmentation fault.
|
||||
0x... in IfcParse::parse_context::push()
|
||||
#1 in_memory_file_storage::load(...) // context& is dangling after reallocation
|
||||
#2 in_memory_file_storage::load(...) // parent call
|
||||
#3 InstanceStreamer::readInstance()
|
||||
```
|
||||
|
||||
## Fix
|
||||
|
||||
`src/ifcparse/storage.h` — change the pool container from `std::vector` to `std::deque`, which does not invalidate references on `push_back`/`emplace_back`:
|
||||
|
||||
```diff
|
||||
+#include <deque>
|
||||
|
||||
struct parse_context_pool {
|
||||
- std::vector<parse_context> nodes_;
|
||||
+ std::deque<parse_context> nodes_;
|
||||
```
|
||||
|
||||
# Runtime fix: `express::Base` comparison operators throw on null/expired instances
|
||||
|
||||
`express::Base::operator<` and `operator==` called `data()`, which throws `std::runtime_error("Trying to access deleted instance reference")` when the internal `weak_ptr` is expired. A default-constructed `express::Base` (the value-type equivalent of a null pointer) always has an expired `weak_ptr`.
|
||||
|
||||
## Why this model triggers it
|
||||
|
||||
The bug requires two conditions to coincide:
|
||||
|
||||
1. A representation is shared by **more than one product** (via `IfcRepresentationMap` / `IfcMappedItem`).
|
||||
2. At least one of those products has **no material association**, so `get_single_material_association()` returns `express::Base{}` (the null equivalent).
|
||||
|
||||
In `advanced_model.ifc`, Body representations like `#449` (Body/Brep) have a single `IfcRepresentationMap` (`#453`) with 13 `IfcMappedItem` usages, meaning 13 products share the geometry. Some of those products (e.g. `IfcFlowTerminal` instances) have no `IfcRelAssociatesMaterial`, so `get_single_material_association` returns `express::Base{}`.
|
||||
|
||||
Smaller or simpler models don't hit this because either:
|
||||
- Every representation maps to only 1 product → `reuse_ok_` short-circuits at `products.size() == 1` before reaching the material check.
|
||||
- Every product has a material association → no null `express::Base` is ever inserted into the set.
|
||||
|
||||
## Exact call sequence
|
||||
|
||||
```
|
||||
Iterator::initialize()
|
||||
try {
|
||||
mapping::get_representations(reps, filters_)
|
||||
addRepresentationsFromDefaultContexts(representations)
|
||||
→ collects reps from subcontexts in order:
|
||||
Axis (#115): 143 reps
|
||||
Body (#117): 7550 reps
|
||||
FootPrint (#119): 12 reps
|
||||
|
||||
for (auto representation : representations):
|
||||
|
||||
── Axis reps (indices 0–142) ──────────────────────────
|
||||
products_represented_by(rep, rmap)
|
||||
→ OfProductRepresentation: 1 product each
|
||||
filter_products(products, filters) → 1 product
|
||||
reuse_ok_(ifcproducts)
|
||||
→ products.size() == 1 → return true ← SHORT-CIRCUIT, no material check
|
||||
representation_mapped_to(rep) → null (no MappedItem)
|
||||
→ task created. 143 tasks accumulated.
|
||||
|
||||
── First Body rep #449 (Body/Brep) ────────────────────
|
||||
products_represented_by(#449, rmap)
|
||||
→ OfProductRepresentation: empty
|
||||
→ RepresentationMap: 1 map (#453)
|
||||
→ MapUsage: 13 MappedItems → traces through to 13 IfcProducts
|
||||
filter_products(products, filters) → 13 products
|
||||
reuse_ok_(ifcproducts) ← CRASH HERE
|
||||
→ products.size() == 1? NO (13 products)
|
||||
→ for each product:
|
||||
find_openings(product) → OK
|
||||
get_single_material_association(product)
|
||||
→ some products have no IfcRelAssociatesMaterial
|
||||
→ returns express::Base{} (expired weak_ptr)
|
||||
associated_single_materials.insert(result)
|
||||
→ std::set::insert calls operator<
|
||||
→ operator< calls data()
|
||||
→ data() calls data_.lock() → expired → THROWS
|
||||
"Trying to access deleted instance reference"
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
Logger::Error(e) ← exception caught here, get_representations aborted
|
||||
}
|
||||
|
||||
→ reps contains only the 143 Axis tasks created before the throw
|
||||
→ all 143 Axis reps have Curve2D geometry → map(representation) returns null
|
||||
→ no valid elements produced → initialize() returns false
|
||||
```
|
||||
|
||||
In the old pointer-based code, `reuse_ok_` used `std::set<const IfcUtil::IfcBaseEntity*>` and `get_single_material_association` returned `nullptr`. Inserting `nullptr` into a `std::set<T*>` is a plain pointer comparison — no dereference, no throw. The refactoring to `std::set<express::Base>` changed the comparison from pointer comparison to `express::Base::operator<`, which unconditionally dereferences through `data()`.
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
[Error] Trying to access deleted instance reference
|
||||
[Notice] Created 143 tasks for 143 products ← only Axis reps; all Body reps lost
|
||||
initialize() returned: False
|
||||
```
|
||||
|
||||
## Fix
|
||||
|
||||
`src/ifcparse/express.h` — use `weak_ptr::lock().get()` instead of `data()` so that expired pointers compare as `nullptr` (matching old raw-pointer semantics):
|
||||
|
||||
```diff
|
||||
bool operator<(const Base& other) const {
|
||||
- return data() < other.data();
|
||||
+ auto a = data_.lock();
|
||||
+ auto b = other.data_.lock();
|
||||
+ return a.get() < b.get();
|
||||
}
|
||||
|
||||
bool operator==(const Base& other) const {
|
||||
- return data() == other.data();
|
||||
+ auto a = data_.lock();
|
||||
+ auto b = other.data_.lock();
|
||||
+ return a.get() == b.get();
|
||||
}
|
||||
```
|
||||
|
||||
# Runtime fix: `entity_instance` missing `get_inverse` due to SWIG `%rename` collision
|
||||
|
||||
Accessing inverse attributes (e.g. `element.IsDecomposedBy`) on any entity raises `AttributeError: entity instance of type 'IFC2X3.IfcProject' has no attribute 'get_inverse'`.
|
||||
|
||||
## Why
|
||||
|
||||
`entity_instance_mixin.__getattr__` (line 106 of `entity_instance.py`) calls `self.get_inverse(name)` when it detects an inverse attribute. Since the mixin inherits into the SWIG-generated `entity_instance` class (via the `object = custom_base` hack in `IfcParseWrapper.i:936`), `self.get_inverse` must resolve to a method on the SWIG class.
|
||||
|
||||
However, `IfcParseWrapper.i:70` has a global rename:
|
||||
|
||||
```
|
||||
%rename("get_inverses_by_declaration") get_inverse;
|
||||
```
|
||||
|
||||
This was intended for `ifcopenshell::file::get_inverse` (which takes an entity + declaration and returns instances by reference), but SWIG `%rename` is global — it also renames the `%extend express::Base` method `get_inverse(const std::string& a)` at line 551. So the Python-side `entity_instance` class exposes the method as `get_inverses_by_declaration`, not `get_inverse`.
|
||||
|
||||
The old code (`v0.8.0`) didn't hit this because `__getattr__` called `self.wrapped_data.get_inverse(name)` on an inner `ifcopenshell_wrapper.entity_instance` object — but in that old layout, the inner object was constructed differently and the rename didn't apply the same way (or the method had a different path). In the new mixin approach, `self` **is** the SWIG object, so the rename is directly visible.
|
||||
|
||||
## Fix
|
||||
|
||||
`src/ifcwrap/IfcParseWrapper.i` — override the global rename specifically for `express::Base::get_inverse`, restoring the original name on entity instances:
|
||||
|
||||
```diff
|
||||
+%rename("get_inverse") express::Base::get_inverse;
|
||||
%rename("get_inverses_by_declaration") get_inverse;
|
||||
```
|
||||
|
||||
Add this line **before** the global rename (or anywhere before the `%extend express::Base` block). This scoped rename takes precedence for `express::Base`, so:
|
||||
- `entity_instance.get_inverse(name)` works as the mixin expects
|
||||
- `file.get_inverses_by_declaration(...)` keeps its intended name
|
||||
|
||||
## Python-side workaround
|
||||
|
||||
`entity_instance.py:106` — call the method by its SWIG-renamed name:
|
||||
|
||||
```diff
|
||||
- vs = self.get_inverse(name)
|
||||
+ vs = self.get_inverses_by_declaration(name)
|
||||
```
|
||||
|
||||
# Runtime fix: `entity_instance` class no longer importable from `entity_instance` module
|
||||
|
||||
The class rename from `entity_instance` to `entity_instance_mixin` broke external code that does `from ifcopenshell.entity_instance import entity_instance`.
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
ImportError: cannot import name 'entity_instance' from 'ifcopenshell.entity_instance'
|
||||
```
|
||||
|
||||
Triggered at import time via `ifcopenshell.util.pset` (and likely other modules).
|
||||
|
||||
## Fix
|
||||
|
||||
`src/ifcopenshell-python/ifcopenshell/entity_instance.py` — add a backwards-compatible alias at the bottom of the module:
|
||||
|
||||
```python
|
||||
entity_instance = entity_instance_mixin
|
||||
```
|
||||
+199
-339
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,7 @@ def unpack_dependencies(install_dir: Path) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
action = None
|
||||
if len(sys.argv) != 2 or (action := sys.argv[1].lower()) not in ("pack", "unpack"):
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 42e403b..764562f 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -249,11 +249,6 @@ set_source_files_properties(
|
||||
PROPERTIES GENERATED TRUE
|
||||
)
|
||||
|
||||
-# If it's an EMSCRIPTEN build, we're done
|
||||
-if(EMSCRIPTEN)
|
||||
- return()
|
||||
-endif()
|
||||
-
|
||||
# CMake exports
|
||||
configure_file(
|
||||
cmake/manifoldConfig.cmake.in
|
||||
+1
-1
@@ -7,7 +7,7 @@ index d765b108..6800c2a7 100644
|
||||
|
||||
# Set the library variable
|
||||
-if(UNIX)
|
||||
+if(@USE_SHARED@)
|
||||
+if(0)
|
||||
set(OPENCOLLADA_LIBRARIES
|
||||
ftoa_shared
|
||||
buffer_shared
|
||||
@@ -34,6 +34,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-13.3.0-h9576a4e_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-13.3.0-hdbfa832_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-13.3.0-h6834431_7.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.4-nompi_h2d575fe_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/imath-3.1.12-h7955e40_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda
|
||||
@@ -148,6 +149,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/freeimage-3.18.0-h7cd8ba8_22.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.4-nompi_h1607680_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.1.12-h2016aa1_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/jxrlib-1.1-h10d778d_3.conda
|
||||
@@ -241,6 +243,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/freeimage-3.18.0-h8310ca0_22.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.4-nompi_hd5d9e70_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/imath-3.1.12-hbb528cf_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-hcfcfb64_3.conda
|
||||
@@ -346,6 +349,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-13.3.0-h9576a4e_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-13.3.0-hdbfa832_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-13.3.0-h6834431_7.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.4-nompi_h2d575fe_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/imath-3.1.12-h7955e40_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda
|
||||
@@ -460,6 +464,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/freeimage-3.18.0-h7cd8ba8_22.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.4-nompi_h1607680_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.1.12-h2016aa1_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/jxrlib-1.1-h10d778d_3.conda
|
||||
@@ -553,6 +558,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/freeimage-3.18.0-h8310ca0_22.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.4-nompi_hd5d9e70_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/imath-3.1.12-hbb528cf_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-hcfcfb64_3.conda
|
||||
@@ -737,6 +743,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-13.3.0-h9576a4e_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-13.3.0-hdbfa832_1.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-13.3.0-h6834431_7.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.4-nompi_h2d575fe_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/imath-3.1.12-h7955e40_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/jxrlib-1.1-hd590300_3.conda
|
||||
@@ -851,6 +858,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/freeimage-3.18.0-h7cd8ba8_22.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.4-nompi_h1607680_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/imath-3.1.12-h2016aa1_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/jxrlib-1.1-h10d778d_3.conda
|
||||
@@ -944,6 +952,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/freeimage-3.18.0-h8310ca0_22.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.12.1-hdaf720e_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.4-nompi_hd5d9e70_105.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/imath-3.1.12-hbb528cf_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-hcfcfb64_3.conda
|
||||
@@ -1059,6 +1068,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-he663afc_4.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-ha7acb78_11.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h6e4c0c1_103.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
|
||||
@@ -1229,6 +1239,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/geos-3.13.1-h502464c_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_hc8237f9_103.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda
|
||||
@@ -1369,6 +1380,7 @@ environments:
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.13.1-h9ea8674_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/gmp-6.3.0-hfeafd45_2.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_he30205f_103.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda
|
||||
@@ -2978,6 +2990,106 @@ packages:
|
||||
- pkg:pypi/h2?source=compressed-mapping
|
||||
size: 95967
|
||||
timestamp: 1756364871835
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.4-nompi_h2d575fe_105.conda
|
||||
sha256: 93d2bfc672f3ee0988d277ce463330a467f3686d3f7ee37812a3d8ca11776d77
|
||||
md5: d76fff0092b6389a12134ddebc0929bd
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- libaec >=1.1.3,<2.0a0
|
||||
- libcurl >=8.10.1,<9.0a0
|
||||
- libgcc >=13
|
||||
- libgfortran
|
||||
- libgfortran5 >=13.3.0
|
||||
- libstdcxx >=13
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- openssl >=3.4.0,<4.0a0
|
||||
license: BSD-3-Clause
|
||||
license_family: BSD
|
||||
size: 3950601
|
||||
timestamp: 1733003331788
|
||||
- conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h6e4c0c1_103.conda
|
||||
sha256: 4f173af9e2299de7eee1af3d79e851bca28ee71e7426b377e841648b51d48614
|
||||
md5: c74d83614aec66227ae5199d98852aaf
|
||||
depends:
|
||||
- __glibc >=2.17,<3.0.a0
|
||||
- libaec >=1.1.4,<2.0a0
|
||||
- libcurl >=8.14.1,<9.0a0
|
||||
- libgcc >=14
|
||||
- libgfortran
|
||||
- libgfortran5 >=14.3.0
|
||||
- libstdcxx >=14
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- openssl >=3.5.1,<4.0a0
|
||||
license: BSD-3-Clause
|
||||
license_family: BSD
|
||||
purls: []
|
||||
size: 3710057
|
||||
timestamp: 1753357500665
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.4-nompi_h1607680_105.conda
|
||||
sha256: 56500937894b1ca917e1ae1bea64b873a9eec57d581173579189d0b1f590db26
|
||||
md5: 12ebafc40b10d4bf519e4c2074c52aef
|
||||
depends:
|
||||
- __osx >=10.13
|
||||
- libaec >=1.1.3,<2.0a0
|
||||
- libcurl >=8.10.1,<9.0a0
|
||||
- libcxx >=18
|
||||
- libgfortran 5.*
|
||||
- libgfortran5 >=13.2.0
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- openssl >=3.4.0,<4.0a0
|
||||
license: BSD-3-Clause
|
||||
license_family: BSD
|
||||
size: 3732340
|
||||
timestamp: 1733003702265
|
||||
- conda: https://conda.anaconda.org/conda-forge/osx-64/hdf5-1.14.6-nompi_hc8237f9_103.conda
|
||||
sha256: e41d22f672b1fbe713d22cf69630abffaee68bdb38a500a708fc70e6f639357f
|
||||
md5: 3f1df98f96e0c369d94232712c9b87d0
|
||||
depends:
|
||||
- __osx >=10.13
|
||||
- libaec >=1.1.4,<2.0a0
|
||||
- libcurl >=8.14.1,<9.0a0
|
||||
- libcxx >=19
|
||||
- libgfortran
|
||||
- libgfortran5 >=14.3.0
|
||||
- libgfortran5 >=15.1.0
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- openssl >=3.5.1,<4.0a0
|
||||
license: BSD-3-Clause
|
||||
license_family: BSD
|
||||
purls: []
|
||||
size: 3522832
|
||||
timestamp: 1753358062940
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.4-nompi_hd5d9e70_105.conda
|
||||
sha256: e8ced65c604a3b9e4803758a25149d71d8096f186fe876817a0d1d97190550c0
|
||||
md5: 4381be33460283890c34341ecfa42d97
|
||||
depends:
|
||||
- libaec >=1.1.3,<2.0a0
|
||||
- libcurl >=8.10.1,<9.0a0
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- openssl >=3.4.0,<4.0a0
|
||||
- ucrt >=10.0.20348.0
|
||||
- vc >=14.2,<15
|
||||
- vc14_runtime >=14.29.30139
|
||||
license: BSD-3-Clause
|
||||
license_family: BSD
|
||||
size: 2048450
|
||||
timestamp: 1733003052575
|
||||
- conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_he30205f_103.conda
|
||||
sha256: 0a90263b97e9860cec6c2540160ff1a1fff2a609b3d96452f8716ae63489dac5
|
||||
md5: f1f7aaf642cefd2190582550eaca4658
|
||||
depends:
|
||||
- libaec >=1.1.4,<2.0a0
|
||||
- libcurl >=8.14.1,<9.0a0
|
||||
- libzlib >=1.3.1,<2.0a0
|
||||
- openssl >=3.5.1,<4.0a0
|
||||
- ucrt >=10.0.20348.0
|
||||
- vc >=14.3,<15
|
||||
- vc14_runtime >=14.44.35208
|
||||
license: BSD-3-Clause
|
||||
license_family: BSD
|
||||
purls: []
|
||||
size: 2031491
|
||||
timestamp: 1753357255237
|
||||
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
|
||||
sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba
|
||||
md5: 0a802cb9888dd14eeefc611f05c40b6e
|
||||
|
||||
@@ -31,6 +31,7 @@ occt = { version = "*", build = "*novtk*" }
|
||||
cgal-cpp = "*"
|
||||
numpy = "*"
|
||||
lark = "*"
|
||||
hdf5 = "*"
|
||||
eigen = "*"
|
||||
mpfr = "*"
|
||||
gmp = "*"
|
||||
|
||||
+2
-10
@@ -28,13 +28,5 @@ since it's pure cmake without any additional moving parts.
|
||||
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
|
||||
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
|
||||
- it will produce Python package in `IfcOpenShell/ifcopenshell`
|
||||
- run `python pyodide/build-all-pack-wheel-local.py`, it will
|
||||
- clean up previous wheels
|
||||
- run `pyodide build`
|
||||
- prepare standalone and modular wheels
|
||||
- produce final wheels in `IfcOpenShell/dist` and `IfcOpenshell/dist-modular`
|
||||
- testing:
|
||||
- ensure you're in pyodide environment
|
||||
- `cd IfcOpenshell/pyodide`
|
||||
- `./run_pytest.py setup`
|
||||
- `./run_pytest.py run`
|
||||
- run `pyodide build`
|
||||
- it will produce a wheel in `IfcOpenShell/dist`
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Intended to be run after nix/build-all.py has finished the wasm build."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
output = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True)
|
||||
return Path(output.strip())
|
||||
|
||||
|
||||
def run(cmd: list[str], **kwargs) -> None:
|
||||
print("$", " ".join(cmd))
|
||||
subprocess.check_call(cmd, **kwargs)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
repo_root = get_repo_root()
|
||||
|
||||
shutil.rmtree(repo_root / "dist", ignore_errors=True)
|
||||
shutil.rmtree(repo_root / "dist_modular", ignore_errors=True)
|
||||
run(["pyodide", "build"], cwd=repo_root)
|
||||
shutil.rmtree(repo_root / "ifcopenshell", ignore_errors=True)
|
||||
(repo_root / "setup.py").unlink(missing_ok=True)
|
||||
run(["git", "restore", "pyproject.toml"], cwd=repo_root)
|
||||
|
||||
wheel = next((repo_root / "dist").glob("ifcopenshell-*.whl"))
|
||||
|
||||
run(["uv", "run", "pyodide/order_pyodide_wheel_shared_objects.py", str(wheel)], cwd=repo_root)
|
||||
run(
|
||||
["uv", "run", "pyodide/split_pyodide_ifcopenshell_wheel.py", str(wheel), "dist-modular/"],
|
||||
cwd=repo_root,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
PYODIDE_VERSION=0.29.4
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PYODIDE_VERSION=0.29.3
|
||||
PYODIDE_BUILD_VERSION=0.33.0
|
||||
PYODIDE_XBUILDENV_ROOT="${HOME}/.cache/.pyodide-xbuildenv-${PYODIDE_BUILD_VERSION}"
|
||||
PYODIDE_XBUILDENV="${PYODIDE_XBUILDENV_ROOT}/${PYODIDE_VERSION}"
|
||||
|
||||
# Script is assuming that it will be possible to execute it multiple times
|
||||
# therefore we're clearing venv each time and ignoring existing 'emsdk' folder.
|
||||
@@ -14,36 +16,25 @@ source .venv/bin/activate
|
||||
|
||||
# Install pyodide cross build environment.
|
||||
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
|
||||
uv pip install -r "${SCRIPT_DIR}/requirements.txt"
|
||||
uv pip install "pyodide-build==${PYODIDE_BUILD_VERSION}"
|
||||
# `uv run` is required, so xbuildenv would skip using `pip`.
|
||||
uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
|
||||
uv run pyodide xbuildenv install-emscripten
|
||||
|
||||
# Cache path includes a hash segment that varies by pyodide-build version,
|
||||
# so query it instead of constructing it manually.
|
||||
EMSDK_ROOT=$(uv run pyodide config get emsdk_dir)
|
||||
[ -f "${EMSDK_ROOT}/emsdk_env.sh" ] && source "${EMSDK_ROOT}/emsdk_env.sh"
|
||||
[ -f "${EMSDK_ROOT}/../../emsdk_env.sh" ] && source "${EMSDK_ROOT}/../../emsdk_env.sh"
|
||||
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
|
||||
source "${EMSDK_ROOT}/emsdk_env.sh"
|
||||
which emcc
|
||||
emcc --version
|
||||
|
||||
mkdir -p packages/ifcopenshell
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
# Normalize to the canonical PEP 440 form (e.g. 0.9.0alpha0 -> 0.9.0a0).
|
||||
VERSION=`python3 -c "from packaging.version import Version; print(Version('$VERSION'))"`
|
||||
cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell
|
||||
sed -i s/9.9.9/$VERSION/g packages/ifcopenshell/meta.yaml
|
||||
sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
|
||||
|
||||
# Use custom build ifcopenshell directory in build-all to make caching simpler
|
||||
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
|
||||
export BUILD_DIR=`readlink -f ifcopenshell_build`
|
||||
|
||||
# Sat, 25 Apr 2026 12:11:39 GMT 2026-04-25 12:11:39,173 - DEBUG - running
|
||||
# command `make -j5 ifcopenshell_wrapper VERBOSE=1` in directory
|
||||
# '/home/runner/work/IfcOpenShell/IfcOpenShell/ifcopenshell_build/Linux/wasm/build/ifcopenshell/build'
|
||||
# Sat, 25 Apr 2026 12:18:01 GMT Error: Process completed with exit code 143.
|
||||
export IFCOS_NUM_BUILD_PROCS=1
|
||||
|
||||
# Use build-recipes-no-deps first, so logs would be printed to stdout.
|
||||
pyodide build-recipes-no-deps ifcopenshell
|
||||
pyodide build-recipes ifcopenshell --install
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
package:
|
||||
name: ifcopenshell
|
||||
# Placeholder, replaced by build_pyodide.sh with the actual version from VERSION file.
|
||||
version: 9.9.9
|
||||
version: 0.8.0
|
||||
|
||||
source:
|
||||
# meta.yaml is placed as `packages/ifcopenshell/meta.yaml`.
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/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.
|
||||
|
||||
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
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_ORDER = {
|
||||
"ifc2x3": 0,
|
||||
"ifc4": 1,
|
||||
"ifc4x1": 2,
|
||||
"ifc4x2": 3,
|
||||
"ifc4x3": 4,
|
||||
"ifc4x3_add1": 5,
|
||||
"ifc4x3_add2": 6,
|
||||
}
|
||||
|
||||
MAIN_SHARED_OBJECT_RE = re.compile(r"^_ifcopenshell_wrapper(?:\.|$)")
|
||||
SCHEMA_PLUGIN_RE = re.compile(r"^ifcopenshell_parse_schema_(.+)\.so$")
|
||||
MAPPING_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_mapping_(.+)\.so$")
|
||||
DOCUMENT_PLUGIN_RE = re.compile(r"^ifcopenshell_document_[a-z0-9]+(?:_(.+))?\.so$")
|
||||
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell_geometry_writer_(.+)\.so$")
|
||||
|
||||
|
||||
def schema_key(schema: str) -> tuple[int, str]:
|
||||
schema = schema.lower()
|
||||
return SCHEMA_ORDER.get(schema, len(SCHEMA_ORDER)), schema
|
||||
|
||||
|
||||
def shared_object_sort_key(filename: str, index: int) -> tuple[int, tuple[int, str], str, int]:
|
||||
basename = Path(filename).name
|
||||
if MAIN_SHARED_OBJECT_RE.match(basename):
|
||||
return 0, schema_key(""), basename, index
|
||||
|
||||
if match := SCHEMA_PLUGIN_RE.match(basename):
|
||||
return 1, schema_key(match.group(1)), basename, index
|
||||
|
||||
if match := MAPPING_PLUGIN_RE.match(basename):
|
||||
return 2, schema_key(match.group(1)), basename, index
|
||||
|
||||
if match := DOCUMENT_PLUGIN_RE.match(basename):
|
||||
return 3, schema_key(match.group(1)), basename, index
|
||||
|
||||
if match := GEOMETRY_SERIALIZATION_PLUGIN_RE.match(basename):
|
||||
return 4, schema_key(match.group(1)), basename, index
|
||||
|
||||
return 5, schema_key(""), basename, index
|
||||
|
||||
|
||||
def ordered_infos(infos: list[zipfile.ZipInfo]) -> list[zipfile.ZipInfo]:
|
||||
shared_infos = [(index, info) for index, info in enumerate(infos) if info.filename.endswith(".so")]
|
||||
ordered_shared_infos = [
|
||||
info for index, info in sorted(shared_infos, key=lambda item: shared_object_sort_key(item[1].filename, item[0]))
|
||||
]
|
||||
ordered_shared_iter = iter(ordered_shared_infos)
|
||||
return [next(ordered_shared_iter) if info.filename.endswith(".so") else info for info in infos]
|
||||
|
||||
|
||||
def zip_info_for_write(source: zipfile.ZipInfo) -> zipfile.ZipInfo:
|
||||
info = zipfile.ZipInfo(source.filename)
|
||||
info.date_time = source.date_time
|
||||
info.compress_type = source.compress_type
|
||||
info.comment = source.comment
|
||||
info.create_system = source.create_system
|
||||
info.external_attr = source.external_attr
|
||||
info.extra = source.extra
|
||||
return info
|
||||
|
||||
|
||||
def shared_object_names(infos: list[zipfile.ZipInfo]) -> list[str]:
|
||||
return [info.filename for info in infos if info.filename.endswith(".so")]
|
||||
|
||||
|
||||
def rewrite_wheel(wheel: Path, ordered: list[zipfile.ZipInfo]) -> None:
|
||||
fd, temp_name = tempfile.mkstemp(prefix=f".{wheel.name}.", suffix=".tmp", dir=wheel.parent)
|
||||
os.close(fd)
|
||||
temp_path = Path(temp_name)
|
||||
try:
|
||||
with zipfile.ZipFile(wheel) as zin, zipfile.ZipFile(
|
||||
temp_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
|
||||
) as zout:
|
||||
for info in ordered:
|
||||
zout.writestr(zip_info_for_write(info), zin.read(info))
|
||||
os.replace(temp_path, wheel)
|
||||
finally:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
|
||||
|
||||
def order_wheel(wheel: Path, check: bool) -> bool:
|
||||
wheel = wheel.resolve()
|
||||
if wheel.suffix != ".whl":
|
||||
raise ValueError(f"not a wheel: {wheel}")
|
||||
|
||||
with zipfile.ZipFile(wheel) as zf:
|
||||
infos = zf.infolist()
|
||||
|
||||
ordered = ordered_infos(infos)
|
||||
changed = shared_object_names(infos) != shared_object_names(ordered)
|
||||
if check:
|
||||
if changed:
|
||||
print(f"{wheel}: shared object order needs updating")
|
||||
return False
|
||||
print(f"{wheel}: shared object order is already valid")
|
||||
return True
|
||||
|
||||
if changed:
|
||||
rewrite_wheel(wheel, ordered)
|
||||
print(f"{wheel}: reordered shared objects")
|
||||
else:
|
||||
print(f"{wheel}: shared object order is already valid")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("wheel", type=Path, help="Wheel to rewrite in place")
|
||||
parser.add_argument("--check", action="store_true", help="Only validate the current shared object order")
|
||||
args = parser.parse_args()
|
||||
|
||||
return 0 if order_wheel(args.wheel, args.check) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1 +0,0 @@
|
||||
pyodide-build==0.39.0
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/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,311 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// 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
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import csv
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
from email.parser import Parser
|
||||
from pathlib import Path
|
||||
|
||||
MAIN_SHARED_OBJECT_RE = re.compile(r"(^|/)_ifcopenshell_wrapper(?:\.|$)")
|
||||
PURE_PYTHON_PACKAGE_NAME = "ifcopenshell-pure-python"
|
||||
PURE_PYTHON_PREFIXES = (
|
||||
"ifcopenshell/api/",
|
||||
"ifcopenshell/express/",
|
||||
"ifcopenshell/mvd/",
|
||||
"ifcopenshell/simple_spf/",
|
||||
)
|
||||
|
||||
|
||||
def wheel_parts(path: Path) -> tuple[str, str, str, str, str]:
|
||||
if path.suffix != ".whl":
|
||||
raise ValueError(f"not a wheel: {path}")
|
||||
stem = path.name[:-4]
|
||||
left, py_tag, abi_tag, platform_tag = stem.rsplit("-", 3)
|
||||
dist, version = left.rsplit("-", 1)
|
||||
return dist, version, py_tag, abi_tag, platform_tag
|
||||
|
||||
|
||||
def safe_name(name: str) -> str:
|
||||
return re.sub(r"[-_.]+", "-", name).lower().strip("-")
|
||||
|
||||
|
||||
def wheel_escape(value: str) -> str:
|
||||
return re.sub(r"[^\w\d.]+", "_", value, flags=re.UNICODE)
|
||||
|
||||
|
||||
def wheel_version_escape(value: str) -> str:
|
||||
return re.sub(r"[^\w\d.+]+", "_", value, flags=re.UNICODE)
|
||||
|
||||
|
||||
def dist_info_dir(name: str, version: str) -> str:
|
||||
return f"{wheel_escape(name)}-{wheel_version_escape(version)}.dist-info"
|
||||
|
||||
|
||||
def sha256_record_value(data: bytes) -> str:
|
||||
digest = hashlib.sha256(data).digest()
|
||||
return "sha256=" + base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
def make_info(name: str, *, source: zipfile.ZipInfo | None = None, mode: int | None = None) -> zipfile.ZipInfo:
|
||||
info = zipfile.ZipInfo(name)
|
||||
if source is not None:
|
||||
info.date_time = source.date_time
|
||||
info.external_attr = source.external_attr
|
||||
info.comment = source.comment
|
||||
info.create_system = source.create_system
|
||||
else:
|
||||
info.date_time = time.localtime(time.time())[:6]
|
||||
info.external_attr = ((mode if mode is not None else 0o644) & 0xFFFF) << 16
|
||||
info.create_system = 3
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
return info
|
||||
|
||||
|
||||
def write_record(zf: zipfile.ZipFile, entries: dict[str, bytes | None], record_name: str) -> None:
|
||||
rows: list[list[str]] = []
|
||||
for name in sorted(entries):
|
||||
data = entries[name]
|
||||
if name == record_name:
|
||||
rows.append([name, "", ""])
|
||||
elif data is None:
|
||||
raise ValueError(f"missing bytes for RECORD entry {name}")
|
||||
else:
|
||||
rows.append([name, sha256_record_value(data), str(len(data))])
|
||||
|
||||
buf = io.StringIO(newline="")
|
||||
writer = csv.writer(buf, lineterminator="\n")
|
||||
writer.writerows(rows)
|
||||
zf.writestr(make_info(record_name), buf.getvalue().encode("utf-8"))
|
||||
|
||||
|
||||
def read_original_metadata(zf: zipfile.ZipFile) -> tuple[str, str, str]:
|
||||
metadata_names = [n for n in zf.namelist() if n.endswith(".dist-info/METADATA")]
|
||||
wheel_names = [n for n in zf.namelist() if n.endswith(".dist-info/WHEEL")]
|
||||
record_names = [n for n in zf.namelist() if n.endswith(".dist-info/RECORD")]
|
||||
if len(metadata_names) != 1 or len(wheel_names) != 1 or len(record_names) != 1:
|
||||
raise ValueError("expected exactly one METADATA, WHEEL, and RECORD in the source wheel")
|
||||
return metadata_names[0], wheel_names[0], record_names[0]
|
||||
|
||||
|
||||
def shared_package_name(so_path: str) -> str:
|
||||
stem = Path(so_path).name.removesuffix(".so")
|
||||
stem = re.sub(r"[^A-Za-z0-9]+", "-", stem).strip("-")
|
||||
return safe_name(stem)
|
||||
|
||||
|
||||
def is_pure_python_split_path(path: str) -> bool:
|
||||
return any(path.startswith(prefix) for prefix in PURE_PYTHON_PREFIXES)
|
||||
|
||||
|
||||
def build_wheel(
|
||||
output_dir: Path,
|
||||
package_name: str,
|
||||
version: str,
|
||||
tag: str,
|
||||
root_is_purelib: bool,
|
||||
summary: str,
|
||||
payloads: list[tuple[zipfile.ZipInfo, bytes]],
|
||||
license_files: dict[str, bytes],
|
||||
) -> Path:
|
||||
di = dist_info_dir(package_name, version)
|
||||
wheel_name = f"{wheel_escape(package_name)}-{wheel_version_escape(version)}-{tag}.whl"
|
||||
out = output_dir / wheel_name
|
||||
record_name = f"{di}/RECORD"
|
||||
entries: dict[str, bytes | None] = {}
|
||||
|
||||
metadata = (
|
||||
"Metadata-Version: 2.4\n"
|
||||
f"Name: {package_name}\n"
|
||||
f"Version: {version}\n"
|
||||
f"Summary: {summary}\n"
|
||||
"License-File: COPYING\n"
|
||||
"License-File: COPYING.LESSER\n"
|
||||
"\n"
|
||||
).encode()
|
||||
wheel = (
|
||||
"Wheel-Version: 1.0\n"
|
||||
"Generator: split_pyodide_ifcopenshell_wheel.py\n"
|
||||
f"Root-Is-Purelib: {str(root_is_purelib).lower()}\n"
|
||||
f"Tag: {tag}\n"
|
||||
"\n"
|
||||
).encode()
|
||||
|
||||
with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for info, data in payloads:
|
||||
zf.writestr(make_info(info.filename, source=info), data)
|
||||
entries[info.filename] = data
|
||||
|
||||
metadata_name = f"{di}/METADATA"
|
||||
wheel_meta_name = f"{di}/WHEEL"
|
||||
zf.writestr(make_info(metadata_name), metadata)
|
||||
zf.writestr(make_info(wheel_meta_name), wheel)
|
||||
entries[metadata_name] = metadata
|
||||
entries[wheel_meta_name] = wheel
|
||||
|
||||
for basename, data in license_files.items():
|
||||
name = f"{di}/licenses/{basename}"
|
||||
zf.writestr(make_info(name), data)
|
||||
entries[name] = data
|
||||
|
||||
entries[record_name] = None
|
||||
write_record(zf, entries, record_name)
|
||||
print(f"Splitting wheel to '{out}'.")
|
||||
return out
|
||||
|
||||
|
||||
def rewrite_main_wheel(source: Path, target: Path, split_paths: set[str]) -> None:
|
||||
with zipfile.ZipFile(source) as zin, zipfile.ZipFile(
|
||||
target, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
|
||||
) as zout:
|
||||
_, _, record_name = read_original_metadata(zin)
|
||||
entries: dict[str, bytes | None] = {}
|
||||
for info in zin.infolist():
|
||||
if info.filename in split_paths or info.filename == record_name:
|
||||
continue
|
||||
data = zin.read(info.filename)
|
||||
zout.writestr(make_info(info.filename, source=info), data)
|
||||
entries[info.filename] = data
|
||||
entries[record_name] = None
|
||||
write_record(zout, entries, record_name)
|
||||
|
||||
|
||||
def verify_wheel(path: Path) -> None:
|
||||
with zipfile.ZipFile(path) as zf:
|
||||
zf.testzip()
|
||||
metadata_name, wheel_name, record_name = read_original_metadata(zf)
|
||||
Parser().parsestr(zf.read(metadata_name).decode("utf-8"))
|
||||
wheel_text = zf.read(wheel_name).decode("utf-8")
|
||||
if "Wheel-Version:" not in wheel_text or "Tag:" not in wheel_text:
|
||||
raise ValueError(f"invalid WHEEL metadata in {path}")
|
||||
|
||||
record_rows = list(csv.reader(io.StringIO(zf.read(record_name).decode("utf-8"))))
|
||||
names = {row[0] for row in record_rows}
|
||||
missing = set(zf.namelist()) - names
|
||||
if missing:
|
||||
raise ValueError(f"{path} RECORD is missing entries: {sorted(missing)[:5]}")
|
||||
for name, digest, size in record_rows:
|
||||
if name == record_name:
|
||||
continue
|
||||
data = zf.read(name)
|
||||
if digest != sha256_record_value(data) or size != str(len(data)):
|
||||
raise ValueError(f"{path} RECORD mismatch for {name}")
|
||||
|
||||
|
||||
def split_wheel(wheel_path: Path, output_dir: Path) -> None:
|
||||
wheel_path = wheel_path.expanduser().resolve()
|
||||
if not wheel_path.exists():
|
||||
raise FileNotFoundError(wheel_path)
|
||||
|
||||
output_dir = output_dir.expanduser().resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
main_wheel_path = output_dir / wheel_path.name
|
||||
if main_wheel_path.resolve(strict=False) == wheel_path:
|
||||
raise ValueError("output directory must not point to the input wheel location")
|
||||
|
||||
_, version, py_tag, abi_tag, platform_tag = wheel_parts(wheel_path)
|
||||
binary_tag = f"{py_tag}-{abi_tag}-{platform_tag}"
|
||||
pure_tag = "py3-none-any"
|
||||
|
||||
with zipfile.ZipFile(wheel_path) as zf:
|
||||
file_infos = [info for info in zf.infolist() if not info.is_dir()]
|
||||
so_infos = [info for info in file_infos if info.filename.endswith(".so")]
|
||||
split_so_infos = [info for info in so_infos if not MAIN_SHARED_OBJECT_RE.search(Path(info.filename).name)]
|
||||
pure_python_infos = [info for info in file_infos if is_pure_python_split_path(info.filename)]
|
||||
if not split_so_infos and not pure_python_infos:
|
||||
raise RuntimeError("no secondary .so files or pure Python subpackages found to split")
|
||||
|
||||
license_files = {
|
||||
Path(info.filename).name: zf.read(info.filename)
|
||||
for info in file_infos
|
||||
if ".dist-info/licenses/" in info.filename
|
||||
}
|
||||
split_so_payloads = [(info, zf.read(info.filename)) for info in split_so_infos]
|
||||
pure_python_payloads = [(info, zf.read(info.filename)) for info in pure_python_infos]
|
||||
|
||||
created_wheels: list[Path] = []
|
||||
for info, data in split_so_payloads:
|
||||
package_name = shared_package_name(info.filename)
|
||||
created_wheels.append(
|
||||
build_wheel(
|
||||
output_dir,
|
||||
package_name,
|
||||
version,
|
||||
binary_tag,
|
||||
False,
|
||||
f"Pyodide shared library split from IfcOpenShell ({Path(info.filename).name}).",
|
||||
[(info, data)],
|
||||
license_files,
|
||||
)
|
||||
)
|
||||
|
||||
if pure_python_payloads:
|
||||
created_wheels.append(
|
||||
build_wheel(
|
||||
output_dir,
|
||||
PURE_PYTHON_PACKAGE_NAME,
|
||||
version,
|
||||
pure_tag,
|
||||
True,
|
||||
"Pure Python subpackages split from IfcOpenShell.",
|
||||
pure_python_payloads,
|
||||
license_files,
|
||||
)
|
||||
)
|
||||
|
||||
temp_main_wheel = output_dir / f".{wheel_path.name}.tmp"
|
||||
try:
|
||||
rewrite_main_wheel(
|
||||
wheel_path,
|
||||
temp_main_wheel,
|
||||
{info.filename for info, _ in split_so_payloads + pure_python_payloads},
|
||||
)
|
||||
verify_wheel(temp_main_wheel)
|
||||
for created in created_wheels:
|
||||
verify_wheel(created)
|
||||
os.replace(temp_main_wheel, main_wheel_path)
|
||||
finally:
|
||||
if temp_main_wheel.exists():
|
||||
temp_main_wheel.unlink()
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract optional IfcOpenShell Pyodide payloads into separate wheel artifacts."
|
||||
)
|
||||
parser.add_argument("wheel", help="IfcOpenShell Pyodide wheel to split")
|
||||
parser.add_argument("output_dir", help="Directory for generated wheels")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(sys.argv[1:] if argv is None else argv)
|
||||
split_wheel(Path(args.wheel), Path(args.output_dir))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,2 +0,0 @@
|
||||
pyodide
|
||||
pyodide-modular
|
||||
+11
-24
@@ -1,33 +1,19 @@
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from ..order_pyodide_wheel_shared_objects import shared_object_sort_key
|
||||
WHEEL_FILENAME = next(
|
||||
p.name for p in (Path.cwd() / "pyodide").iterdir() if p.name.startswith("ifcopenshell-") and p.suffix == ".whl"
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def test_ifcopenshell_import(selenium):
|
||||
selenium.load_package("micropip")
|
||||
selenium.run_async(f"""
|
||||
# Important to test it with `micropip.install`
|
||||
# without any dependencies loaded to ensure micropip will load them automatically.
|
||||
selenium.run_async(
|
||||
f"""
|
||||
import micropip
|
||||
wheel_filenames = {WHEEL_NAMES!r}
|
||||
for wheel_filename in wheel_filenames:
|
||||
print(f"Loading {{wheel_filename}}...")
|
||||
await micropip.install(f"./{{wheel_filename}}")
|
||||
await micropip.install(f"./{WHEEL_FILENAME}")
|
||||
import ifcopenshell
|
||||
from pathlib import Path
|
||||
ifcopenshell.set_plugin_search_paths([str(Path(ifcopenshell.__file__).parent)])
|
||||
ifc_file = ifcopenshell.file()
|
||||
wall = ifc_file.create_entity("IfcWall")
|
||||
wall1 = ifc_file.by_type("IfcWall")[0]
|
||||
@@ -36,4 +22,5 @@ def test_ifcopenshell_import(selenium, request):
|
||||
wall.Name = "Test"
|
||||
assert wall.Name == "Test", f"Entity name wasn't changed: {{wall}}"
|
||||
print(wall)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
+14
-33
@@ -6,6 +6,10 @@ 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
|
||||
@@ -14,15 +18,6 @@ extend-exclude = '''
|
||||
|src/ifc2ca/templates/*
|
||||
|src/svgfill
|
||||
|src/exterior-shell-extractor
|
||||
|choco/bonsai/tools/enable_blenderbim_addon.py
|
||||
|choco/bonsai/tools/disable_blenderbim_addon.py
|
||||
|docs/conf.py
|
||||
|docs/generate_docs.py
|
||||
|aws/lambda/example_handler/__init__.py
|
||||
|conda/update_version_init.py
|
||||
|test/bpy.py
|
||||
|test/tests.py
|
||||
|test/run.py
|
||||
'''
|
||||
|
||||
[tool.pyright]
|
||||
@@ -67,30 +62,20 @@ select = [
|
||||
#
|
||||
"FA", # future annotations
|
||||
"UP", # pyupgrade
|
||||
"unnecessary-iterable-allocation-for-first-element",
|
||||
"unsorted-dunder-all",
|
||||
"RUF015", # next() > list_comprehension[0]
|
||||
"RUF022", # sort __all__
|
||||
"I", # import sorting
|
||||
"unused-noqa",
|
||||
"rule-codes-in-selectors",
|
||||
"noqa-comments",
|
||||
"rule-codes-in-suppression-comments",
|
||||
# General util rules.
|
||||
"invalid-rule-code",
|
||||
"redirected-noqa",
|
||||
"invalid-pyproject-toml",
|
||||
"invalid-suppression-comment",
|
||||
]
|
||||
ignore = [
|
||||
# Conflicts with Blender using annotations for props definitions.
|
||||
"future-rewritable-type-annotation",
|
||||
"FA100", # Conflicts with Blender using annotations for props definitions.
|
||||
# Maybe will enable later:
|
||||
"non-pep604-annotation-union", # Union[X,Y] to X | Y
|
||||
"non-pep604-annotation-optional", # Optional to X | None
|
||||
"redundant-open-modes", # Unnecessary mode argument
|
||||
"yield-in-for-loop", # yield for -> yield from
|
||||
"format-literals", # implicit references for positional format fields
|
||||
"printf-string-formatting", # Replace % with .format
|
||||
"f-string", # Replace .format with f-string
|
||||
"UP007", # 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
|
||||
]
|
||||
|
||||
[tool.ty.rules]
|
||||
@@ -116,9 +101,7 @@ invalid-assignment = "ignore"
|
||||
invalid-parameter-default = "ignore"
|
||||
missing-override-decorator = "ignore"
|
||||
invalid-yield = "ignore"
|
||||
unsound-yield = "ignore"
|
||||
invalid-return-type = "ignore"
|
||||
unsound-return-statement = "ignore"
|
||||
non-callable-init-subclass = "ignore"
|
||||
not-iterable = "ignore"
|
||||
possibly-missing-attribute = "ignore"
|
||||
@@ -187,8 +170,6 @@ dev-setup.help = "Install repo packages in editable mode"
|
||||
|
||||
ruff = "ruff check"
|
||||
|
||||
check-whitespace = "uv run .github/scripts/check-whitespace.py"
|
||||
|
||||
black = "black ."
|
||||
|
||||
ty.sequence = ["ty-bonsai", "ty-ios"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
black==26.3.1
|
||||
ruff==0.16.0
|
||||
poethepoet
|
||||
ty==0.0.72
|
||||
ty==0.0.63
|
||||
gersemi==0.28.0
|
||||
|
||||
@@ -35,7 +35,7 @@ def camera_vectors_from_target_position(
|
||||
"""
|
||||
camera_offset = np.array((5, 5, 5)) if offset is None else offset
|
||||
camera_position = target_position + camera_offset
|
||||
camera_direction = unit_vector(-camera_offset)
|
||||
camera_direction = unit_vector(-camera_offset) # pylint: disable=invalid-unary-operand-type
|
||||
camera_right = unit_vector(np.cross(np.array([0.0, 0.0, 1.0]), camera_direction))
|
||||
camera_up = unit_vector(np.cross(camera_direction, camera_right))
|
||||
return camera_position.tolist(), camera_direction.tolist(), camera_up.tolist()
|
||||
|
||||
@@ -316,7 +316,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
|
||||
Returns:
|
||||
The BCF viewpoint definition.
|
||||
"""
|
||||
ifc_file = element.file
|
||||
ifc_file = element.wrapped_data.file
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
elem_placement[:3, 3] *= unit_scale
|
||||
|
||||
@@ -96,10 +96,10 @@ class FoundationClient:
|
||||
webbrowser.open(f"{auth_endpoint}?{query}")
|
||||
server.timeout = 100
|
||||
server.handle_request()
|
||||
if server.auth_code and server.auth_state == state:
|
||||
if server.auth_code and server.auth_state == state: # pylint: disable=E1101
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": server.auth_code,
|
||||
"code": server.auth_code, # pylint: disable=E1101 type:ignore
|
||||
"redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}",
|
||||
}
|
||||
headers = self._get_access_token_headers()
|
||||
|
||||
@@ -316,7 +316,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
|
||||
Returns:
|
||||
The BCF viewpoint definition.
|
||||
"""
|
||||
ifc_file = element.file
|
||||
ifc_file = element.wrapped_data.file
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
elem_placement[:3, 3] *= unit_scale
|
||||
|
||||
+82
-4
@@ -10,7 +10,7 @@ name = "bcf-client"
|
||||
# author = "IfcOpenShell"
|
||||
description = "BCF-XML file handler."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.8"
|
||||
keywords = ["IFC", "BCF", "BIM"]
|
||||
dependencies = [
|
||||
"xsdata>=24.4",
|
||||
@@ -31,6 +31,14 @@ classifiers = [
|
||||
Source = "https://github.com/IfcOpenShell/IfcOpenShell"
|
||||
Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
extend-exclude = "model"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
extend_skip_glob = ["src/bcf/*/model/*"]
|
||||
|
||||
[tool.coverage.paths]
|
||||
source = ["src"]
|
||||
|
||||
@@ -52,7 +60,7 @@ exclude_lines = [
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
env_list = py3{10,11}
|
||||
env_list = lint, type, py3{10,11}
|
||||
skip_missing_interpreters = true
|
||||
|
||||
[testenv]
|
||||
@@ -61,10 +69,80 @@ deps =
|
||||
pytest-cov
|
||||
coverage
|
||||
commands = pytest --cov --cov-report=term tests
|
||||
|
||||
[testenv:lint]
|
||||
description = run linters
|
||||
skip_install = true
|
||||
deps =
|
||||
black
|
||||
isort
|
||||
pylint
|
||||
commands =
|
||||
black {posargs:.}
|
||||
isort {posargs:.}
|
||||
pylint {posargs:.} --output-format=colorized
|
||||
|
||||
[testenv:type]
|
||||
description = run type checks
|
||||
deps =
|
||||
mypy>=0.991
|
||||
commands =
|
||||
- mypy {posargs:src}
|
||||
"""
|
||||
|
||||
[tool.mypy]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
#no_implicit_reexport = true
|
||||
show_column_numbers = true
|
||||
show_error_codes = true
|
||||
show_error_context = true
|
||||
strict_equality = true
|
||||
strict_optional = true
|
||||
warn_redundant_casts = true
|
||||
#warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
warn_unused_ignores = true
|
||||
exclude= "src/bcf/v(2|3)/model"
|
||||
plugins = "numpy.typing.mypy_plugin"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "tests"
|
||||
disallow_untyped_decorators = false
|
||||
disallow_untyped_defs = false
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"pytest",
|
||||
"pytest_mock",
|
||||
"ifcopenshell",
|
||||
"ifcopenshell.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pylint.main]
|
||||
ignore = ["model"]
|
||||
ignored-modules = ["bcf.v2.model", "bcf.v3.model", "xsdata"]
|
||||
jobs = 0
|
||||
disable="all"
|
||||
enable="E" # B,B9,BLK,C,D,E,F,I,N,S,W
|
||||
|
||||
[tool.pylint.design]
|
||||
max-args = 10
|
||||
max-attributes = 10
|
||||
|
||||
[tool.pylint.format]
|
||||
expected-line-ending-format = "LF"
|
||||
max-line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
lint.extend-select = [
|
||||
"unused-import", # unused imports
|
||||
lint.select = [
|
||||
"F401", # unused imports
|
||||
]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
black
|
||||
mypy
|
||||
pylint
|
||||
isort
|
||||
xsdata
|
||||
tox==3.27.1
|
||||
+16
-13
@@ -42,12 +42,10 @@ endif
|
||||
|
||||
IS_STABLE:=FALSE
|
||||
VERSION:=$(shell cat ../../VERSION)
|
||||
VERSION_BASE:=$(shell sed -E 's/[[:alpha:]]+[0-9]+$$//' ../../VERSION)
|
||||
VERSION_PYTHON:=$(shell sed 's/alpha/a/' ../../VERSION)
|
||||
VERSION_SEMVER:=$(shell sed -E 's/([[:alpha:]]+)([0-9]+)$$/-\\1\\2/' ../../VERSION)
|
||||
VERSION_MAJOR:=$(shell cat '../../VERSION' | cut -d '.' -f 1)
|
||||
VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2)
|
||||
VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
|
||||
VERSION_DATE:=$(shell date '+%y%m%d')
|
||||
VERSION_DAILY:=$(VERSION_BASE)a$(VERSION_DATE)
|
||||
VERSION_SEMVER_DAILY:=$(VERSION_BASE)-alpha$(VERSION_DATE)
|
||||
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
|
||||
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
|
||||
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
|
||||
@@ -262,14 +260,14 @@ endif
|
||||
|
||||
cp pyproject.toml build/
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
$(SED) "s/0.0.0/$(VERSION_SEMVER)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
|
||||
$(SED) "s/0.0.0/$(VERSION)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
|
||||
else
|
||||
$(SED) "s/0.0.0/$(VERSION_SEMVER_DAILY)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
|
||||
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
|
||||
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
|
||||
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
|
||||
endif
|
||||
|
||||
# Blender 5.1+ requires Python 3.13.
|
||||
@@ -281,9 +279,9 @@ endif
|
||||
|
||||
# Provides bonsai Add-on functionality
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_PYTHON)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)"/' build/pyproject.toml
|
||||
else
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION_DAILY)"/' build/pyproject.toml
|
||||
$(SED) 's/version = "0.0.0"/version = "$(VERSION)a$(VERSION_DATE)"/' build/pyproject.toml
|
||||
endif
|
||||
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m build
|
||||
cp build/dist/*.whl build/wheels/
|
||||
@@ -317,9 +315,9 @@ endif
|
||||
rm -rf build/bonsai/libs/
|
||||
|
||||
ifeq ($(IS_STABLE), TRUE)
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
else
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION_SEMVER_DAILY)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
cd build && zip -r bonsai_$(PYVERSION)-$(VERSION)-alpha$(VERSION_DATE)-$(BLENDER_PLATFORM).zip ./bonsai
|
||||
endif
|
||||
|
||||
mv build/bonsai*.zip dist/
|
||||
@@ -372,6 +370,11 @@ test-modal:
|
||||
test-reregister:
|
||||
blender --python scripts/reregister_bonsai.py
|
||||
|
||||
.PHONY: qa
|
||||
qa:
|
||||
black .
|
||||
pylint ./* --output-format=colorized --disable all --enable E --disable import-error
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
coverage run --source bonsai.core -m pytest -p no:pytest-blender test/core
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
This cache folder contains .h5 files. These files cache IFC geometry for performance only. You may safely clear the contents of this cache folder without losing data.
|
||||
@@ -5,7 +5,7 @@ FILE_NAME('Psets_BBIM_Annotation.ifc','2020-01-01T00:00:00',$,$,'Psets_BBIM_Anno
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2));
|
||||
#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation,IfcTypeProduct',(#4,#33,#29,#32,#3,#2,#41,#42));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3q0oxMUKP47vZ4jnyG$dDb',$,'Classes','Classes separated by spaces that end up in classes for this element in svg. Can be used to specify the text font size: small - 1.8mm; regular - 2.5mm; large - 3.5mm; header - 5mm; title - 7mm. By default regular size is used.',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -28,7 +28,7 @@ DATA;
|
||||
#21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30));
|
||||
#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcAnnotation/ANGLE,IfcAnnotation/PLAN_LEVEL,IfcAnnotation/SECTION_LEVEL,IfcTypeProduct',(#25,#26,#35,#36,#27,#28,#30,#34,#37,#38,#39,#40));
|
||||
#25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -38,5 +38,14 @@ DATA;
|
||||
#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$);
|
||||
#32=IFCSIMPLEPROPERTYTEMPLATE('0gjJzDYBX8P85qn1xcAOOo',$,'Reverse_List','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#33=IFCSIMPLEPROPERTYTEMPLATE('22TrcxF8jFNB4buSmzjGEF',$,'List_Separator','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#34=IFCSIMPLEPROPERTYTEMPLATE('1Kx4Pm9nR8vBwZqTs2uYeL',$,'Separator','Characters placed between multiple dimension values when CustomUnit has more than one unit selected (default: '' / '')',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#35=IFCSIMPLEPROPERTYTEMPLATE('3Nf6Qs1mT0pWxBuCvDyEzA',$,'SuppressZeroFeet','Suppress 0 feet in dimension annotation text (for example: 0'' - 3 1/2" -> 3 1/2")',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#36=IFCSIMPLEPROPERTYTEMPLATE('2Rg7Hn5jK4mLpNqOsVwXtY',$,'IsOrdinate','Show accumulated distance from the first vertex instead of individual segment lengths',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#37=IFCSIMPLEPROPERTYTEMPLATE('1XpRnKoT2sGuW7vYcZaMqb',$,'Anchors','JSON array of parametric anchor descriptors — one per polyline vertex. Each entry: {"guid": str|null, "type": "FACE"|"CIRCLE_CENTER"|"WORLD", "addr": {...}, "hint": [x,y,z]|null, "pt": [x,y,z]}',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#38=IFCSIMPLEPROPERTYTEMPLATE('2YqSmLoU3tHvX8wZdaNrjc',$,'MeasureAxis','Axis along which distances are projected: X | Y | Z | TRUE | PERPENDICULAR',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#39=IFCSIMPLEPROPERTYTEMPLATE('3Ny31Go6T5Z9fh8j4yQC0p',$,'ForcePerpendicularToFace','When enabled the polyline is constrained to follow the face normal of the first anchor vertex so the dimension measures straight-line distance perpendicular to that face',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#40=IFCSIMPLEPROPERTYTEMPLATE('1LoNpKqR3sTuVwXyZaBcDe',$,'LinePosition','Absolute world-space coordinate (metres) of the dimension line along the horizontal offset axis (perpendicular to the dimension direction). When set, the dimension line is held at this fixed global position even if the measured geometry moves. When absent the line sits at the anchor points.',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.);
|
||||
#41=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsAnnotFaux0001aB',$,'IsManualDrawingReference','Marks this annotation as a manually placed drawing reference, exempt from automatic drawing regeneration.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#42=IFCSIMPLEPROPERTYTEMPLATE('0FauxIsDocRefFaux001aB',$,'IsDocumentReference','Marks this annotation as pointing to an external document reference (not a Bonsai drawing camera).',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -43,6 +43,7 @@ class IfcExporter:
|
||||
def export(self):
|
||||
self.file = tool.Ifc.get()
|
||||
self.set_header()
|
||||
IfcStore.update_cache()
|
||||
self.sync_all_objects()
|
||||
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
|
||||
if extension == "ifczip":
|
||||
|
||||
@@ -187,7 +187,7 @@ def import_attributes(
|
||||
info = {a.name(): None for a in attributes}
|
||||
info["type"] = element
|
||||
else:
|
||||
assert (entity := element.declaration.as_entity())
|
||||
assert (entity := element.wrapped_data.declaration().as_entity())
|
||||
attributes = entity.all_attributes()
|
||||
info = element.get_info()
|
||||
for attribute in attributes:
|
||||
|
||||
@@ -110,6 +110,8 @@ class IfcStore:
|
||||
"""Should be set only using ``tool.Ifc.set``."""
|
||||
|
||||
schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None
|
||||
cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None
|
||||
cache_path: Optional[str] = None
|
||||
id_map: dict[int, IFC_CONNECTED_TYPE] = {}
|
||||
guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
|
||||
edited_objs: set[bpy.types.Object] = set()
|
||||
@@ -131,6 +133,8 @@ class IfcStore:
|
||||
IfcStore.path = ""
|
||||
IfcStore.file = None
|
||||
IfcStore.schema = None
|
||||
IfcStore.cache = None
|
||||
IfcStore.cache_path = None
|
||||
IfcStore.id_map = {}
|
||||
IfcStore.guid_map = {}
|
||||
IfcStore.edited_objs = set()
|
||||
@@ -185,10 +189,13 @@ class IfcStore:
|
||||
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
|
||||
IfcStore.cache_path = cache_path
|
||||
cache_path = Path(IfcStore.cache_path)
|
||||
settings = ifcopenshell.geom.settings()
|
||||
cache_settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
cache_preexists = cache_path.exists()
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
|
||||
IfcStore.cache_path, cache_settings, serializer_settings
|
||||
)
|
||||
if cache_preexists:
|
||||
print(f"Successfully loaded existing cache: {cache_path.name}.")
|
||||
else:
|
||||
@@ -203,7 +210,9 @@ class IfcStore:
|
||||
|
||||
os.remove(IfcStore.cache_path)
|
||||
try:
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, settings)
|
||||
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
|
||||
IfcStore.cache_path, cache_settings, serializer_settings
|
||||
)
|
||||
print("New cache was created.")
|
||||
except Exception as e:
|
||||
print(f"Failed to create a cache: {str(e)}.")
|
||||
@@ -241,7 +250,7 @@ class IfcStore:
|
||||
IfcStore.file = ifcopenshell.open(filename)
|
||||
return
|
||||
elif extension.lower() == "ifcxml":
|
||||
raise NotImplementedError("Reading .ifcXML files is not currently supported.")
|
||||
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
|
||||
elif prefs.should_stream:
|
||||
IfcStore.file = ifcopenshell.open(path, should_stream=True)
|
||||
else:
|
||||
|
||||
@@ -718,6 +718,10 @@ class IfcImporter:
|
||||
iterator = ifcopenshell.geom.iterator(
|
||||
settings, self.file, include=products, geometry_library=self.ifc_import_settings.geometry_library
|
||||
)
|
||||
if self.ifc_import_settings.should_cache:
|
||||
cache = IfcStore.get_cache()
|
||||
if cache:
|
||||
iterator.set_cache(cache)
|
||||
valid_file = iterator.initialize()
|
||||
if not valid_file:
|
||||
return results
|
||||
@@ -740,7 +744,7 @@ class IfcImporter:
|
||||
self.update_progress((percent_average / 100 * progress_range) + start_progress)
|
||||
shape = iterator.get()
|
||||
if shape:
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
product = self.file.by_id(shape.id)
|
||||
self.create_product(product, shape)
|
||||
results.add(product)
|
||||
@@ -1079,9 +1083,9 @@ class IfcImporter:
|
||||
def create_curve(
|
||||
self,
|
||||
element: ifcopenshell.entity_instance,
|
||||
shape: Union[W.triangulation, W.triangulation_element],
|
||||
shape: Union[W.Triangulation, W.TriangulationElement],
|
||||
) -> bpy.types.Curve:
|
||||
if isinstance(shape, W.triangulation_element):
|
||||
if isinstance(shape, W.TriangulationElement):
|
||||
geometry = shape.geometry
|
||||
else:
|
||||
geometry = shape
|
||||
@@ -1112,11 +1116,11 @@ class IfcImporter:
|
||||
def create_mesh(
|
||||
self,
|
||||
element: ifcopenshell.entity_instance,
|
||||
shape: Union[W.triangulation, W.triangulation_element],
|
||||
shape: Union[W.Triangulation, W.TriangulationElement],
|
||||
cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None,
|
||||
) -> Union[bpy.types.Mesh, None]:
|
||||
try:
|
||||
if isinstance(shape, W.triangulation_element):
|
||||
if isinstance(shape, W.TriangulationElement):
|
||||
# shape is ShapeElementType
|
||||
geometry = shape.geometry
|
||||
else:
|
||||
@@ -1277,6 +1281,7 @@ class IfcImportSettings:
|
||||
self.should_merge_materials_by_colour = False
|
||||
self.should_load_geometry = True
|
||||
self.should_clean_mesh = False
|
||||
self.should_cache = True
|
||||
self.deflection_tolerance = 0.05 # Default is 0.001, but I find this to be more practical
|
||||
self.angular_tolerance = 0.5
|
||||
self.void_limit = 30
|
||||
@@ -1304,6 +1309,7 @@ class IfcImportSettings:
|
||||
context=None, input_file: Optional[str] = None, logger: Optional[logging.Logger] = None
|
||||
) -> IfcImportSettings:
|
||||
scene_diff = tool.Blender.get_diff_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
props = tool.Project.get_project_props()
|
||||
settings = IfcImportSettings()
|
||||
settings.input_file = input_file
|
||||
@@ -1316,6 +1322,7 @@ class IfcImportSettings:
|
||||
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
|
||||
settings.should_load_geometry = props.should_load_geometry
|
||||
settings.should_clean_mesh = props.should_clean_mesh
|
||||
settings.should_cache = prefs.should_always_cache or props.should_cache
|
||||
settings.deflection_tolerance = props.deflection_tolerance
|
||||
settings.angular_tolerance = props.angular_tolerance
|
||||
settings.void_limit = props.void_limit
|
||||
|
||||
@@ -678,7 +678,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
# Identify all potential building elements
|
||||
# TODO: don't select everything, use AABB culling in Blender
|
||||
building_elements = list(
|
||||
building_elements = (
|
||||
tool.Ifc.get().by_type("IfcWall")
|
||||
+ tool.Ifc.get().by_type("IfcSlab")
|
||||
+ tool.Ifc.get().by_type("IfcVirtualElement")
|
||||
@@ -708,7 +708,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
while True:
|
||||
tree.add_element(iterator.get_native())
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
shapes[shape.id] = {
|
||||
"verts": ifcopenshell.util.shape.get_vertices(shape.geometry),
|
||||
"faces": ifcopenshell.util.shape.get_faces(shape.geometry),
|
||||
@@ -1059,6 +1059,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return tool.Ifc.get().createIfcConnectionSurfaceGeometry(surface)
|
||||
|
||||
def export_surface(self, polygon, target_face_matrix):
|
||||
ifc_file = tool.Ifc.get()
|
||||
x_axis = target_face_matrix.col[0][:3]
|
||||
z_axis = target_face_matrix.col[2][:3]
|
||||
p1 = target_face_matrix.translation
|
||||
@@ -1071,19 +1072,20 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
placement = builder.create_axis2_placement_3d([o / self.unit_scale for o in p1], z_axis, x_axis)
|
||||
surface.BasisSurface = tool.Ifc.get().create_entity("IfcPlane", placement)
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
schema = ifc_file.schema
|
||||
if schema != "IFC2X3":
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in polygon.exterior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
outer_boundary = tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False)
|
||||
|
||||
inner_boundaries = []
|
||||
inner_boundaries: list[ifcopenshell.entity_instance] = []
|
||||
for interior in polygon.interiors:
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in interior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
inner_boundaries.append(tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False))
|
||||
else:
|
||||
# TODO:
|
||||
raise NotImplementedError(tool.Ifc.get().schema)
|
||||
raise NotImplementedError(schema)
|
||||
|
||||
surface.OuterBoundary = outer_boundary
|
||||
surface.InnerBoundaries = inner_boundaries
|
||||
|
||||
@@ -348,7 +348,10 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
if self.obj_type == "Object":
|
||||
objects = [o.name for o in tool.Blender.get_selected_objects()]
|
||||
if context.selected_objects:
|
||||
objects = [o.name for o in context.selected_objects]
|
||||
else:
|
||||
objects = [context.active_object.name]
|
||||
else:
|
||||
objects = [self.obj]
|
||||
props = tool.Classification.get_classification_props()
|
||||
|
||||
@@ -516,7 +516,7 @@ def _world_segment_to_screen_pixels(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BIM_GT_box_face_quad(bpy.types.Gizmo):
|
||||
class BIM_GT_box_face_quad(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
|
||||
"""Near-invisible face-quad click target with drag-to-resize modal.
|
||||
|
||||
Geometry: a unit quad in the local XY plane at z=0. The adapter
|
||||
@@ -620,7 +620,7 @@ class BIM_GT_box_face_quad(bpy.types.Gizmo):
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class BIM_GT_box_face_outline(bpy.types.Gizmo):
|
||||
class BIM_GT_box_face_outline(bpy.types.Gizmo): # noqa: N801 — Blender bl_idname convention
|
||||
"""Thin non-interactive colored edge outline for one face.
|
||||
|
||||
Drawn as 4 line segments in the face plane. The layout helper
|
||||
|
||||
@@ -160,7 +160,7 @@ def _make_face_set_cb(gz: Any, group: Any, axis: int, is_max: bool):
|
||||
return setter
|
||||
|
||||
|
||||
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup):
|
||||
class OBJECT_GGT_bim_clip_box(bpy.types.GizmoGroup): # noqa: N801 — Blender bl_idname convention
|
||||
"""Face-quad resize handles on the active clip box.
|
||||
|
||||
Renders six near-invisible click-target quads and six colored edge
|
||||
|
||||
@@ -208,7 +208,12 @@ class CostSchedulesData:
|
||||
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
measure_class = (
|
||||
quantity.declaration.as_entity().attribute_by_index(3).type_of_attribute().declared_type().name()
|
||||
quantity.wrapped_data.declaration()
|
||||
.as_entity()
|
||||
.attribute_by_index(3)
|
||||
.type_of_attribute()
|
||||
.declared_type()
|
||||
.name()
|
||||
)
|
||||
if "Count" in measure_class:
|
||||
data["UnitSymbol"] = "U"
|
||||
|
||||
@@ -987,7 +987,7 @@ class ExportCostSchedulesToPDF(bpy.types.Operator, ExportHelper):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
try:
|
||||
import typst # ruff: ignore[unused-import]
|
||||
import typst # noqa: F401
|
||||
|
||||
return True
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -37,6 +37,7 @@ classes = (
|
||||
operator.PrintObjectPlacement,
|
||||
operator.PrintUnusedElementStats,
|
||||
operator.ProfileImportIFC,
|
||||
operator.PurgeHdf5Cache,
|
||||
operator.PurgeUnusedElementsByClass,
|
||||
operator.PurgeUnusedObjects,
|
||||
operator.RestartBlender,
|
||||
|
||||
@@ -90,7 +90,7 @@ class PrintIfcFile(bpy.types.Operator):
|
||||
return tool.Ifc.get()
|
||||
|
||||
def execute(self, context):
|
||||
print(tool.Ifc.get().to_string())
|
||||
print(tool.Ifc.get().wrapped_data.to_string())
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -313,7 +313,7 @@ class CreateAllShapes(bpy.types.Operator):
|
||||
failures.append(element)
|
||||
print("***** FAILURE *****")
|
||||
if shape:
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
geom = shape.geometry
|
||||
print(
|
||||
f"Success {time.time() - start:.3f}s "
|
||||
@@ -570,6 +570,17 @@ class SelectExpressFile(bpy.types.Operator, ImportHelper):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class PurgeHdf5Cache(bpy.types.Operator):
|
||||
bl_idname = "bim.purge_hdf5_cache"
|
||||
bl_label = "Purge HDF5 Cache"
|
||||
bl_description = "Clean up HDF5 cache files except the ones that currently loaded"
|
||||
|
||||
def execute(self, context):
|
||||
core.purge_hdf5_cache(tool.Debug)
|
||||
self.report({"INFO"}, "HDF5 cache purged.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class OverrideDisplayType(bpy.types.Operator):
|
||||
bl_idname = "bim.override_display_type"
|
||||
bl_label = "Override Display Type"
|
||||
|
||||
@@ -64,6 +64,9 @@ class BIM_PT_debug(Panel):
|
||||
row = layout.row()
|
||||
row.operator("bim.copy_debug_information")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.purge_hdf5_cache")
|
||||
|
||||
row = layout.row()
|
||||
row.operator("bim.update_representation", text="Manually Save Representation")
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ classes = (
|
||||
operator.ActivateModel,
|
||||
operator.AddAnnotation,
|
||||
operator.AddAnnotationType,
|
||||
operator.AddElevationAnnotation,
|
||||
operator.AddDrawing,
|
||||
operator.AddDrawingStyle,
|
||||
operator.AddDrawingToSheet,
|
||||
@@ -111,6 +112,16 @@ classes = (
|
||||
operator.ToggleDrawingCategorySelection,
|
||||
operator.OpenDocumentationWebUi,
|
||||
operator.FilterSelectedObjectsIfIntersectedByCamera,
|
||||
operator.DrawParametricDimension,
|
||||
operator.SetDimensionAnchor,
|
||||
operator.RegenerateDimensions,
|
||||
operator.DriveDimensionLength,
|
||||
operator.RemoveDimensionAnchor,
|
||||
operator.InsertDimensionAnchor,
|
||||
operator.ClickNearestDimensionAnchor,
|
||||
operator.MakeDimensionParametric,
|
||||
operator.BakeParametricDimension,
|
||||
operator.DebugDimensionClicks,
|
||||
prop.Variable,
|
||||
prop.Drawing,
|
||||
prop.Document,
|
||||
@@ -172,11 +183,19 @@ classes = (
|
||||
gizmos.UglyDotGizmo,
|
||||
gizmos.ExtrusionGuidesGizmo,
|
||||
gizmos.ExtrusionWidget,
|
||||
gizmos.GizmoAnchorHandle,
|
||||
gizmos.GizmoDriveDimLabel,
|
||||
gizmos.DimensionAnchorWidget,
|
||||
gizmos.DimensionLinePositionWidget,
|
||||
gizmos.DimensionDriveLabelWidget,
|
||||
workspace.LaunchAnnotationTypeManager,
|
||||
workspace.Hotkey,
|
||||
)
|
||||
|
||||
|
||||
_keymaps = []
|
||||
|
||||
|
||||
def menu_func(self, context):
|
||||
active_obj = context.active_object
|
||||
if active_obj:
|
||||
@@ -196,9 +215,21 @@ def register():
|
||||
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
|
||||
bpy.app.handlers.load_post.append(handler.load_post)
|
||||
bpy.app.handlers.depsgraph_update_pre.append(handler.depsgraph_update_pre_handler)
|
||||
bpy.app.handlers.depsgraph_update_post.append(handler.depsgraph_update_post_handler)
|
||||
bpy.types.VIEW3D_MT_image_add.append(ui.add_object_button)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
kc = wm.keyconfigs.addon
|
||||
if kc:
|
||||
km = kc.keymaps.new(name="3D View", space_type="VIEW_3D")
|
||||
kmi = km.keymap_items.new("bim.click_nearest_dimension_anchor", "LEFTMOUSE", "PRESS")
|
||||
_keymaps.append((km, kmi))
|
||||
kmi_alt = km.keymap_items.new("bim.click_nearest_dimension_anchor", "LEFTMOUSE", "PRESS", alt=True)
|
||||
_keymaps.append((km, kmi_alt))
|
||||
kmi_ctrl = km.keymap_items.new("bim.click_nearest_dimension_anchor", "LEFTMOUSE", "PRESS", ctrl=True)
|
||||
_keymaps.append((km, kmi_ctrl))
|
||||
|
||||
|
||||
def unregister():
|
||||
if not bpy.app.background:
|
||||
@@ -211,5 +242,10 @@ def unregister():
|
||||
del bpy.types.TextCurve.BIMTextProperties
|
||||
bpy.app.handlers.load_post.remove(handler.load_post)
|
||||
bpy.app.handlers.depsgraph_update_pre.remove(handler.depsgraph_update_pre_handler)
|
||||
bpy.app.handlers.depsgraph_update_post.remove(handler.depsgraph_update_post_handler)
|
||||
|
||||
for km, kmi in _keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
_keymaps.clear()
|
||||
bpy.types.VIEW3D_MT_image_add.remove(ui.add_object_button)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func)
|
||||
|
||||
@@ -55,6 +55,14 @@ class ProductAssignmentsData:
|
||||
element = tool.Ifc.get_entity(bpy.context.active_object)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
return
|
||||
# Document-reference annotations link to an IfcDocumentInformation, not a product.
|
||||
if tool.Drawing.is_document_reference(element):
|
||||
for rel in element.HasAssociations:
|
||||
if rel.is_a("IfcRelAssociatesDocument"):
|
||||
doc = rel.RelatingDocument
|
||||
if doc.is_a("IfcDocumentInformation"):
|
||||
return doc.Name or "Unnamed"
|
||||
return None
|
||||
for rel in element.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToProduct"):
|
||||
name = rel.RelatingProduct.Name or "Unnamed"
|
||||
@@ -312,6 +320,9 @@ class DecoratorData:
|
||||
"StartArrowSymbol": "",
|
||||
"ShowEndArrow": True,
|
||||
"EndArrowSymbol": "",
|
||||
"BorderOffset": 8.0,
|
||||
"AutoStartPosition": "",
|
||||
"AutoEndPosition": "",
|
||||
}
|
||||
obj_pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Section") or {}
|
||||
pset_data.update(obj_pset_data)
|
||||
@@ -331,6 +342,9 @@ class DecoratorData:
|
||||
"symbol": end_symbol or "section-arrow",
|
||||
},
|
||||
"connect_markers": pset_data["HasConnectedSectionLine"],
|
||||
"border_offset": float(pset_data["BorderOffset"]),
|
||||
"auto_start_position": pset_data["AutoStartPosition"] or "",
|
||||
"auto_end_position": pset_data["AutoEndPosition"] or "",
|
||||
}
|
||||
|
||||
cls.data[obj.name] = display_data
|
||||
@@ -652,7 +666,7 @@ class DecoratorData:
|
||||
try:
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
if matrix is not None:
|
||||
ifc_file = element.file
|
||||
ifc_file = element.wrapped_data.file
|
||||
project = ifc_file.by_type("IfcProject")[0] if ifc_file.by_type("IfcProject") else None
|
||||
|
||||
if project:
|
||||
@@ -799,19 +813,24 @@ class DecoratorData:
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension") or {}
|
||||
show_description_only = pset_data.get("ShowDescriptionOnly", False)
|
||||
suppress_zero_inches = pset_data.get("SuppressZeroInches", False)
|
||||
suppress_zero_feet = pset_data.get("SuppressZeroFeet", False)
|
||||
is_ordinate = pset_data.get("IsOrdinate", False)
|
||||
text_prefix = pset_data.get("TextPrefix", None) or ""
|
||||
text_suffix = pset_data.get("TextSuffix", None) or ""
|
||||
custom_unit_list = pset_data.get("CustomUnit", None) or ""
|
||||
custom_unit = custom_unit_list[0] if custom_unit_list else ""
|
||||
custom_units = list(pset_data.get("CustomUnit", None) or [])
|
||||
separator = pset_data.get("Separator", None) or " / "
|
||||
|
||||
return {
|
||||
"dimension_style": dimension_style,
|
||||
"show_description_only": show_description_only,
|
||||
"suppress_zero_inches": suppress_zero_inches,
|
||||
"suppress_zero_feet": suppress_zero_feet,
|
||||
"is_ordinate": is_ordinate,
|
||||
"text_prefix": text_prefix,
|
||||
"text_suffix": text_suffix,
|
||||
"fill_bg": fill_bg,
|
||||
"custom_unit": custom_unit,
|
||||
"custom_units": custom_units,
|
||||
"separator": separator,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -494,7 +494,7 @@ class BaseDecorator:
|
||||
self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs)
|
||||
|
||||
@cache
|
||||
def format_value(self, context, value, suppress_zero_inches=False, custom_unit=None, in_unit_length=False):
|
||||
def format_value(self, context, value, suppress_zero_inches=False, suppress_zero_feet=False, custom_unit=None, in_unit_length=False):
|
||||
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
|
||||
precision = drawing_pset_data.get("MetricPrecision", None)
|
||||
if not precision:
|
||||
@@ -506,6 +506,7 @@ class BaseDecorator:
|
||||
precision=precision,
|
||||
decimal_places=decimal_places,
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
suppress_zero_feet=suppress_zero_feet,
|
||||
custom_unit=custom_unit,
|
||||
in_unit_length=in_unit_length,
|
||||
)
|
||||
@@ -722,11 +723,13 @@ class DimensionDecorator(BaseDecorator):
|
||||
if not dimension_data:
|
||||
return
|
||||
show_description_only = dimension_data["show_description_only"]
|
||||
is_ordinate = dimension_data["is_ordinate"]
|
||||
text_prefix = dimension_data["text_prefix"]
|
||||
text_suffix = dimension_data["text_suffix"]
|
||||
viewportDrawingScale = self.get_viewport_drawing_scale(context)
|
||||
text_offset_value = viewportDrawingScale * 3
|
||||
|
||||
ordinate_total = 0.0
|
||||
for i0, i1 in indices:
|
||||
v0 = Vector(vertices[i0])
|
||||
v1 = Vector(vertices[i1])
|
||||
@@ -737,6 +740,10 @@ class DimensionDecorator(BaseDecorator):
|
||||
text_dir = p1 - p0
|
||||
if text_dir.length < 1:
|
||||
continue
|
||||
# Normalize so text always reads left-to-right (or bottom-to-top for
|
||||
# vertical dims) regardless of which end was drawn first.
|
||||
if text_dir.x < 0 or (abs(text_dir.x) < 1e-6 and text_dir.y < 0):
|
||||
text_dir = -text_dir
|
||||
perpendicular = Vector((-text_dir.y, text_dir.x)).normalized()
|
||||
text_offset = perpendicular * text_offset_value
|
||||
|
||||
@@ -745,16 +752,25 @@ class DimensionDecorator(BaseDecorator):
|
||||
"multiline": True,
|
||||
"text_dir": text_dir,
|
||||
}
|
||||
base_pos = p0 + text_dir * 0.5
|
||||
base_pos = p1 if is_ordinate else (p0 + p1) / 2
|
||||
|
||||
if not show_description_only:
|
||||
length = (v1 - v0).length
|
||||
text = self.format_value(
|
||||
context,
|
||||
length,
|
||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
segment_length = (v1 - v0).length
|
||||
if is_ordinate:
|
||||
ordinate_total += segment_length
|
||||
length = ordinate_total if is_ordinate else segment_length
|
||||
units_to_format = dimension_data["custom_units"] if dimension_data["custom_units"] else [None]
|
||||
parts = [
|
||||
self.format_value(
|
||||
context,
|
||||
length,
|
||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||
suppress_zero_feet=dimension_data["suppress_zero_feet"],
|
||||
custom_unit=unit,
|
||||
)
|
||||
for unit in units_to_format
|
||||
]
|
||||
text = dimension_data["separator"].join(str(p) for p in parts)
|
||||
if isinstance(self, DiameterDecorator):
|
||||
text = "D" + text
|
||||
text = text_prefix + text + text_suffix
|
||||
@@ -765,15 +781,18 @@ class DimensionDecorator(BaseDecorator):
|
||||
|
||||
self.draw_label(
|
||||
text=text,
|
||||
pos=base_pos + text_offset,
|
||||
box_alignment="bottom-middle",
|
||||
pos=base_pos + text_offset + (Vector((0, text_offset_value)) if is_ordinate else Vector((0, 0))),
|
||||
box_alignment="bottom-right" if is_ordinate else "bottom-middle",
|
||||
multiline_to_bottom=False,
|
||||
**common_label_attrs,
|
||||
)
|
||||
|
||||
if not show_description_only and description:
|
||||
self.draw_label(
|
||||
text=description, pos=base_pos - text_offset, box_alignment="top-middle", **common_label_attrs
|
||||
text=description,
|
||||
pos=base_pos - text_offset + (Vector((0, text_offset_value)) if is_ordinate else Vector((0, 0))),
|
||||
box_alignment="top-right" if is_ordinate else "top-middle",
|
||||
**common_label_attrs,
|
||||
)
|
||||
|
||||
|
||||
@@ -969,7 +988,9 @@ class RadiusDecorator(BaseDecorator):
|
||||
|
||||
def get_text():
|
||||
length = (spline_points[-1] - spline_points[-2]).length
|
||||
return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"])
|
||||
units_to_format = dimension_data["custom_units"] if dimension_data["custom_units"] else [None]
|
||||
parts = [self.format_value(context, length, suppress_zero_feet=dimension_data["suppress_zero_feet"], custom_unit=unit) for unit in units_to_format]
|
||||
return "R" + dimension_data["separator"].join(str(p) for p in parts)
|
||||
|
||||
self.draw_dimension_text(
|
||||
context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center"
|
||||
@@ -1275,7 +1296,6 @@ class SectionLevelDecorator(BaseDecorator):
|
||||
add_verts_sequence([v0 + gap, v1 - gap], start_i, **out_kwargs)
|
||||
|
||||
self.draw_lines(context, obj, output_verts, output_edges)
|
||||
assert text_position is not None and text_dir is not None
|
||||
self.draw_labels(context, obj, self.get_splines(obj), text_position.to_2d(), text_dir.to_2d())
|
||||
|
||||
def draw_labels(self, context, obj, splines, text_position, text_dir):
|
||||
@@ -1506,6 +1526,20 @@ class ElevationDecorator(BaseDecorator):
|
||||
"output_edges": output_edges,
|
||||
}
|
||||
|
||||
# Determine the arrow direction in camera-image-plane (XY) space.
|
||||
# The elevation tag's local -Z is intentionally parallel to the drawing
|
||||
# camera's view direction, so projecting it always gives a near-zero XY
|
||||
# delta. Fall through to local +X (which is perpendicular to the view
|
||||
# and rotates visibly when the user spins the tag).
|
||||
view_mat = context.region_data.view_matrix
|
||||
edge_dir_2d = Vector((1.0, 0.0)) # final fallback
|
||||
for local_axis in (Vector((0, 0, -1)), Vector((1, 0, 0)), Vector((0, 1, 0))):
|
||||
world_axis = obj.matrix_world.to_3x3() @ local_axis
|
||||
cam_xy = (view_mat.to_3x3() @ world_axis).xy
|
||||
if cam_xy.length > 1e-6:
|
||||
edge_dir_2d = cam_xy.normalized()
|
||||
break
|
||||
|
||||
# process edges
|
||||
for edge in edges_original:
|
||||
v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]]
|
||||
@@ -1514,7 +1548,7 @@ class ElevationDecorator(BaseDecorator):
|
||||
circle_head = get_circle_head(circle_size)
|
||||
start_i = add_verts_sequence(add_offsets(v0, circle_head), start_i, **out_kwargs, closed=True)
|
||||
|
||||
edge_dir = (v1 - v0).normalized()
|
||||
edge_dir = edge_dir_2d.to_3d()
|
||||
side = (edge_dir.yx * Vector((1, -1))).to_3d()
|
||||
triangle_head = get_triangle_head(side, edge_dir, triangle_length, triangle_width)
|
||||
start_i = add_verts_sequence(add_offsets(v0, triangle_head), start_i, **out_kwargs, closed=True)
|
||||
@@ -1599,7 +1633,6 @@ class SectionDecorator(BaseDecorator):
|
||||
start_i = add_verts_sequence([v + v1 for v in circle_head], start_i, **out_kwargs, closed=True)
|
||||
# circle middle divider
|
||||
if not display_end_symbol:
|
||||
assert divider_offset is not None
|
||||
start_i = add_verts_sequence(
|
||||
[v1 + divider_offset[1], v1 - divider_offset[0]], start_i, **out_kwargs
|
||||
)
|
||||
@@ -2131,4 +2164,8 @@ class DecorationsHandler:
|
||||
|
||||
object_decorators = DecoratorData.data.get("object_decorators", [])
|
||||
for obj, decorator in object_decorators:
|
||||
decorator.decorate(context, obj)
|
||||
try:
|
||||
decorator.decorate(context, obj)
|
||||
except ReferenceError:
|
||||
DecoratorData.is_loaded = False
|
||||
break
|
||||
|
||||
@@ -28,7 +28,7 @@ operators via ``target_set_operator``; drag handles inherit modal state
|
||||
from ``GizmoMovable``.
|
||||
"""
|
||||
|
||||
__all__ = [ # ruff: ignore[unsorted-dunder-all]
|
||||
__all__ = [ # noqa: RUF022 (unsorted `__all__`)
|
||||
"GizmoColor",
|
||||
"GizmoAxis",
|
||||
"TextAlignment",
|
||||
@@ -2297,6 +2297,19 @@ DISC = (
|
||||
(1.0, 0.0, 0),
|
||||
)
|
||||
|
||||
# Anchor index currently being edited by SetDimensionAnchor (-1 = none).
|
||||
_active_anchor_idx: int = -1
|
||||
# The annotation curve object being edited (kept so the gizmo group stays
|
||||
# visible even when SetDimensionAnchor temporarily changes the active object).
|
||||
_editing_annotation_obj = None
|
||||
|
||||
|
||||
def set_active_anchor(idx: int, annotation_obj=None) -> None:
|
||||
global _active_anchor_idx, _editing_annotation_obj
|
||||
_active_anchor_idx = idx
|
||||
_editing_annotation_obj = annotation_obj if idx >= 0 else None
|
||||
|
||||
|
||||
X3DISC = (
|
||||
(0.0, 0.0, 0.0),
|
||||
(1.0, 0.0, 0),
|
||||
@@ -2621,6 +2634,370 @@ class ExtrusionWidget(types.GizmoGroup):
|
||||
self.handle.target_set_prop("offset", prop, "value")
|
||||
self.guides.target_set_prop("depth", prop, "value")
|
||||
|
||||
|
||||
class GizmoAnchorHandle(bpy.types.Gizmo):
|
||||
"""Visual-only dot at a parametric dimension vertex.
|
||||
|
||||
No draw_select/invoke — draw_select puts the gizmo in Blender's select buffer
|
||||
and causes the gizmo system to consume clicks even without an explicit invoke,
|
||||
blocking ClickNearestDimensionAnchor from receiving them. All click handling
|
||||
is done by the bim.click_nearest_dimension_anchor keymap operator.
|
||||
"""
|
||||
|
||||
bl_idname = "BIM_GT_anchor_handle"
|
||||
|
||||
__slots__ = ("anchor_index", "custom_shape")
|
||||
|
||||
def setup(self):
|
||||
self.anchor_index = 0
|
||||
self.custom_shape = self.new_custom_shape(type="TRIS", verts=X3DISC)
|
||||
|
||||
def draw(self, context):
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
|
||||
|
||||
class DimensionAnchorWidget(types.GizmoGroup):
|
||||
"""Anchor handle gizmos at each vertex of the active parametric dimension.
|
||||
|
||||
Green dots indicate vertices that are anchored to an IFC element face;
|
||||
orange dots are free world-point anchors. Clicking any dot fires
|
||||
``bim.set_dimension_anchor`` pre-targeted at that vertex index.
|
||||
"""
|
||||
|
||||
bl_idname = "BIM_GGT_dimension_anchors"
|
||||
bl_label = "Dimension Anchor Handles"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
|
||||
|
||||
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE", "PLAN_LEVEL", "SECTION_LEVEL"))
|
||||
_MAX_ANCHORS = 16
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
# Stay visible while SetDimensionAnchor is running (active obj may temporarily
|
||||
# be an IFC element in the face-picking phase rather than the annotation).
|
||||
if _active_anchor_idx >= 0 and _editing_annotation_obj is not None:
|
||||
active = context.active_object
|
||||
if active is _editing_annotation_obj:
|
||||
return True # annotation still active
|
||||
if active is not None and tool.Ifc.get_entity(active) is not None:
|
||||
return True # face-picking phase: active obj is a target element
|
||||
# Active object is None or a non-IFC object — the modal ended without
|
||||
# calling set_active_anchor(-1). Reset stale state and fall through.
|
||||
set_active_anchor(-1)
|
||||
obj = context.active_object
|
||||
if not obj or obj.type != "CURVE":
|
||||
return False
|
||||
if not obj.select_get():
|
||||
return False
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
return False
|
||||
import ifcopenshell.util.element as _ue
|
||||
if _ue.get_predefined_type(element) not in cls._DIM_TYPES:
|
||||
return False
|
||||
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||
return bool(pset and pset.get("Anchors"))
|
||||
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
self._handles: list = []
|
||||
for _ in range(self._MAX_ANCHORS):
|
||||
gz = self.gizmos.new("BIM_GT_anchor_handle")
|
||||
gz.scale_basis = 0.2
|
||||
gz.use_draw_modal = True
|
||||
gz.hide = True
|
||||
self._handles.append(gz)
|
||||
|
||||
def refresh(self, context: bpy.types.Context) -> None:
|
||||
import json
|
||||
import ifcopenshell.util.element as _ue
|
||||
|
||||
obj = _editing_annotation_obj if _active_anchor_idx >= 0 and _editing_annotation_obj else context.active_object
|
||||
if not obj or not obj.data or not getattr(obj.data, "splines", None):
|
||||
for gz in self._handles:
|
||||
gz.hide = True
|
||||
return
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
for gz in self._handles:
|
||||
gz.hide = True
|
||||
return
|
||||
|
||||
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||
if not pset or not pset.get("Anchors"):
|
||||
for gz in self._handles:
|
||||
gz.hide = True
|
||||
return
|
||||
|
||||
try:
|
||||
anchors = json.loads(pset["Anchors"])
|
||||
except Exception:
|
||||
for gz in self._handles:
|
||||
gz.hide = True
|
||||
return
|
||||
|
||||
spline = obj.data.splines[0]
|
||||
n = min(len(spline.points), len(anchors), self._MAX_ANCHORS)
|
||||
|
||||
import ifcopenshell.util.element as _ue_gz
|
||||
_ptype_gz = _ue_gz.get_predefined_type(element)
|
||||
_is_elevation_gz = _ptype_gz in ("SECTION_LEVEL", "PLAN_LEVEL")
|
||||
|
||||
for i in range(n):
|
||||
gz = self._handles[i]
|
||||
if _is_elevation_gz:
|
||||
# The object origin IS the anchor reference point (placed at face hit).
|
||||
# Spline vertices are offset from the origin and should not be used.
|
||||
world_co = obj.matrix_world.translation.copy()
|
||||
else:
|
||||
raw_co = spline.points[i].co
|
||||
world_co = obj.matrix_world @ raw_co.to_3d()
|
||||
gz.matrix_basis = Matrix.Translation(world_co)
|
||||
gz.anchor_index = i
|
||||
if i == _active_anchor_idx and obj is _editing_annotation_obj:
|
||||
gz.color = (0.2, 0.7, 1.0)
|
||||
gz.color_highlight = (0.4, 0.85, 1.0)
|
||||
elif anchors[i].get("guid"):
|
||||
gz.color = (0.2, 0.85, 0.2)
|
||||
gz.color_highlight = (0.4, 1.0, 0.4)
|
||||
else:
|
||||
gz.color = (0.9, 0.6, 0.1)
|
||||
gz.color_highlight = (1.0, 0.85, 0.2)
|
||||
gz.alpha = 0.85
|
||||
gz.alpha_highlight = 1.0
|
||||
gz.hide = False
|
||||
|
||||
for i in range(n, self._MAX_ANCHORS):
|
||||
self._handles[i].hide = True
|
||||
|
||||
def draw_prepare(self, context: bpy.types.Context) -> None:
|
||||
self.refresh(context)
|
||||
|
||||
|
||||
class DimensionLinePositionWidget(types.GizmoGroup):
|
||||
"""Drag handle for the LinePosition of a parametric dimension annotation.
|
||||
|
||||
Shows two opposing cones at the midpoint of the dimension curve, oriented
|
||||
along the horizontal offset axis (cross(world_Z, dim_direction)). Dragging
|
||||
either cone updates BBIM_Dimension.LinePosition and regenerates the curve in
|
||||
real time. The forward cone points in +offset_dir; the reverse cone in
|
||||
-offset_dir — both respond to mouse movement along the shared axis so the
|
||||
user can drag in either direction from either handle.
|
||||
"""
|
||||
|
||||
bl_idname = "BIM_GGT_dimension_line_position"
|
||||
bl_label = "Dimension Line Position"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
|
||||
|
||||
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE"))
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
obj = context.active_object
|
||||
if not obj or obj.type != "CURVE":
|
||||
return False
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
return False
|
||||
import ifcopenshell.util.element as _ue
|
||||
if _ue.get_predefined_type(element) not in cls._DIM_TYPES:
|
||||
return False
|
||||
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||
return bool(pset and pset.get("Anchors"))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
|
||||
@staticmethod
|
||||
def _cam_dir() -> "Vector | None":
|
||||
"""Scene camera forward direction, or None."""
|
||||
cam = bpy.context.scene.camera
|
||||
if not cam:
|
||||
return None
|
||||
return (cam.matrix_world.to_3x3() @ Vector((0.0, 0.0, -1.0))).normalized()
|
||||
|
||||
@classmethod
|
||||
def _offset_dir(cls, obj: bpy.types.Object) -> "Vector | None":
|
||||
"""World-space direction perpendicular to the dimension line and in the view plane.
|
||||
|
||||
Plan view (camera mostly vertical): cross(world_Z, dim_dir) — preserves
|
||||
existing stored LinePosition values.
|
||||
Section/elevation (camera mostly horizontal): cross(cam_forward, dim_dir) —
|
||||
keeps the offset axis inside the view plane so the gizmo moves the line
|
||||
visually sideways (up/down in section) rather than into/out of the screen.
|
||||
"""
|
||||
if not obj.data or not hasattr(obj.data, "splines") or not obj.data.splines:
|
||||
return None
|
||||
spline = obj.data.splines[0]
|
||||
if len(spline.points) < 2:
|
||||
return None
|
||||
a = obj.matrix_world @ spline.points[0].co.to_3d()
|
||||
b = obj.matrix_world @ spline.points[-1].co.to_3d()
|
||||
dim = b - a
|
||||
if dim.length < 1e-10:
|
||||
return None
|
||||
dim.normalize()
|
||||
cam_view = cls._cam_dir()
|
||||
cam_is_plan = (cam_view is None) or abs(cam_view.z) > 0.7
|
||||
ref = Vector((0.0, 0.0, 1.0)) if cam_is_plan else cam_view
|
||||
od = ref.cross(dim)
|
||||
if od.length < 1e-6:
|
||||
od = Vector((1.0, 0.0, 0.0)).cross(dim)
|
||||
if od.length < 1e-6:
|
||||
return None
|
||||
return od.normalized()
|
||||
|
||||
@staticmethod
|
||||
def _midpoint(obj: bpy.types.Object) -> "Vector":
|
||||
spline = obj.data.splines[0]
|
||||
pts = [obj.matrix_world @ p.co.to_3d() for p in spline.points]
|
||||
return sum(pts, Vector()) / len(pts)
|
||||
|
||||
@staticmethod
|
||||
def _basis(origin: "Vector", x_axis: "Vector") -> "Matrix":
|
||||
"""4×4 matrix with translation=origin, local-X=x_axis."""
|
||||
ref = Vector((0.0, 0.0, 1.0)) if abs(x_axis.dot(Vector((0.0, 0.0, 1.0)))) < 0.9 else Vector((1.0, 0.0, 0.0))
|
||||
y_ax = x_axis.cross(ref).normalized()
|
||||
z_ax = x_axis.cross(y_ax)
|
||||
return Matrix([
|
||||
[x_axis.x, y_ax.x, z_ax.x, origin.x],
|
||||
[x_axis.y, y_ax.y, z_ax.y, origin.y],
|
||||
[x_axis.z, y_ax.z, z_ax.z, origin.z],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Value callbacks
|
||||
|
||||
def _get_pos(self) -> float:
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return 0.0
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return 0.0
|
||||
import ifcopenshell.util.element as _ue
|
||||
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||
if not pset:
|
||||
return 0.0
|
||||
stored = pset.get("LinePosition")
|
||||
if stored is not None:
|
||||
return float(stored)
|
||||
# Natural position: projection of midpoint onto offset axis
|
||||
od = self._offset_dir(obj)
|
||||
if od is None:
|
||||
return 0.0
|
||||
return self._midpoint(obj).dot(od)
|
||||
|
||||
def _set_pos(self, value: float) -> None:
|
||||
bpy.ops.ed.undo_push(message="Set Line Position")
|
||||
import json
|
||||
import numpy as np
|
||||
import ifcopenshell.util.element as _ue
|
||||
import ifcopenshell.api.pset as _pset_api
|
||||
import ifcopenshell.api.drawing as drawing_api
|
||||
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
file = tool.Ifc.get()
|
||||
if not file:
|
||||
return
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
pset_data = _ue.get_pset(element, "BBIM_Dimension")
|
||||
if not pset_data:
|
||||
return
|
||||
|
||||
pset_entity = file.by_id(pset_data["id"])
|
||||
_pset_api.edit_pset(file, pset=pset_entity, properties={"LinePosition": value})
|
||||
|
||||
anchors = json.loads(pset_data.get("Anchors") or "[]")
|
||||
placement_override: dict = {}
|
||||
for a in anchors:
|
||||
guid = a.get("guid")
|
||||
if not guid:
|
||||
continue
|
||||
try:
|
||||
elem = file.by_guid(guid)
|
||||
elem_obj = tool.Ifc.get_object(elem)
|
||||
if elem_obj:
|
||||
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cam_view = self._cam_dir()
|
||||
cam_dir_tuple = tuple(cam_view) if cam_view is not None else None
|
||||
resolved_pts = drawing_api.regenerate_dimension(
|
||||
file, element, placement_override=placement_override, camera_dir=cam_dir_tuple
|
||||
)
|
||||
if resolved_pts:
|
||||
_update_blender_curve(element, resolved_pts)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GizmoGroup interface
|
||||
|
||||
def _make_cone(self, color: tuple, highlight: tuple) -> "bpy.types.Gizmo":
|
||||
gz = self.gizmos.new("BIM_GT_gizmo_cone")
|
||||
gz.color = color
|
||||
gz.alpha = 0.8
|
||||
gz.color_highlight = highlight
|
||||
gz.alpha_highlight = 1.0
|
||||
gz.scale_basis = 0.15
|
||||
gz.use_draw_modal = True
|
||||
gz.prop_name = "Line Position"
|
||||
gz.move_get_cb = self._get_pos
|
||||
gz.move_set_cb = self._set_pos
|
||||
gz.gizmo_group = self
|
||||
gz.delta_scale = 1.0
|
||||
return gz
|
||||
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
color = (0.9, 0.6, 0.1)
|
||||
highlight = (1.0, 0.9, 0.2)
|
||||
self.gz_fwd = self._make_cone(color, highlight)
|
||||
self.gz_rev = self._make_cone(color, highlight)
|
||||
|
||||
def refresh(self, context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
self.gz_fwd.hide = self.gz_rev.hide = True
|
||||
return
|
||||
|
||||
od = self._offset_dir(obj)
|
||||
if od is None:
|
||||
self.gz_fwd.hide = self.gz_rev.hide = True
|
||||
return
|
||||
|
||||
mid = self._midpoint(obj)
|
||||
# Lift each cone off the dimension line so the arrow base doesn't
|
||||
# overlap anchor dots. 0.3 m gives clear separation at typical zoom.
|
||||
_GAP = 0.15
|
||||
fwd_origin = mid + _GAP * od
|
||||
rev_origin = mid - _GAP * od
|
||||
|
||||
self.gz_fwd.matrix_basis = self._basis(fwd_origin, od)
|
||||
self.gz_fwd.axis = od.copy()
|
||||
self.gz_fwd.hide = False
|
||||
|
||||
# Reverse cone: visually points in -od; same drag axis so both cones
|
||||
# respond identically — drag toward either tip to move the line.
|
||||
self.gz_rev.matrix_basis = self._basis(rev_origin, -od)
|
||||
self.gz_rev.axis = od.copy()
|
||||
self.gz_rev.hide = False
|
||||
|
||||
@staticmethod
|
||||
def get_scale_value(system: str, length_unit: str) -> float:
|
||||
scale_value = 1
|
||||
@@ -2645,6 +3022,76 @@ class ExtrusionWidget(types.GizmoGroup):
|
||||
return scale_value
|
||||
|
||||
|
||||
class DimensionDriveLabelWidget(types.GizmoGroup):
|
||||
"""Pen-icon gizmos at each segment midpoint of the active parametric dimension.
|
||||
|
||||
Clicking a pen invokes ``bim.drive_dimension_length`` for that segment,
|
||||
opening a dialog pre-filled with the current length.
|
||||
"""
|
||||
|
||||
bl_idname = "BIM_GGT_dimension_drive_label"
|
||||
bl_label = "Dimension Drive Label"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
|
||||
|
||||
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE"))
|
||||
_MAX_SEGMENTS = 15
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
if not tool.Ifc.get():
|
||||
return False
|
||||
obj = context.active_object
|
||||
if not obj or obj.type != "CURVE":
|
||||
return False
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
return False
|
||||
import ifcopenshell.util.element as _ue
|
||||
if _ue.get_predefined_type(element) not in cls._DIM_TYPES:
|
||||
return False
|
||||
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||
return bool(pset and pset.get("Anchors"))
|
||||
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
self._labels: list = []
|
||||
for _ in range(self._MAX_SEGMENTS):
|
||||
gz = self.gizmos.new("BIM_GT_drive_dim_label")
|
||||
gz.color = (0.9, 0.75, 0.1)
|
||||
gz.color_highlight = (1.0, 0.95, 0.3)
|
||||
gz.alpha = 0.85
|
||||
gz.alpha_highlight = 1.0
|
||||
gz.scale_basis = 0.18
|
||||
gz.use_draw_modal = True
|
||||
gz.hide = True
|
||||
self._labels.append(gz)
|
||||
|
||||
def refresh(self, context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
if not obj or not obj.data or not getattr(obj.data, "splines", None) or not obj.data.splines:
|
||||
for gz in self._labels:
|
||||
gz.hide = True
|
||||
return
|
||||
|
||||
spline = obj.data.splines[0]
|
||||
pts = [obj.matrix_world @ p.co.to_3d() for p in spline.points]
|
||||
n_segs = min(len(pts) - 1, self._MAX_SEGMENTS)
|
||||
|
||||
for i in range(n_segs):
|
||||
gz = self._labels[i]
|
||||
mid = (pts[i] + pts[i + 1]) * 0.5
|
||||
gz.matrix_basis = Matrix.Translation(mid)
|
||||
gz.segment_index = i
|
||||
gz.hide = False
|
||||
|
||||
for i in range(n_segs, self._MAX_SEGMENTS):
|
||||
self._labels[i].hide = True
|
||||
|
||||
def draw_prepare(self, context: bpy.types.Context) -> None:
|
||||
self.refresh(context)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Core Gizmo Classes
|
||||
# ============================================================================
|
||||
@@ -3611,6 +4058,24 @@ class GizmoPen(StaticTrisGizmoMixin, bpy.types.Gizmo):
|
||||
)
|
||||
|
||||
|
||||
class GizmoDriveDimLabel(bpy.types.Gizmo):
|
||||
"""Visual-only pen icon at a parametric dimension segment midpoint.
|
||||
|
||||
No draw_select/invoke — click handling is done by ClickNearestDimensionAnchor,
|
||||
which dispatches bim.drive_dimension_length on a plain LMB at a midpoint.
|
||||
"""
|
||||
|
||||
bl_idname = "BIM_GT_drive_dim_label"
|
||||
__slots__ = ("segment_index", "custom_shape")
|
||||
|
||||
def setup(self):
|
||||
self.segment_index = 0
|
||||
self.custom_shape = self.new_custom_shape("TRIS", GizmoPen.tris)
|
||||
|
||||
def draw(self, context):
|
||||
self.draw_custom_shape(self.custom_shape)
|
||||
|
||||
|
||||
class GizmoValidate(StaticTrisGizmoMixin, bpy.types.Gizmo):
|
||||
"""Validate/checkmark icon gizmo for confirming edits."""
|
||||
|
||||
@@ -5660,7 +6125,7 @@ class BaseParametricGizmoGroup:
|
||||
"""
|
||||
return 0.0
|
||||
|
||||
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None:
|
||||
def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None: # noqa: ARG002
|
||||
"""Update overall_width, overall_height, and lining_offset based on view direction.
|
||||
|
||||
This base implementation handles the common pattern for door/window gizmos.
|
||||
@@ -5837,7 +6302,7 @@ class BaseParametricGizmoGroup:
|
||||
self.update_dimension_gizmos(mw, props)
|
||||
self._refresh_element_specific(context, mw, props)
|
||||
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
|
||||
def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None: # noqa: ARG002
|
||||
"""Override for element-specific refresh logic.
|
||||
|
||||
Called from both refresh() (on state change) and draw_prepare() (per frame),
|
||||
@@ -6344,7 +6809,7 @@ class BaseParametricGizmoGroup:
|
||||
"""
|
||||
return (0.0, 0.0)
|
||||
|
||||
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float:
|
||||
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float: # noqa: ARG002
|
||||
"""Get Y offset for icons based on view direction.
|
||||
|
||||
Uses get_icon_y_extent() to determine how far to offset icons based on
|
||||
@@ -6546,7 +7011,9 @@ class BaseParametricGizmoGroup:
|
||||
|
||||
self._refresh_element_specific(context, mw, props)
|
||||
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: "Matrix", props) -> None:
|
||||
def _update_dimension_gizmo_positions(
|
||||
self, context: bpy.types.Context, mw: "Matrix", props # noqa: ARG002
|
||||
) -> None:
|
||||
"""Update dimension gizmo positions based on view direction.
|
||||
|
||||
Override this method in subclasses to implement view-dependent
|
||||
|
||||
@@ -16,15 +16,148 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
import bonsai.bim.module.drawing.decoration as decoration
|
||||
import bonsai.tool as tool
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parametric dimension auto-regeneration state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Maps element GUID → list of annotation STEP IDs that reference it.
|
||||
_dim_guid_index: dict = {}
|
||||
# Persistent tessellation cache for the depsgraph handler (element id → shape).
|
||||
_dim_shape_cache: dict = {}
|
||||
# Set True whenever BBIM_Dimension anchors change or a new file loads.
|
||||
_dim_index_dirty: bool = True
|
||||
# Re-entry guard so curve updates don't trigger a second handler call.
|
||||
_dim_handler_running: bool = False
|
||||
|
||||
|
||||
def invalidate_dim_index() -> None:
|
||||
"""Mark the GUID index as stale so it is rebuilt on the next handler call."""
|
||||
global _dim_index_dirty, _dim_shape_cache
|
||||
_dim_index_dirty = True
|
||||
_dim_shape_cache.clear()
|
||||
|
||||
|
||||
def _rebuild_dim_guid_index(file) -> None:
|
||||
global _dim_guid_index, _dim_index_dirty
|
||||
import ifcopenshell.util.element
|
||||
|
||||
_dim_guid_index = {}
|
||||
for annotation in file.by_type("IfcAnnotation"):
|
||||
pset_data = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||
if not pset_data or not pset_data.get("Anchors"):
|
||||
continue
|
||||
try:
|
||||
anchors = json.loads(pset_data["Anchors"])
|
||||
except Exception:
|
||||
continue
|
||||
ann_id = annotation.id()
|
||||
for anchor in anchors:
|
||||
guid = anchor.get("guid")
|
||||
if not guid:
|
||||
continue
|
||||
ids = _dim_guid_index.setdefault(guid, [])
|
||||
if ann_id not in ids:
|
||||
ids.append(ann_id)
|
||||
_dim_index_dirty = False
|
||||
|
||||
|
||||
def regenerate_dims_for_layer(file, layer) -> None:
|
||||
"""Regenerate all parametric dimensions anchored to elements that use *layer*."""
|
||||
global _dim_shape_cache, _dim_index_dirty, _dim_guid_index
|
||||
|
||||
if _dim_index_dirty:
|
||||
_rebuild_dim_guid_index(file)
|
||||
|
||||
affected_guids: set = set()
|
||||
for layer_set in file.get_inverse(layer):
|
||||
if not layer_set.is_a("IfcMaterialLayerSet"):
|
||||
continue
|
||||
for inv in file.get_inverse(layer_set):
|
||||
if inv.is_a("IfcRelAssociatesMaterial"):
|
||||
rels = [inv]
|
||||
elif inv.is_a("IfcMaterialLayerSetUsage"):
|
||||
rels = [r for r in file.get_inverse(inv) if r.is_a("IfcRelAssociatesMaterial")]
|
||||
else:
|
||||
continue
|
||||
for rel in rels:
|
||||
for element in rel.RelatedObjects:
|
||||
if hasattr(element, "GlobalId"):
|
||||
affected_guids.add(element.GlobalId)
|
||||
_dim_shape_cache.pop(element.id(), None)
|
||||
|
||||
if not affected_guids:
|
||||
return
|
||||
|
||||
annotation_ids: set = set()
|
||||
for guid in affected_guids:
|
||||
for ann_id in _dim_guid_index.get(guid, []):
|
||||
annotation_ids.add(ann_id)
|
||||
|
||||
if not annotation_ids:
|
||||
return
|
||||
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.api.drawing as drawing_api
|
||||
import ifcopenshell.geom
|
||||
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||
|
||||
geom_settings = ifcopenshell.geom.settings()
|
||||
geom_settings.set("APPLY_DEFAULT_MATERIALS", False)
|
||||
|
||||
cam = bpy.context.scene.camera
|
||||
cam_dir_tuple = None
|
||||
if cam:
|
||||
from mathutils import Vector as _Vec
|
||||
cam_dir_tuple = tuple((cam.matrix_world.to_3x3() @ _Vec((0, 0, -1))).normalized())
|
||||
|
||||
for ann_id in annotation_ids:
|
||||
try:
|
||||
annotation = file.by_id(ann_id)
|
||||
except Exception:
|
||||
continue
|
||||
pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||
if not pset:
|
||||
continue
|
||||
placement_override: dict = {}
|
||||
try:
|
||||
anchors_raw = json.loads(pset.get("Anchors") or "[]")
|
||||
for anchor in anchors_raw:
|
||||
guid = anchor.get("guid")
|
||||
if not guid:
|
||||
continue
|
||||
try:
|
||||
elem = file.by_guid(guid)
|
||||
elem_obj = tool.Ifc.get_object(elem)
|
||||
if elem_obj:
|
||||
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
resolved_pts = drawing_api.regenerate_dimension(
|
||||
file,
|
||||
annotation,
|
||||
settings=geom_settings,
|
||||
shape_cache=_dim_shape_cache,
|
||||
placement_override=placement_override,
|
||||
camera_dir=cam_dir_tuple,
|
||||
)
|
||||
if resolved_pts:
|
||||
_update_blender_curve(annotation, resolved_pts)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post(*args):
|
||||
invalidate_dim_index()
|
||||
props = tool.Drawing.get_document_props()
|
||||
if props.should_draw_decorations:
|
||||
decoration.DecorationsHandler.install(bpy.context)
|
||||
@@ -61,3 +194,193 @@ def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
|
||||
raster_x, raster_y = props.update_camera_resolution()
|
||||
scene_render.resolution_x = raster_x
|
||||
scene_render.resolution_y = raster_y
|
||||
|
||||
|
||||
def _sync_dimension_anchors_to_curve(file, annotation, obj) -> bool:
|
||||
"""Sync BBIM_Dimension.Anchors length to match the curve's spline point count.
|
||||
|
||||
Called when the user adds or removes vertices from a dimension annotation in
|
||||
Edit Mode. New vertices get a free WORLD-type anchor at their current world
|
||||
position; removed tail vertices simply lose their anchor entries.
|
||||
|
||||
Returns True if the pset was changed.
|
||||
"""
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.api.pset
|
||||
|
||||
if not obj.data or not getattr(obj.data, "splines", None) or not obj.data.splines:
|
||||
return False
|
||||
|
||||
pset_data = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||
if not pset_data or not pset_data.get("Anchors"):
|
||||
return False
|
||||
|
||||
try:
|
||||
anchors: list = json.loads(pset_data["Anchors"])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
spline = obj.data.splines[0]
|
||||
spline_world = [obj.matrix_world @ p.co.to_3d() for p in spline.points]
|
||||
n_pts = len(spline_world)
|
||||
n_anchors = len(anchors)
|
||||
|
||||
if n_pts == n_anchors:
|
||||
return False
|
||||
|
||||
# Match each spline point to the nearest unused anchor by proximity.
|
||||
# This handles insertions (subdivide) and deletions correctly regardless
|
||||
# of where in the polyline the edit happened.
|
||||
_MATCH_THRESH_SQ = 1e-4 # 1 cm² — distinguishes existing pts from new midpoints
|
||||
used: set = set()
|
||||
new_anchors: list = []
|
||||
|
||||
for pt in spline_world:
|
||||
best_idx, best_sq = None, float("inf")
|
||||
for i, anc in enumerate(anchors):
|
||||
if i in used:
|
||||
continue
|
||||
stored = anc.get("pt")
|
||||
if not stored:
|
||||
continue
|
||||
dx, dy, dz = stored[0] - pt.x, stored[1] - pt.y, stored[2] - pt.z
|
||||
sq = dx * dx + dy * dy + dz * dz
|
||||
if sq < best_sq:
|
||||
best_sq, best_idx = sq, i
|
||||
if best_idx is not None and best_sq < _MATCH_THRESH_SQ:
|
||||
new_anchors.append(anchors[best_idx])
|
||||
used.add(best_idx)
|
||||
else:
|
||||
new_anchors.append({
|
||||
"guid": None,
|
||||
"type": "WORLD",
|
||||
"addr": {},
|
||||
"hint": None,
|
||||
"pt": [pt.x, pt.y, pt.z],
|
||||
})
|
||||
|
||||
|
||||
pset_entity = file.by_id(pset_data["id"])
|
||||
ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties={"Anchors": json.dumps(new_anchors)})
|
||||
invalidate_dim_index()
|
||||
return True
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_update_post_handler(scene, depsgraph):
|
||||
"""Auto-regenerate parametric dimensions when referenced elements are moved."""
|
||||
global _dim_handler_running, _dim_index_dirty, _dim_guid_index, _dim_shape_cache
|
||||
|
||||
if _dim_handler_running:
|
||||
return
|
||||
|
||||
file = tool.Ifc.get()
|
||||
if not file:
|
||||
return
|
||||
|
||||
if _dim_index_dirty:
|
||||
_rebuild_dim_guid_index(file)
|
||||
|
||||
import ifcopenshell.util.element
|
||||
|
||||
moved_guids: set = set()
|
||||
edited_annotation_ids: set = set()
|
||||
|
||||
for update in depsgraph.updates:
|
||||
obj = update.id
|
||||
if not isinstance(obj, bpy.types.Object):
|
||||
continue
|
||||
if not (update.is_updated_transform or update.is_updated_geometry):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element is None or not hasattr(element, "GlobalId"):
|
||||
continue
|
||||
|
||||
|
||||
if update.is_updated_geometry and obj.type == "CURVE" and element.is_a("IfcAnnotation"):
|
||||
import ifcopenshell.util.element as _ue
|
||||
ptype = _ue.get_predefined_type(element)
|
||||
if ptype in ("DIMENSION", "RADIUS", "DIAMETER", "ANGLE"):
|
||||
changed = _sync_dimension_anchors_to_curve(file, element, obj)
|
||||
if changed:
|
||||
edited_annotation_ids.add(element.id())
|
||||
continue
|
||||
|
||||
moved_guids.add(element.GlobalId)
|
||||
if update.is_updated_geometry:
|
||||
_dim_shape_cache.pop(element.id(), None)
|
||||
|
||||
annotation_ids: set = set(edited_annotation_ids)
|
||||
for guid in moved_guids:
|
||||
for ann_id in _dim_guid_index.get(guid, []):
|
||||
annotation_ids.add(ann_id)
|
||||
|
||||
if not annotation_ids:
|
||||
return
|
||||
|
||||
import ifcopenshell.api.drawing as drawing_api
|
||||
import ifcopenshell.geom
|
||||
from bonsai.bim.module.drawing.operator import _update_blender_curve, _update_elevation_marker_z
|
||||
|
||||
geom_settings = ifcopenshell.geom.settings()
|
||||
geom_settings.set("APPLY_DEFAULT_MATERIALS", False)
|
||||
|
||||
cam = bpy.context.scene.camera
|
||||
cam_dir_tuple = None
|
||||
if cam:
|
||||
from mathutils import Vector as _Vec
|
||||
cam_dir_tuple = tuple((cam.matrix_world.to_3x3() @ _Vec((0, 0, -1))).normalized())
|
||||
|
||||
_dim_handler_running = True
|
||||
try:
|
||||
for ann_id in annotation_ids:
|
||||
try:
|
||||
annotation = file.by_id(ann_id)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||
if not pset:
|
||||
continue
|
||||
|
||||
placement_override: dict = {}
|
||||
try:
|
||||
anchors_raw = json.loads(pset.get("Anchors") or "[]")
|
||||
for anchor in anchors_raw:
|
||||
guid = anchor.get("guid")
|
||||
if not guid:
|
||||
continue
|
||||
try:
|
||||
elem = file.by_guid(guid)
|
||||
elem_id = elem.id()
|
||||
if elem_id in placement_override:
|
||||
continue
|
||||
elem_obj = tool.Ifc.get_object(elem)
|
||||
if elem_obj:
|
||||
placement_override[elem_id] = np.array(elem_obj.matrix_world)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ptype = ifcopenshell.util.element.get_predefined_type(annotation)
|
||||
if ptype in ("SECTION_LEVEL", "PLAN_LEVEL"):
|
||||
_update_elevation_marker_z(
|
||||
file, annotation,
|
||||
settings=geom_settings,
|
||||
shape_cache=_dim_shape_cache,
|
||||
placement_override=placement_override,
|
||||
)
|
||||
else:
|
||||
resolved_pts = drawing_api.regenerate_dimension(
|
||||
file,
|
||||
annotation,
|
||||
settings=geom_settings,
|
||||
shape_cache=_dim_shape_cache,
|
||||
placement_override=placement_override,
|
||||
camera_dir=cam_dir_tuple,
|
||||
)
|
||||
if resolved_pts:
|
||||
_update_blender_curve(annotation, resolved_pts)
|
||||
finally:
|
||||
_dim_handler_running = False
|
||||
|
||||
@@ -170,6 +170,7 @@ def format_distance(
|
||||
precision=None,
|
||||
decimal_places=None,
|
||||
suppress_zero_inches=False,
|
||||
suppress_zero_feet=False,
|
||||
in_unit_length=False,
|
||||
custom_unit=None,
|
||||
):
|
||||
@@ -319,10 +320,10 @@ def format_distance(
|
||||
tx_dist = ""
|
||||
if feet:
|
||||
tx_dist += str(feet) + "'"
|
||||
if not feet and not add_inches:
|
||||
if not feet and not add_inches and not suppress_zero_feet:
|
||||
tx_dist += str(feet) + "'"
|
||||
|
||||
if not feet and add_inches and unit_length != "INCHES":
|
||||
if not feet and add_inches and unit_length != "INCHES" and not suppress_zero_feet:
|
||||
if value < 0:
|
||||
tx_dist += "-0' - "
|
||||
else:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,14 @@ def update_diagram_scale(self: "BIMCameraProperties", context: bpy.types.Context
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties=diagram_scale)
|
||||
self.update_camera_resolution()
|
||||
|
||||
group = tool.Drawing.get_drawing_group(element)
|
||||
if group:
|
||||
for annotation in tool.Drawing.get_group_elements(group) or []:
|
||||
if annotation.is_a("IfcAnnotation") and ifcopenshell.util.element.get_predefined_type(annotation) == "SECTION":
|
||||
ann_obj = tool.Ifc.get_object(annotation)
|
||||
if ann_obj:
|
||||
tool.Drawing.update_section_endpoints(ann_obj, camera)
|
||||
|
||||
|
||||
def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
|
||||
if not self.update_props:
|
||||
@@ -1038,6 +1046,276 @@ def update_sheet_data(self, context):
|
||||
SheetsData.is_loaded = False
|
||||
|
||||
|
||||
# Guard against re-entrant calls when one constraint callback clears the other property.
|
||||
_face_constraint_updating = False
|
||||
|
||||
|
||||
def _update_force_perpendicular(self, context):
|
||||
"""Apply ForcePerpendicularToFace to all selected dimension annotations and regenerate them."""
|
||||
global _face_constraint_updating
|
||||
if _face_constraint_updating:
|
||||
return
|
||||
_face_constraint_updating = True
|
||||
try:
|
||||
import json
|
||||
import numpy as np
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.drawing as drawing_api
|
||||
import bonsai.tool as tool
|
||||
|
||||
file = tool.Ifc.get()
|
||||
if not file:
|
||||
return
|
||||
|
||||
new_value = self.force_perpendicular_to_face
|
||||
if new_value and self.force_parallel_to_face:
|
||||
self.force_parallel_to_face = False
|
||||
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE"))
|
||||
|
||||
targets = []
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_predefined_type(element) not in _DIM_TYPES:
|
||||
continue
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension")
|
||||
if not pset_data:
|
||||
continue
|
||||
targets.append((obj, element, pset_data))
|
||||
|
||||
if not targets:
|
||||
return
|
||||
|
||||
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||
|
||||
for obj, element, pset_data in targets:
|
||||
pset_entity = file.by_id(pset_data["id"])
|
||||
pset_props = {"ForcePerpendicularToFace": new_value}
|
||||
if new_value:
|
||||
pset_props["ForceParallelToFace"] = False
|
||||
ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties=pset_props)
|
||||
|
||||
anchors = json.loads(pset_data.get("Anchors") or "[]")
|
||||
placement_override = {}
|
||||
for a in anchors:
|
||||
guid = a.get("guid")
|
||||
if not guid:
|
||||
continue
|
||||
try:
|
||||
elem = file.by_guid(guid)
|
||||
elem_obj = tool.Ifc.get_object(elem)
|
||||
if elem_obj:
|
||||
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
resolved_pts = drawing_api.regenerate_dimension(file, element, placement_override=placement_override)
|
||||
if resolved_pts:
|
||||
_update_blender_curve(element, resolved_pts)
|
||||
finally:
|
||||
_face_constraint_updating = False
|
||||
|
||||
|
||||
def _update_force_parallel(self, context):
|
||||
"""Apply ForceParallelToFace to all selected dimension annotations and regenerate them."""
|
||||
global _face_constraint_updating
|
||||
if _face_constraint_updating:
|
||||
return
|
||||
_face_constraint_updating = True
|
||||
try:
|
||||
import json
|
||||
import numpy as np
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.drawing as drawing_api
|
||||
import bonsai.tool as tool
|
||||
from mathutils import Vector
|
||||
|
||||
file = tool.Ifc.get()
|
||||
if not file:
|
||||
return
|
||||
|
||||
new_value = self.force_parallel_to_face
|
||||
if new_value and self.force_perpendicular_to_face:
|
||||
self.force_perpendicular_to_face = False
|
||||
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE"))
|
||||
|
||||
targets = []
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_predefined_type(element) not in _DIM_TYPES:
|
||||
continue
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension")
|
||||
if not pset_data:
|
||||
continue
|
||||
targets.append((obj, element, pset_data))
|
||||
|
||||
if not targets:
|
||||
return
|
||||
|
||||
cam = context.scene.camera
|
||||
cam_dir_tuple = None
|
||||
if cam:
|
||||
cd = cam.matrix_world.to_3x3() @ Vector((0, 0, -1))
|
||||
cd.normalize()
|
||||
cam_dir_tuple = (cd.x, cd.y, cd.z)
|
||||
|
||||
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||
|
||||
for obj, element, pset_data in targets:
|
||||
pset_entity = file.by_id(pset_data["id"])
|
||||
pset_props = {"ForceParallelToFace": new_value}
|
||||
if new_value:
|
||||
pset_props["ForcePerpendicularToFace"] = False
|
||||
ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties=pset_props)
|
||||
|
||||
anchors = json.loads(pset_data.get("Anchors") or "[]")
|
||||
placement_override = {}
|
||||
for a in anchors:
|
||||
guid = a.get("guid")
|
||||
if not guid:
|
||||
continue
|
||||
try:
|
||||
elem = file.by_guid(guid)
|
||||
elem_obj = tool.Ifc.get_object(elem)
|
||||
if elem_obj:
|
||||
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
resolved_pts = drawing_api.regenerate_dimension(
|
||||
file, element, placement_override=placement_override, camera_dir=cam_dir_tuple
|
||||
)
|
||||
if resolved_pts:
|
||||
_update_blender_curve(element, resolved_pts)
|
||||
finally:
|
||||
_face_constraint_updating = False
|
||||
|
||||
|
||||
def _get_line_position(self) -> float:
|
||||
"""Return LinePosition from the active annotation's BBIM_Dimension pset.
|
||||
|
||||
Falls back to the natural anchor projection when LinePosition has not been
|
||||
explicitly set, so the field always shows a meaningful value.
|
||||
"""
|
||||
import math
|
||||
import json
|
||||
try:
|
||||
import bpy as _bpy
|
||||
import ifcopenshell.util.element as _ue
|
||||
import bonsai.tool as _tool
|
||||
obj = getattr(_bpy.context, "active_object", None)
|
||||
if obj:
|
||||
element = _tool.Ifc.get_entity(obj)
|
||||
if element and element.is_a("IfcAnnotation"):
|
||||
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||
if pset:
|
||||
stored = pset.get("LinePosition")
|
||||
if stored is not None:
|
||||
return float(stored)
|
||||
raw = pset.get("Anchors")
|
||||
if raw:
|
||||
anchors = json.loads(raw)
|
||||
if len(anchors) >= 2 and anchors[0].get("pt") and anchors[1].get("pt"):
|
||||
a, b = anchors[0]["pt"], anchors[1]["pt"]
|
||||
dx, dy, dz = b[0] - a[0], b[1] - a[1], b[2] - a[2]
|
||||
m = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
if m > 1e-10:
|
||||
ddx, ddy, ddz = dx / m, dy / m, dz / m
|
||||
cam = _bpy.context.scene.camera
|
||||
cam_is_plan = True
|
||||
cvx, cvy, cvz = 0.0, 0.0, 1.0
|
||||
if cam:
|
||||
from mathutils import Vector as _Vec
|
||||
cv = (cam.matrix_world.to_3x3() @ _Vec((0, 0, -1))).normalized()
|
||||
cvx, cvy, cvz = cv.x, cv.y, cv.z
|
||||
cam_is_plan = abs(cvz) > 0.7
|
||||
if cam_is_plan:
|
||||
# cross(world_Z, dim_dir)
|
||||
ox, oy, oz = -ddy, ddx, 0.0
|
||||
else:
|
||||
# cross(cam_dir, dim_dir)
|
||||
ox = cvy * ddz - cvz * ddy
|
||||
oy = cvz * ddx - cvx * ddz
|
||||
oz = cvx * ddy - cvy * ddx
|
||||
om = math.sqrt(ox * ox + oy * oy + oz * oz)
|
||||
if om > 1e-6:
|
||||
od = (ox / om, oy / om, oz / om)
|
||||
pt = anchors[0]["pt"]
|
||||
return float(pt[0] * od[0] + pt[1] * od[1] + pt[2] * od[2])
|
||||
except Exception:
|
||||
pass
|
||||
return 0.0
|
||||
|
||||
|
||||
def _set_line_position(self, value: float) -> None:
|
||||
"""Write LinePosition to all selected dimension annotations and regenerate."""
|
||||
import json
|
||||
import numpy as np
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.drawing as drawing_api
|
||||
import bonsai.tool as tool
|
||||
|
||||
file = tool.Ifc.get()
|
||||
if not file:
|
||||
return
|
||||
|
||||
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE"))
|
||||
|
||||
targets = []
|
||||
import bpy as _bpy
|
||||
for obj in getattr(_bpy.context, "selected_objects", []):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcAnnotation"):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_predefined_type(element) not in _DIM_TYPES:
|
||||
continue
|
||||
pset_data = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension")
|
||||
if not pset_data:
|
||||
continue
|
||||
targets.append((obj, element, pset_data))
|
||||
|
||||
if not targets:
|
||||
return
|
||||
|
||||
from bonsai.bim.module.drawing.operator import _update_blender_curve
|
||||
|
||||
cam = _bpy.context.scene.camera
|
||||
cam_dir_tuple = None
|
||||
if cam:
|
||||
from mathutils import Vector as _Vec
|
||||
cam_dir_tuple = tuple((cam.matrix_world.to_3x3() @ _Vec((0, 0, -1))).normalized())
|
||||
|
||||
for obj, element, pset_data in targets:
|
||||
pset_entity = file.by_id(pset_data["id"])
|
||||
ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties={"LinePosition": value})
|
||||
|
||||
anchors = json.loads(pset_data.get("Anchors") or "[]")
|
||||
placement_override = {}
|
||||
for a in anchors:
|
||||
guid = a.get("guid")
|
||||
if not guid:
|
||||
continue
|
||||
try:
|
||||
elem = file.by_guid(guid)
|
||||
elem_obj = tool.Ifc.get_object(elem)
|
||||
if elem_obj:
|
||||
placement_override[elem.id()] = np.array(elem_obj.matrix_world)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
resolved_pts = drawing_api.regenerate_dimension(
|
||||
file, element, placement_override=placement_override, camera_dir=cam_dir_tuple
|
||||
)
|
||||
if resolved_pts:
|
||||
_update_blender_curve(element, resolved_pts)
|
||||
|
||||
|
||||
class BIMAnnotationProperties(PropertyGroup):
|
||||
object_type: bpy.props.EnumProperty(
|
||||
name="Annotation Object Type", items=annotation_classes, default="TEXT", update=update_annotation_object_type
|
||||
@@ -1051,6 +1329,25 @@ class BIMAnnotationProperties(PropertyGroup):
|
||||
)
|
||||
is_adding_type: bpy.props.BoolProperty(default=False)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
force_perpendicular_to_face: bpy.props.BoolProperty(
|
||||
name="Force ⊥ to Face",
|
||||
description="Constrain dimension vertices to the face normal of the first anchor. When dimensions are selected, toggling this updates them all.",
|
||||
default=False,
|
||||
update=_update_force_perpendicular,
|
||||
)
|
||||
force_parallel_to_face: bpy.props.BoolProperty(
|
||||
name="Force ∥ to Face",
|
||||
description="Constrain dimension vertices to run parallel to the face of the first anchor (along the face, perpendicular to its normal). When dimensions are selected, toggling this updates them all.",
|
||||
default=False,
|
||||
update=_update_force_parallel,
|
||||
)
|
||||
line_position: bpy.props.FloatProperty(
|
||||
name="Line Position",
|
||||
description="Absolute world position of the dimension line along the horizontal axis perpendicular to the dimension. The line is held at this fixed global coordinate even when the measured geometry moves. Updates all selected dimensions.",
|
||||
unit="LENGTH",
|
||||
get=_get_line_position,
|
||||
set=_set_line_position,
|
||||
)
|
||||
tag_rotation_mode: bpy.props.EnumProperty(
|
||||
name="Tag Rotation Mode",
|
||||
description="How to orient the tag relative to the tagged object",
|
||||
|
||||
@@ -102,16 +102,16 @@ void angle_circle_head(
|
||||
in vec4 circle_start, in float circle_angle,
|
||||
in bool counterclockwise,
|
||||
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
|
||||
|
||||
|
||||
// 1 added to CIRCLE_SEGS because we're number of vertices
|
||||
// for n segments is n+1
|
||||
|
||||
|
||||
float angle_d;
|
||||
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
|
||||
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
|
||||
angle_segs = max(1, ceil(circle_angle / angle_d));
|
||||
angle_d = circle_angle / angle_segs;
|
||||
|
||||
|
||||
for(int i = 0; i < (angle_segs + 1); i++) {
|
||||
float angle = angle_d * i;
|
||||
if (counterclockwise) {
|
||||
@@ -143,7 +143,7 @@ void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
|
||||
#define do_vertex(pos, e) (do_vertex_util(pos, vec2(-(e).y, (e).x) / winsize.xy))
|
||||
#define do_vertex_win(pos, e) ( do_vertex( WIN2CLIP( pos ), e ) )
|
||||
|
||||
// if vertex is shared by two segments of the line still need to emit it twice
|
||||
// if vertex is shared by two segments of the line still need to emit it twice
|
||||
// to avoid smoothing artifacts
|
||||
// don't forget to initialize `vec2 EDGE_DIR` for macro to work
|
||||
// `pos0` / `pos1` - vertex position in clip space
|
||||
@@ -203,6 +203,7 @@ def add_verts_sequence(verts, start_i, output_verts, output_edges, closed=False)
|
||||
output_edges.append((i, i + 1))
|
||||
output_verts.append(verts[-1])
|
||||
assert i is not None
|
||||
|
||||
if closed:
|
||||
output_edges.append((i + 1, start_i))
|
||||
return i + 2
|
||||
@@ -275,7 +276,7 @@ class BaseShader:
|
||||
FRAG_GLSL = """
|
||||
uniform vec4 color;
|
||||
uniform float lineWidth;
|
||||
|
||||
|
||||
in float smoothline;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
|
||||
@@ -872,7 +872,11 @@ class SvgWriter:
|
||||
|
||||
v1 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, 0)))
|
||||
v2 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, -1)))
|
||||
angle = -math.degrees((v2 - v1).xy.angle_signed(Vector((0, 1))))
|
||||
delta = (v2 - v1).xy
|
||||
if delta.length <= 1e-6:
|
||||
v2 = self.project_point_onto_camera(obj.matrix_world @ Vector((1, 0, 0)))
|
||||
delta = (v2 - v1).xy
|
||||
angle = -math.degrees(delta.angle_signed(Vector((0, 1)))) if delta.length > 1e-6 else 90.0
|
||||
|
||||
transform = "rotate({}, {}, {})".format(angle, *symbol_position_svg.xy)
|
||||
|
||||
@@ -895,6 +899,8 @@ class SvgWriter:
|
||||
reference_id = "-"
|
||||
sheet_id = "-"
|
||||
drawing = tool.Drawing.get_annotation_element(element)
|
||||
if not drawing:
|
||||
return ("-", "-")
|
||||
reference = tool.Drawing.get_drawing_reference(drawing)
|
||||
if reference:
|
||||
for sheet_reference in tool.Ifc.get().by_type("IfcDocumentReference"):
|
||||
@@ -1367,14 +1373,18 @@ class SvgWriter:
|
||||
|
||||
def get_text():
|
||||
radius = (points[-1].co - points[-2].co).length
|
||||
radius = helper.format_distance(
|
||||
radius,
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
)
|
||||
text = f"R{radius}"
|
||||
return text
|
||||
units_to_format = dimension_data["custom_units"] if dimension_data["custom_units"] else [None]
|
||||
parts = [
|
||||
helper.format_distance(
|
||||
radius,
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
suppress_zero_feet=dimension_data["suppress_zero_feet"],
|
||||
custom_unit=unit,
|
||||
)
|
||||
for unit in units_to_format
|
||||
]
|
||||
return "R" + dimension_data["separator"].join(str(p) for p in parts)
|
||||
|
||||
self.draw_dimension_text(
|
||||
get_text, tag, dimension_data, text_position=text_position, class_str="RADIUS", box_alignment="center"
|
||||
@@ -1445,12 +1455,11 @@ class SvgWriter:
|
||||
O = A.copy()
|
||||
O.z = B.z
|
||||
run = (B - O).length
|
||||
|
||||
angle_tg = None
|
||||
if run != 0:
|
||||
angle_tg = rise / run
|
||||
angle = round(degrees(atan(angle_tg)))
|
||||
else:
|
||||
angle_tg = None
|
||||
angle = 90
|
||||
|
||||
# ues SLOPE_ANGLE as default
|
||||
@@ -1502,10 +1511,12 @@ class SvgWriter:
|
||||
text_format=lambda x: "D" + x,
|
||||
show_description_only=dimension_data["show_description_only"],
|
||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||
suppress_zero_feet=dimension_data["suppress_zero_feet"],
|
||||
text_prefix=dimension_data["text_prefix"],
|
||||
text_suffix=dimension_data["text_suffix"],
|
||||
fill_bg=dimension_data["fill_bg"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
custom_units=dimension_data["custom_units"],
|
||||
separator=dimension_data["separator"],
|
||||
)
|
||||
|
||||
def draw_dimension_annotations(self, obj: bpy.types.Object) -> None:
|
||||
@@ -1516,11 +1527,15 @@ class SvgWriter:
|
||||
dimension_data = DecoratorData.get_dimension_data(obj)
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Curve)
|
||||
is_ordinate = dimension_data["is_ordinate"]
|
||||
for spline in obj.data.splines:
|
||||
points = self.get_spline_points(spline)
|
||||
ordinate_total = 0.0
|
||||
for i in range(len(points) - 1):
|
||||
v0_global = matrix_world @ points[i].co.xyz
|
||||
v1_global = matrix_world @ points[i + 1].co.xyz
|
||||
if is_ordinate:
|
||||
ordinate_total += (v1_global - v0_global).length
|
||||
self.draw_dimension_annotation(
|
||||
v0_global,
|
||||
v1_global,
|
||||
@@ -1528,10 +1543,13 @@ class SvgWriter:
|
||||
dimension_text=dimension_text,
|
||||
show_description_only=dimension_data["show_description_only"],
|
||||
suppress_zero_inches=dimension_data["suppress_zero_inches"],
|
||||
suppress_zero_feet=dimension_data["suppress_zero_feet"],
|
||||
text_prefix=dimension_data["text_prefix"],
|
||||
text_suffix=dimension_data["text_suffix"],
|
||||
fill_bg=dimension_data["fill_bg"],
|
||||
custom_unit=dimension_data["custom_unit"],
|
||||
custom_units=dimension_data["custom_units"],
|
||||
separator=dimension_data["separator"],
|
||||
distance_override=ordinate_total if is_ordinate else None,
|
||||
)
|
||||
|
||||
def draw_measureit_arch_dimension_annotations(self) -> None:
|
||||
@@ -1555,10 +1573,13 @@ class SvgWriter:
|
||||
text_format=lambda x: x,
|
||||
show_description_only=False,
|
||||
suppress_zero_inches=False,
|
||||
suppress_zero_feet=False,
|
||||
text_prefix="",
|
||||
text_suffix="",
|
||||
fill_bg=False,
|
||||
custom_unit=None,
|
||||
custom_units=None,
|
||||
separator=" / ",
|
||||
distance_override=None,
|
||||
) -> None:
|
||||
offset = Vector([self.raw_width, self.raw_height]) / 2
|
||||
v0 = self.project_point_onto_camera(v0_global)
|
||||
@@ -1567,12 +1588,24 @@ class SvgWriter:
|
||||
end = (offset + v1.xy * Vector((1, -1))) * self.svg_scale
|
||||
mid = ((end - start) / 2) + start
|
||||
vector = end - start
|
||||
sheet_dimension = vector.length
|
||||
if sheet_dimension < 1e-6:
|
||||
return
|
||||
perpendicular = Vector((vector.y, -vector.x)).normalized()
|
||||
sheet_dimension = (end - start).length
|
||||
|
||||
# if annotation can't fit offset text to the right of marker
|
||||
text_position = mid if sheet_dimension > 5 else (end + (3 * vector.normalized()))
|
||||
if distance_override is not None:
|
||||
text_position = end
|
||||
else:
|
||||
text_position = mid if sheet_dimension > 5 else (end + (3 * vector.normalized()))
|
||||
angle = math.degrees(vector.angle_signed(Vector((1, 0))))
|
||||
# Keep text readable regardless of draw direction: if the dimension runs
|
||||
# right-to-left the raw angle is near ±180° which renders text upside-down.
|
||||
# Flip both angle and perpendicular so text always reads left-to-right and
|
||||
# stays on the same side of the dimension line.
|
||||
if abs(angle) > 90:
|
||||
angle += 180
|
||||
perpendicular = -perpendicular
|
||||
|
||||
line = self.svg.line(start=start, end=end, class_=" ".join(classes))
|
||||
self.svg.add(line)
|
||||
@@ -1586,15 +1619,20 @@ class SvgWriter:
|
||||
}
|
||||
|
||||
if not show_description_only:
|
||||
dimension = (v1_global - v0_global).length
|
||||
dimension = helper.format_distance(
|
||||
dimension,
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
custom_unit=custom_unit,
|
||||
)
|
||||
text = text_prefix + str(dimension) + text_suffix
|
||||
dimension = distance_override if distance_override is not None else (v1_global - v0_global).length
|
||||
units_to_format = custom_units if custom_units else [None]
|
||||
parts = [
|
||||
helper.format_distance(
|
||||
dimension,
|
||||
precision=self.precision,
|
||||
decimal_places=self.decimal_places,
|
||||
suppress_zero_inches=suppress_zero_inches,
|
||||
suppress_zero_feet=suppress_zero_feet,
|
||||
custom_unit=unit,
|
||||
)
|
||||
for unit in units_to_format
|
||||
]
|
||||
text = text_prefix + separator.join(str(p) for p in parts) + text_suffix
|
||||
else:
|
||||
if not dimension_text:
|
||||
return
|
||||
@@ -1602,8 +1640,8 @@ class SvgWriter:
|
||||
|
||||
text_tags += self.create_text_tag(
|
||||
text,
|
||||
text_position + perpendicular,
|
||||
box_alignment="bottom-middle",
|
||||
text_position + perpendicular + (Vector((0, 1.5)) if distance_override is not None else Vector((0, 0))),
|
||||
box_alignment="bottom-right" if distance_override is not None else "bottom-middle",
|
||||
multiline_to_bottom=False,
|
||||
**text_tag_kwargs,
|
||||
)
|
||||
@@ -1611,8 +1649,8 @@ class SvgWriter:
|
||||
if not show_description_only and dimension_text:
|
||||
text_tags += self.create_text_tag(
|
||||
dimension_text,
|
||||
text_position - perpendicular,
|
||||
box_alignment="top-middle",
|
||||
text_position - perpendicular + (Vector((0, 1.5)) if distance_override is not None else Vector((0, 0))),
|
||||
box_alignment="top-right" if distance_override is not None else "top-middle",
|
||||
multiline_to_bottom=True,
|
||||
**text_tag_kwargs,
|
||||
)
|
||||
|
||||
@@ -555,6 +555,17 @@ class BIM_PT_product_assignments(Panel):
|
||||
|
||||
assert self.layout
|
||||
assert (obj := context.active_object)
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element and tool.Drawing.is_manual_drawing_reference(element):
|
||||
row = self.layout.row(align=True)
|
||||
fallback = "No Reference Assigned" if element.ObjectType == "REFERENCE" else "No Drawing Assigned"
|
||||
row.label(
|
||||
text=ProductAssignmentsData.data["relating_product"] or fallback, icon="IMAGE_DATA"
|
||||
)
|
||||
row.operator("bim.assign_manual_drawing_reference", icon="GREASEPENCIL", text="")
|
||||
return
|
||||
|
||||
props = tool.Drawing.get_object_assigned_product_props(obj)
|
||||
|
||||
if props.is_editing_product:
|
||||
@@ -572,6 +583,8 @@ class BIM_PT_product_assignments(Panel):
|
||||
col.enabled = bool(ProductAssignmentsData.data["relating_product"])
|
||||
|
||||
|
||||
|
||||
|
||||
def get_category_icon(category_name):
|
||||
"""Get appropriate icon for each category"""
|
||||
icons = {
|
||||
@@ -1031,11 +1044,9 @@ class BIM_UL_sheets(bpy.types.UIList):
|
||||
|
||||
if self.filter_name:
|
||||
filter_name = self.filter_name.lower()
|
||||
active_sheet = None
|
||||
active_sheet_index = None
|
||||
for sheet in data.sheets:
|
||||
if sheet.is_sheet:
|
||||
active_sheet = sheet
|
||||
active_sheet_index = len(flt_flags)
|
||||
if filter_name in sheet.name.lower() or filter_name in sheet.identification.lower():
|
||||
flt_flags.append(self.bitflag_filter_item)
|
||||
|
||||
@@ -114,7 +114,11 @@ class AnnotationTool(WorkSpaceTool):
|
||||
bl_description = "Gives you Annotation related superpowers"
|
||||
bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.annotation")
|
||||
bl_widget = None
|
||||
bl_keymap = tool.Blender.get_default_selection_keypmap() + (
|
||||
bl_keymap = (
|
||||
# Before view3d.select: tool keymaps take priority over the addon keymap
|
||||
# where ClickNearestDimensionAnchor is also registered.
|
||||
("bim.click_nearest_dimension_anchor", {"type": "LEFTMOUSE", "value": "PRESS"}, None),
|
||||
) + tool.Blender.get_default_selection_keypmap() + (
|
||||
("bim.annotation_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
|
||||
("bim.annotation_hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
|
||||
("bim.annotation_hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
|
||||
@@ -221,14 +225,68 @@ class AnnotationToolUI:
|
||||
props = tool.Drawing.get_document_props()
|
||||
row.prop(props, "should_draw_decorations", text="Viewport Annotations")
|
||||
|
||||
_DIMENSION_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE"))
|
||||
_ELEVATION_TYPES = frozenset(("SECTION_LEVEL", "PLAN_LEVEL"))
|
||||
|
||||
@classmethod
|
||||
def draw_edit_object_interface(cls, context):
|
||||
if DecoratorData.get_text_data(bpy.context.active_object):
|
||||
obj = bpy.context.active_object
|
||||
if tool.Ifc.get_entity(obj) and DecoratorData.get_text_data(obj):
|
||||
add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "")
|
||||
if bpy.ops.bim.copy_annotation_to_drawing.poll():
|
||||
row = cls.layout.row(align=True)
|
||||
row.operator("bim.copy_annotation_to_drawing", icon="PASTEDOWN", text="Copy To Drawing")
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj) if obj else None
|
||||
if element and element.is_a("IfcAnnotation"):
|
||||
ptype = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if ptype in cls._DIMENSION_TYPES:
|
||||
cls.layout.separator()
|
||||
ann_props = tool.Drawing.get_annotation_props()
|
||||
if ann_props.force_perpendicular_to_face:
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(ann_props, "line_position")
|
||||
cls.layout.separator()
|
||||
row = cls.layout.row(align=True)
|
||||
op = row.operator("bim.regenerate_dimensions", icon="FILE_REFRESH", text="Regenerate")
|
||||
op.active_only = True
|
||||
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj) if obj else None
|
||||
if element and element.is_a("IfcAnnotation"):
|
||||
ptype = ifcopenshell.util.element.get_predefined_type(element)
|
||||
if ptype in cls._DIMENSION_TYPES:
|
||||
cls.layout.separator()
|
||||
ann_props = tool.Drawing.get_annotation_props()
|
||||
if ann_props.force_perpendicular_to_face:
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(ann_props, "line_position")
|
||||
cls.layout.separator()
|
||||
row = cls.layout.row(align=True)
|
||||
op = row.operator("bim.regenerate_dimensions", icon="FILE_REFRESH", text="Regenerate")
|
||||
op.active_only = True
|
||||
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension")
|
||||
if pset and pset.get("Anchors"):
|
||||
row = cls.layout.row(align=True)
|
||||
row.operator("bim.bake_parametric_dimension", text="Bake to Static", icon="UNLINKED")
|
||||
else:
|
||||
row = cls.layout.row(align=True)
|
||||
row.operator("bim.make_dimension_parametric", text="Make Parametric", icon="LINKED")
|
||||
elif ptype in cls._ELEVATION_TYPES:
|
||||
cls.layout.separator()
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension")
|
||||
if pset and pset.get("Anchors"):
|
||||
row = cls.layout.row(align=True)
|
||||
op = row.operator("bim.regenerate_dimensions", icon="FILE_REFRESH", text="Regenerate")
|
||||
op.active_only = True
|
||||
row = cls.layout.row(align=True)
|
||||
row.operator("bim.bake_parametric_dimension", text="Bake to Static", icon="UNLINKED")
|
||||
else:
|
||||
row = cls.layout.row(align=True)
|
||||
row.operator("bim.make_dimension_parametric", text="Make Parametric", icon="LINKED")
|
||||
|
||||
@classmethod
|
||||
def draw_type_selection_interface(cls):
|
||||
# shared by both sidebar and header
|
||||
@@ -251,6 +309,13 @@ class AnnotationToolUI:
|
||||
|
||||
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
|
||||
|
||||
_DIMENSION_TYPES = {"DIMENSION", "RADIUS", "DIAMETER", "ANGLE"}
|
||||
if object_type in _DIMENSION_TYPES:
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(cls.props, "force_perpendicular_to_face")
|
||||
row = cls.layout.row(align=True)
|
||||
row.prop(cls.props, "force_parallel_to_face")
|
||||
|
||||
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")
|
||||
@@ -333,8 +398,20 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if created_objects:
|
||||
bpy.context.view_layer.objects.active = created_objects[-1]
|
||||
|
||||
_PARAMETRIC_DIMENSION_TYPES = frozenset(
|
||||
("DIMENSION", "RADIUS", "DIAMETER", "ANGLE")
|
||||
)
|
||||
_ELEVATION_TYPES = frozenset(("SECTION_LEVEL", "PLAN_LEVEL"))
|
||||
|
||||
def hotkey_S_A(self):
|
||||
if bpy.ops.bim.add_annotation.poll():
|
||||
props = tool.Drawing.get_annotation_props()
|
||||
if props.object_type in self._PARAMETRIC_DIMENSION_TYPES:
|
||||
if bpy.ops.bim.draw_parametric_dimension.poll():
|
||||
bpy.ops.bim.draw_parametric_dimension("INVOKE_DEFAULT")
|
||||
elif props.object_type in self._ELEVATION_TYPES:
|
||||
if bpy.ops.bim.add_elevation_annotation.poll():
|
||||
bpy.ops.bim.add_elevation_annotation("INVOKE_DEFAULT")
|
||||
elif bpy.ops.bim.add_annotation.poll():
|
||||
bpy.ops.bim.add_annotation()
|
||||
|
||||
def hotkey_S_E(self):
|
||||
|
||||
@@ -588,6 +588,9 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
return
|
||||
|
||||
if not product.is_a("IfcGridAxis"):
|
||||
tool.Geometry.clear_cache(product)
|
||||
|
||||
if product.is_a("IfcGridAxis"):
|
||||
# Grid geometry does not follow the "representation" paradigm and needs to be treated specially
|
||||
tool.Model.create_axis_curve(obj, product)
|
||||
@@ -799,7 +802,7 @@ def lock_error_message(name: str) -> str:
|
||||
|
||||
|
||||
def calc_delete_is_batch(ifc_file: ifcopenshell.file, context: bpy.types.Context) -> bool:
|
||||
total_elements = len(tool.Ifc.get().entity_names())
|
||||
total_elements = len(tool.Ifc.get().wrapped_data.entity_names())
|
||||
total_polygons = sum([len(o.data.polygons) for o in context.selected_objects if o.type == "MESH"])
|
||||
# These numbers are a bit arbitrary, but basically batching is only
|
||||
# really necessary on large models and large geometry removals.
|
||||
@@ -1322,6 +1325,10 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
||||
if new_active_obj:
|
||||
context.view_layer.objects.active = new_active_obj
|
||||
|
||||
if any(e.is_a("IfcAnnotation") for e in old_to_new):
|
||||
import bonsai.bim.module.drawing.handler as _drawing_handler
|
||||
_drawing_handler.invalidate_dim_index()
|
||||
|
||||
return old_to_new
|
||||
|
||||
|
||||
@@ -3531,10 +3538,11 @@ class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
|
||||
for representation_map in element.RepresentationMaps:
|
||||
if representation_map.MappedRepresentation == active_representation:
|
||||
product_shape = representation_map
|
||||
assert product_shape is not None
|
||||
|
||||
previous_shape_aspect_id = props.active_item.shape_aspect_id
|
||||
# will be None if item didn't had a shape aspect
|
||||
previous_shape_aspect = tool.Ifc.get_entity_by_id(previous_shape_aspect_id)
|
||||
assert product_shape is not None
|
||||
shape_aspect = tool.Geometry.create_shape_aspect(
|
||||
product_shape, active_representation, [representation_item], previous_shape_aspect
|
||||
)
|
||||
|
||||
@@ -72,10 +72,11 @@ class ExportOBJ(bpy.types.Operator):
|
||||
# Conversion from IFC to OBJ
|
||||
# Settings for obj
|
||||
settings = ifcopenshell.geom.settings()
|
||||
serializer_settings = ifcopenshell.geom.serializer_settings()
|
||||
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SURFACES_AND_SOLIDS)
|
||||
settings.set("apply-default-materials", True)
|
||||
settings.set("use-element-guids", True)
|
||||
serializer_settings.set("use-element-guids", True)
|
||||
settings.set("use-world-coords", True)
|
||||
|
||||
ifc_file: ifcopenshell.file
|
||||
@@ -89,7 +90,7 @@ class ExportOBJ(bpy.types.Operator):
|
||||
obj_file_path = os.path.join(output_dir, "model.obj")
|
||||
mtl_file_path = os.path.join(output_dir, "model.mtl")
|
||||
|
||||
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings)
|
||||
serialiser = ifcopenshell.geom.serializers.obj(obj_file_path, mtl_file_path, settings, serializer_settings)
|
||||
serialiser.setFile(ifc_file)
|
||||
serialiser.setUnitNameAndMagnitude("METER", 1.0)
|
||||
serialiser.writeHeader()
|
||||
@@ -106,7 +107,7 @@ class ExportOBJ(bpy.types.Operator):
|
||||
if iterator.initialize():
|
||||
while True:
|
||||
shape = iterator.get()
|
||||
assert isinstance(shape, W.triangulation_element)
|
||||
assert isinstance(shape, W.TriangulationElement)
|
||||
materials = shape.geometry.materials
|
||||
|
||||
for material in materials:
|
||||
@@ -155,7 +156,7 @@ class RadianceRender(bpy.types.Operator):
|
||||
print(f"Quality: {quality}, Detail: {detail}, Variability: {variability}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
|
||||
hdr_image_path, hdr_mask_path, sky_map_cal_path = None, None, None
|
||||
hdr_image_path, hdr_mask_path, sky_map_cal_path = None, None
|
||||
if use_hdr:
|
||||
hdr_image = "noon_grass_2k.hdr"
|
||||
hdr_mask = "noon_grass_2k_mask.hdr"
|
||||
|
||||
@@ -834,6 +834,8 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
slab.DumbSlabPlaner().regenerate_from_layer(layer)
|
||||
wall.DumbWallPlaner().regenerate_from_layer(layer)
|
||||
from bonsai.bim.module.drawing.handler import regenerate_dims_for_layer
|
||||
regenerate_dims_for_layer(self.file, layer)
|
||||
elif material.is_a("IfcMaterialProfileSet"):
|
||||
profile_def = None
|
||||
if mprops.profiles:
|
||||
|
||||
@@ -430,7 +430,7 @@ class SverchokData:
|
||||
@classmethod
|
||||
def has_sverchok(cls) -> bool:
|
||||
try:
|
||||
import sverchok # ruff: ignore[unused-import]
|
||||
import sverchok # noqa: F401
|
||||
|
||||
return True
|
||||
except ModuleNotFoundError:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user