Compare commits

..

16 Commits

Author SHA1 Message Date
Richard Brice d506f06f95 Add target-unit pickers to the Qto panels; keep unit symbols live 2026-08-10 14:37:36 -07:00
Richard Brice ebb498ddd0 Add dimensional-analysis fallback to get_project_unit() for IfcDerivedUnit 2026-08-10 14:36:28 -07:00
Richard Brice 7727a4fb4d Make quantity take-off respect manual Unit overrides; add target-unit support 2026-08-10 14:36:26 -07:00
Richard Brice c0f967e81e Add UI to pick/override a property or quantity's unit of measure in the Pset/Qto editor
Bonsai's Pset/Qto editor could display a property or quantity's own Unit
override, but had no UI to author one -- only the project-level Project
Units panel existed, which sets defaults, not per-instance overrides.
Builds on the edit_pset/edit_qto Unit-wrapping support and the
get_unit_scale/get_candidate_units helpers added in the previous commit.

- bim/prop.py: Attribute gains unit_id (the STEP id of the property's own
  override, 0 = project default) and unit_id_enum (the dropdown-driving
  dynamic enum, "Default (<symbol>)" plus every candidate unit for the
  attribute's measure type). update_attribute_unit_id converts the stored
  value live when a different unit is picked, so the physical quantity is
  preserved rather than the number being silently relabeled.
- tool/pset.py: is_measurable_special_type/get_candidate_units_for_special_type/
  resolve_effective_unit/convert_attribute_unit support the picker and the
  live conversion. get_special_type_for_prop classifies a property by its
  value's own declared measure type, falling back to an explicitly-attached
  Unit for generic numeric types (e.g. IfcReal) whose spec carries no unit
  semantics of its own but which may still legitimately carry one. Seeding
  in import_pset_from_existing ignores a stray Unit attached to a property
  whose value has no numeric/measure semantics at all (e.g. text), which
  used to crash trying to select an identifier the picker's enum items
  never include.
- bim/module/pset/ui.py: the picker widget itself, next to the value field
  in edit mode, gated on the attribute being measurable.
- bim/module/pset/operator.py: EditPset wraps measurable values with their
  chosen Unit on save, for both properties and quantities. The qto
  rounding-loop fix reaches into the wrapped dict instead of assuming a
  bare float/int, which would otherwise zero out every unit-overridden
  quantity.

Adds regression tests across all of the above, including conversion
correctness, explicit-clear/default round-trips, an unrelated sibling
property's override surviving untouched, and the stray-Unit crash guard.
2026-08-10 11:02:05 -07:00
Richard Brice 3a3c00e6f6 Add per-property/quantity Unit-override support to edit_pset/edit_qto, plus unit-scale and candidate-unit helpers
edit_pset()'s unpack_unit_value() couldn't distinguish "no Unit dict was
passed" from "{"Unit": None, ...} passed to explicitly clear an existing
override" -- both collapsed to a bare None, and every consuming call site
checked truthiness, so there was no way to actually clear a previously-set
property Unit override once one existed. Fixed with a private _NO_UNIT
sentinel; bare (unwrapped) values still leave Unit untouched exactly as
before.

edit_qto() had no Unit-handling capability at all: neither
update_existing_property() nor add_new_properties() ever read or wrote a
quantity's Unit attribute. Added the same {"Unit": ..., "NominalValue": ...}
wrapped-dict convention edit_pset() already supports, disambiguated from
the pre-existing IfcPhysicalComplexQuantity dict convention
({"Discrimination": ..., "HasQuantities": ...}) by checking for a "Unit"
key -- a complex-quantity spec never contains one.

ifcopenshell.util.unit gains two small helpers:
- get_unit_scale(unit): dispatches to get_derived_unit_scale/
  get_named_unit_scale depending on unit type, also used to de-duplicate
  calculate_unit_scale()'s own inline dispatch of the same logic.
- get_candidate_units(ifc_file, unit_type): all units in a file matching a
  given unit type, unlike get_project_unit()'s single-default lookup.

Adds regression tests for all of the above, including explicit-clear,
bare-value-preserves-override, and complex-quantity-routing-unaffected
cases.
2026-08-10 11:02:01 -07:00
Richard Brice b8211143b1 Show resolved unit symbols in read-only Pset/Qto view; add write-back and fallback regression tests
Previously, unit symbols only appeared while a Pset/Qto was in edit mode
(pencil icon) -- the read-only summary view read raw {name: value} dicts
straight from ifcopenshell.util.element.get_psets(), a completely separate
path from the Attribute/unit_symbol machinery, so it never showed a label
even after the earlier fixes. This matters for the "someone in the field
just looking at values" use case, not just editing.

- bim/module/pset/data.py: switch to get_psets(verbose=True) to get each
  property's own entity id, then resolve its unit symbol the same
  override-aware way the edit-mode path does (tool.Pset.get_unit_symbol_for_prop).
  Falls back gracefully (empty symbol) for IfcPreDefinedPropertySet
  attributes, which aren't IfcProperty entities and can't carry a Unit
  override.
- bim/module/pset/ui.py: read-only value button now shows "250 mm" instead
  of just "250".

Also adds the regression tests planned but not yet committed:
- test/tool/test_pset.py: edit a property with its own Unit override and
  write it back, confirming no rescale and the override survives.
- test/bim/test_prop.py (new): get_display_name() falls back to the plain
  name (no crash) when no unit is resolvable or the project has no units
  assigned at all.
2026-08-10 11:01:56 -07:00
Richard Brice b401393fa8 Fix get_property_unit() crash on IfcPropertySingleValue.NominalValue = None
NominalValue is optional -- IfcPropertySingleValue permits a null value --
but get_property_unit() unconditionally accessed prop.NominalValue.is_a(),
crashing on any single-value property that's legitimately blank.

Also adds a regression test confirming IfcContextDependentUnit symbols
("each", "boxes", etc.) aren't shadowed by the IfcDerivedUnit branch added
in the previous commit.
2026-08-10 11:01:35 -07:00
Richard Brice 439ea7bf9c Add IfcDerivedUnit support to ifcopenshell.util.unit (scale, symbol, dimension identification) 2026-08-10 11:00:12 -07:00
Richard Brice ae6eb4d18a Fix calculate_unit_scale() crash on SQLite-linked files
IfcSIUnit.Dimensions is a schema-derived attribute that isn't computed for
Bonsai's SQLite-linked "large model" file representation, returning None
there instead of an IfcDimensionalExponents entity. #9278 added an
unconditional unit.Dimensions.LengthExponent access to every IfcSIUnit
processed by calculate_unit_scale(), so it crashed project loading for
any linked file, even ones with no unit prefixes at all -- not just the
prefixed-area/volume case the fix targeted.

Fixed by reading dimensions from the existing si_dimensions table (keyed
by the unit's stored Name, not the unresolvable derived attribute) instead
of unit.Dimensions.

See the PR discussion for a standalone reproduction script.
2026-08-10 10:59:25 -07:00
Robert Sigmundsson f05dd4aea5 Fix #9278. calculate_unit_scale raises the SI prefix to the length exponent for prefixed SQUARE_METRE/CUBIC_METRE units.
An SI prefix attaches to the base unit symbol and the prefixed symbol is
raised to the power as a whole: DECI CUBIC_METRE is dm3 = a litre = 1e-3 m3,
not 0.1 m3. The scale factor previously applied the prefix multiplier
linearly for all IfcSIUnits, inflating volumes x100 and areas x10 for such
declarations (produced e.g. by MagiCAD for Revit MEP exports).

Following the reviewer note in #9278, the exponent is taken from the
derived attribute IfcSIUnit.Dimensions rather than from substring matching
on the unit name: the multiplier is raised to LengthExponent only when the
unit's dimensions are a pure power of length, so prefixed derived units
(KILO PASCAL, MEGA NEWTON) and non-length units (KILO GRAM) correctly keep
the linear multiplier. This matches the exponent handling already present
in convert() and named_dimensions in the same module.

Adds regression tests for prefixed AREAUNIT/VOLUMEUNIT and for the
linear-prefix behaviour of PRESSUREUNIT/MASSUNIT.
2026-08-09 08:41:15 +02:00
Richard Brice 7ed8584edc Revised update_alignment_parameter_segment_tags to make EndTag optional 2026-08-08 10:35:20 -07:00
Richard Brice c5ba22451f Adds update_alignment_parameter_segment_tags function 2026-08-07 14:33:04 -07:00
Richard Brice 048242783e Updates update_key_point_referents to confirm to CT 4.1.4.4.3 2026-08-05 07:26:27 -07:00
Bruno Postle 6f3acc84ee ifcmcp: source tool descriptions from ifcquery/ifcedit instead of duplicating them
Alternative to #8955, for #8951 (23 of 25 ifcmcp tools reach MCP clients
with an empty description because FastMCP reads each wrapper's own
__doc__, and the server.py wrappers had none).

#8955 fixes this by hand-writing a new docstring directly onto each
server.py wrapper. Most of those wrappers are thin passthroughs to
IfcSession methods in core.py, which already had short docstrings, which
themselves mostly delegate to already-documented ifcquery/ifcedit
functions -- so that fix tripled up content across three layers that can
drift out of sync.

This instead enriches the true source (the ifcquery/ifcedit library
functions, useful independently of MCP) and has core.py's IfcSession
methods copy __doc__ from their delegate via a small _use_doc()
decorator, and server.py's tool registration pull description= from the
matching IfcSession method. Methods that aren't pure passthroughs
(session lifecycle, generic API/shape dispatch) keep their own
hand-written docs. Keeps #8955's regression test.

Generated with the assistance of an AI coding tool.
2026-08-03 12:52:55 +02:00
Richard Brice e077390e3d add update_key_point_referents to label key alignment points 2026-08-01 15:24:03 -07:00
Richard Brice 80cc603932 alignment: rename get_referent_nest to get_stationing_nest 2026-08-01 15:21:50 -07:00
1198 changed files with 374062 additions and 455857 deletions
+2 -4
View File
@@ -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,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@v6
- 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
+13 -81
View File
@@ -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
@@ -40,20 +39,10 @@ jobs:
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
@@ -88,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}" \
python3 ./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
@@ -133,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.
python3 src/bonsaiviewer-autodesk/packaging/build.py
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
test -d "$autodesk_connector_dir"
cd ./build/`uname`/*/10.15/install/ifcopenshell
mkdir -p ~/output
install_root="$PWD"
stage_runtime_payload() {
dest="$1"
while IFS= read -r runtime_file; do
cp -L "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
}
mkdir ~/output
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
@@ -169,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
+15 -36
View File
@@ -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,32 +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: Setup tmate session
if: failure()
uses: mxschmitt/action-tmate@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
@@ -106,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"
+11 -166
View File
@@ -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
+13 -158
View File
@@ -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
+1 -19
View File
@@ -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:
+2 -1
View File
@@ -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 \
+24 -10
View File
@@ -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
@@ -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: |
+2 -1
View File
@@ -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 -14
View File
@@ -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,14 +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.parse.so
src/ifcopenshell-python/ifcopenshell/libifcopenshell.plugin.so
# apple
.DS_Store
@@ -144,6 +134,3 @@ CLAUDE.local.md
*.py.tmp*
*.json.tmp*
# bonsaiviewer-autodesk connector build artifacts
/src/bonsaiviewer-autodesk/build/
/src/bonsaiviewer-autodesk/dist/
+1 -1
View File
@@ -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/
+1
View File
@@ -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"
+195 -240
View File
@@ -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
@@ -39,7 +39,8 @@ message(STATUS "Detected version '${RELEASE_VERSION}'")
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.
@@ -48,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,100 +66,67 @@ 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 "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" 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."
)
if (VERSION_OVERRIDE)
file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
else()
set(RELEASE_VERSION "0.8.0")
endif()
project(IfcOpenShell VERSION ${RELEASE_VERSION})
# Make sure CMake modules in this project are found first
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
# 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)
@@ -164,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)
@@ -182,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")
@@ -210,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)
@@ -242,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)
@@ -312,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})
@@ -325,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)
@@ -356,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)
@@ -367,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})
@@ -375,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,
@@ -389,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")
@@ -443,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()
@@ -462,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)
@@ -476,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()
@@ -487,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})
@@ -528,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)
@@ -547,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")
@@ -557,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})
@@ -572,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)
@@ -636,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(
@@ -649,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)
@@ -668,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)
@@ -697,6 +683,10 @@ if(BUILD_DOCUMENTATION)
add_subdirectory(../docs docs)
endif()
if(BUILD_IFCPYTHON)
add_subdirectory(../src/ifcwrap ifcwrap)
endif()
if(BUILD_EXAMPLES)
add_subdirectory(../src/examples examples)
endif()
@@ -709,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
@@ -758,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.")
@@ -787,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")
@@ -794,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")
+4 -1
View File
@@ -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 @@
}
}
]
}
}
+1 -1
View File
@@ -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:
+117
View File
@@ -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})
+3 -4
View File
@@ -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
View File
@@ -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})
+3
View File
@@ -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)
-2
View File
@@ -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@)
-1
View File
@@ -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})
-115
View File
@@ -41,121 +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)
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
+4
View File
@@ -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
View File
@@ -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"
+2
View File
@@ -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:
+6
View File
@@ -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>
-317
View File
@@ -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 0142) ──────────────────────────
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
```
+164 -287
View File
@@ -22,7 +22,7 @@
"""
Example usage:
# Build all targets by default, except BonsaiViewer (it's set explicitly).
# Build all targets by default.
python build-all.py
# Build just the provided targets.
@@ -36,38 +36,30 @@ Available arguments:
``-without-xxx`` - do not build dependency ``xxx`` (e.g. ``--without-swig``)
``-mac-cross-compile-intel`` - cross compile for Intel Mac on Apple Silicon host
``-shared`` - build shared libraries. By default will build static.
``-ifcopenshell-shared`` - build only IfcOpenShell's own libraries as shared
(dependencies stay static). Redundant if ``-shared`` is also passed.
``-diskcleanup`` - clean up build directories after finishing building dependencies
``-v`` - enable verbose logs
Used environment variables:
Boolean-like env variables accept the following values (case-insensitive):
`opt-in` - `1`, `on`, `true`, `yes`; ``opt-out`` - `0`, `off`, `false`, `no`.
- ``CXXFLAGS``, ``CPPFLAGS``, ``CFLAGS``, ``LDFLAGS``
- ``BUILD_DIR`` - build directory. By default will use "build" folder in IfcOpenShell repository.
- ``DEPS_DIR`` - dependencies directory. By default will create automatic folder in build directory.
- ``BUILD_CFG`` - build configuration, 'RelWithDebInfo' by default.
- ``USE_CURRENT_PYTHON_VERSION`` - use current python config instead of compile from source
`off` by default.
- ``IFCOS_NUM_BUILD_PROCS`` - number of concurrent processes defaults to available cores + 1
- ``NO_CLEAN`` - do not clean `ifcopenshell` build directories but continue working on current build
(installed dependencies are never cleared).
By default option is disabled, to enable pass any value from `1`, `on`, `true`.
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (8 schemas), to be supplied as `2x3;4;4x3_add2`
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
`on` by default
(`true` by default, any other value is considered `false`)
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
used to deduce `PYVERSION` (e.g. '3.13.2'), `PYTHONINCLUDE`,
`SIDE_MODULE_CFLAGS`, `SIDE_MODULE_LDFLAGS`.
Allows to build wasm without pyodide build environment, which can be useful for debugging build issues.
Example value: 'pyodide/cpython/installs/python-3.13.2'
- ``ADD_COMMIT_SHA`` - `off` by default. If enabled
- ``ADD_COMMIT_SHA`` - if defined with any non-empty value then
`ADD_COMMIT_SHA` and `VERSION_OVERRIDE` will be set to `ON` while configuring IfcOpenShell
- ``BUILD_BONSAIVIEWER`` - enable building BonsaiViewer, `off` by default.
- ``IFCOS_BUILD_PYTHON_WRAPPER`` - enable building the Python wrapper, `on` by default.
# This script builds IfcOpenShell and its dependencies #
# #
@@ -117,18 +109,24 @@ import logging
import multiprocessing
import os
import platform
import re
import shutil
# @todo temporary for expired mpfr.org certificate on 2023-04-08
import ssl
import subprocess as sp
import sys
import sysconfig
import tarfile
import threading
from datetime import datetime
ssl._create_default_https_context = ssl._create_unverified_context # ty:ignore[invalid-assignment]
import time
from collections.abc import Generator, Sequence
from datetime import datetime
from pathlib import Path
from typing import Literal
from typing import Literal, Union
from urllib.request import urlretrieve
logger = logging.getLogger(__name__)
@@ -136,24 +134,9 @@ logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
logger.addHandler(ch)
def is_on_off(value: str | None, *, default: bool) -> bool:
if value is None:
return default
lowered = value.lower()
if lowered in {"1", "on", "true", "yes"}:
return True
if lowered in {"0", "off", "false", "no"}:
return False
return default
PROJECT_NAME = "IfcOpenShell"
USE_CURRENT_PYTHON_VERSION = is_on_off(os.getenv("USE_CURRENT_PYTHON_VERSION"), default=False)
ADD_COMMIT_SHA = is_on_off(os.getenv("ADD_COMMIT_SHA"), default=False)
IFCOS_BUILD_PYTHON_WRAPPER = is_on_off(os.getenv("IFCOS_BUILD_PYTHON_WRAPPER"), default=True)
BUILD_BONSAIVIEWER = is_on_off(os.getenv("BUILD_BONSAIVIEWER"), default=False)
USE_OCCT = is_on_off(os.getenv("USE_OCCT"), default=True)
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
JSON_VERSION = "3.11.3"
@@ -165,6 +148,7 @@ PCRE_VERSION = "8.41"
LIBXML2_VERSION = "2.13.8"
SWIG_VERSION = "4.2.1"
OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.13.1"
GMP_VERSION = "6.3.0"
MPFR_VERSION = "3.1.6" # latest is 4.1.0
@@ -173,9 +157,6 @@ USD_VERSION = "23.05"
TBB_VERSION = "2021.9.0"
ROCKSDB_VERSION = "10.4.2"
ZSTD_VERSION = "1.5.7"
MANIFOLD_VERSION = "3.2.1"
QT6_VERSION = os.getenv("QT6_VERSION", "6.8.3")
# binaries
cp = "cp"
bash = "bash"
@@ -234,7 +215,6 @@ if WASM:
cecho("WARNING. Couldn't find 'PYODIDE_ROOT' in environment variables.", YELLOW)
cecho("Assuming building wasm outside pyodide build environment and resetting necessary variables.", YELLOW)
os.environ["SIDE_MODULE_CFLAGS"] = get_pyodide_config_var("cflags")
os.environ["SIDE_MODULE_CXXFLAGS"] = get_pyodide_config_var("cxxflags")
os.environ["SIDE_MODULE_LDFLAGS"] = get_pyodide_config_var("ldflags")
# Override cmake toolchain for all `emcmake` calls,
# needed for shared libraries (resulting .so wrapper)
@@ -242,14 +222,13 @@ if WASM:
os.environ["CMAKE_TOOLCHAIN_FILE"] = get_pyodide_config_var("cmake_toolchain_file")
required_vars = (
"SIDE_MODULE_CFLAGS",
"SIDE_MODULE_CXXFLAGS",
"SIDE_MODULE_LDFLAGS",
"CMAKE_TOOLCHAIN_FILE",
)
missing_vars = [v for v in required_vars if v not in os.environ]
assert not missing_vars, f"Some variables required for WASM compilation are missing: {', '.join(missing_vars)}"
def get_pyodide_build_version() -> tuple[int, ...]:
def get_pyodide_build_version() -> "tuple[int, ...]":
pyodide_build_suffix = "pyodide-build version:"
output = sp.check_output(["pyodide", "--version"], encoding="utf-8").strip()
assert pyodide_build_suffix in output, output
@@ -267,6 +246,10 @@ if WASM:
# 0.31 is required for SIDE_MODULE_CXXFLAGS to be provided.
assert get_pyodide_build_version() >= (0, 31)
# Set defaults for missing empty environment variables
USE_OCCT = os.environ.get("USE_OCCT", "true").lower() == "true"
TOOLSET = None
if platform.system() == "Darwin":
# C++11 features used in OCCT 7+ need a more recent stdlib
@@ -334,19 +317,19 @@ if BUILD_CFG == "MinSizeRel":
cecho(" WARNING: MinSizeRel build can suffer from a significant performance loss.", RED)
cecho(f"* IFCOS_NUM_BUILD_PROCS = {IFCOS_NUM_BUILD_PROCS}", MAGENTA)
cecho(""" - How many compiler processes may be run in parallel.""")
cecho(f"* IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA)
cecho(""" - How many compiler processes may be run in parallel.
""")
cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA)
cecho(""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
""")
dependency_tree: dict[str, tuple[str, ...]] = {
"IfcParse": ("boost", "libxml2", "rocksdb"),
"IfcGeom": ("IfcParse", "occ", "manifold", "json", "cgal", "eigen", "OpenCOLLADA"),
dependency_tree: "dict[str, tuple[str, ...]]" = {
"IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"),
"IfcGeom": ("IfcParse", "occ", "json", "cgal", "eigen", "OpenCOLLADA"),
"IfcConvert": ("IfcGeom",),
"OpenCOLLADA": ("libxml2", "pcre"),
"IfcGeomServer": ("IfcGeom",),
"IfcOpenShell-Python": ("python", "swig", "IfcGeom"),
"BonsaiViewer": ("IfcGeom", "qt6"),
"swig": (),
"boost": (),
"libxml2": (),
@@ -354,17 +337,16 @@ dependency_tree: dict[str, tuple[str, ...]] = {
"occ": (),
"pcre": (),
"json": (),
"hdf5": (),
"cgal": (),
"eigen": (),
"rocksdb": ("zstd",),
"zstd": (),
"manifold": (),
"qt6": (),
# 'usd': ('boost', 'oneTBB')
}
def gather_dependencies(dep: str) -> Generator[str]:
def gather_dependencies(dep: str) -> "Generator[str]":
yield dep
for d in dependency_tree[dep]:
if f"without-{d.lower()}" not in flags:
@@ -394,9 +376,6 @@ else:
OFF_ON = ["OFF", "ON"]
BUILD_STATIC = "shared" not in flags
"""Whether dependencies are built static."""
IFCOPENSHELL_STATIC = BUILD_STATIC and "ifcopenshell-shared" not in flags
"""Whether IfcOpenShell's own libraries are built static."""
ENABLE_FLAG = "--enable-static" if BUILD_STATIC else "--enable-shared"
DISABLE_FLAG = "--disable-shared" if BUILD_STATIC else "--disable-static"
LINK_TYPE = "static" if BUILD_STATIC else "shared"
@@ -416,20 +395,9 @@ else:
targets = set(dependency_tree.keys())
targets = set(t for t in targets if "without-%s" % t.lower() not in flags)
if not explicit_targets and not BUILD_BONSAIVIEWER:
targets.difference_update({"BonsaiViewer", "qt6"})
if BUILD_BONSAIVIEWER:
targets.update(gather_dependencies("BonsaiViewer"))
# Opt-out for the Python wrapper. Currently used by the bonsai CI workflow
# on macOS, where the post-plug-in-refactor wrapper hard-links
# ifcopenshell.document.rdb.dylib but the CREATE_BUNDLE install rule does
# not actually drop the dylib next to the wrapper in site-packages — see
# the commit message that introduced this gate.
if not IFCOS_BUILD_PYTHON_WRAPPER:
targets.discard("IfcOpenShell-Python")
if WASM:
SKIP_TARGETS_FOR_WASM = {
"hdf5",
"rocksdb",
"opencollada",
"swig",
@@ -437,8 +405,6 @@ if WASM:
"IfcGeom",
"IfcConvert",
"IfcGeomServer",
"BonsaiViewer",
"qt6",
}
SKIP_TARGETS_FOR_WASM = {t.lower() for t in SKIP_TARGETS_FOR_WASM}
skip_targets = {t for t in targets if t.lower() in SKIP_TARGETS_FOR_WASM}
@@ -452,7 +418,7 @@ print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t
yacc = "yacc" # Used during swig building process, installed with `bison` on Debian / `byacc` on Red Hat.
bison = "bison"
missing_commands: list[str] = []
missing_commands: "list[str]" = []
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz, bison]
if "wasm" in flags:
# Skip swig build for WASM.
@@ -460,8 +426,6 @@ if "wasm" in flags:
required_commands.append("pyodide")
required_commands.remove(yacc)
required_commands.remove(bison)
if platform.system() == "Linux" and "BonsaiViewer" in targets:
required_commands.append("patchelf")
for cmd in required_commands:
if shutil.which(cmd) is None:
@@ -496,14 +460,14 @@ except:
pass
def restore_env(var_name: str, old_value: str | None) -> None:
def restore_env(var_name: str, old_value: Union[str, None]) -> None:
if old_value is None:
del os.environ[var_name]
else:
os.environ[var_name] = old_value
def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False) -> str:
def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool = False) -> str:
"""
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
sets up logging `stderr` to `LOG_FILE` (in append mode) and returns stdout
@@ -513,7 +477,7 @@ def run(cmds: Sequence[str], cwd: str | None = None, can_fail: bool = False) ->
def timestamp() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3] # same format as logging
def stream_reader(pipe, collector: list[str], log_file) -> None:
def stream_reader(pipe, collector: "list[str]", log_file) -> None:
for line in iter(pipe.readline, ""):
log_file.write(f"{timestamp()} {line}")
log_file.flush()
@@ -566,7 +530,7 @@ BOOST_LOCATION = f"https://github.com/boostorg/boost/releases/download/boost-{BO
# Helper functions
def run_autoconf(dependency_name: str, configure_args: list[str], cwd: str) -> None:
def run_autoconf(dependency_name: str, configure_args: "list[str]", cwd: str) -> None:
configure_path = os.path.realpath(os.path.join(cwd, "..", "configure"))
if not os.path.exists(configure_path):
run(
@@ -592,7 +556,7 @@ def run_autoconf(dependency_name: str, configure_args: list[str], cwd: str) -> N
)
def run_cmake(arg1, cmake_args: list[str], cmake_dir: str | None = None, cwd: str | None = None):
def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None, cwd: Union[str, None] = None):
if cmake_dir is None:
P = ".."
else:
@@ -614,11 +578,6 @@ def run_cmake(arg1, cmake_args: list[str], cmake_dir: str | None = None, cwd: st
]
)
if not any("BUILD_SHARED_LIBS" in f for f in cmake_args):
cmake_flags.append(
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
)
run(
[
*wasm,
@@ -627,13 +586,14 @@ def run_cmake(arg1, cmake_args: list[str], cmake_dir: str | None = None, cwd: st
*cmake_flags,
*cmake_args,
f"-DCMAKE_BUILD_TYPE={BUILD_CFG}",
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}",
],
cwd=cwd,
)
def git_clone_or_pull_repository(clone_url: str, target_dir: str, revision: str | None = None) -> None:
def git_clone_or_pull_repository(clone_url: str, target_dir: str, revision: Union[str, None] = None) -> None:
"""Lazily clones the `git` repository denoted by `clone_url` into
the `target_dir` or pulls latest changes if the `target_dir` exists (naively assumes
that a working clone exists there) and optionally checks out a revision
@@ -661,18 +621,19 @@ def build_dependency(
mode: Literal[
"cmake",
"autoconf",
"ctest",
"bjam",
],
build_tool_args: list[str],
build_tool_args: "list[str]",
download_url: str,
download_name: str,
*,
download_tool: Literal["py", "git"] = download_tool_default,
revision: str | None = None,
revision: "Union[str, None]" = None,
patch: list[str] | None = None,
shell=None,
pre_compile_subs: Sequence[tuple[str, str, str]] = (),
additional_files: dict[str, str] | None = None,
pre_compile_subs: "Sequence[tuple[str, str, str]]" = (),
additional_files: "Union[dict[str, str], None]" = None,
no_append_name=False,
cmake_dir=None,
**kwargs,
@@ -771,7 +732,23 @@ def build_dependency(
if shell is not None:
sp.run(shell, shell=True, check=True, cwd=extract_dir)
if mode != "bjam":
if mode == "ctest":
try:
run(
["ctest", "-S", "HDF5config.cmake,BUILD_GENERATOR=Unix", "-C", BUILD_CFG, "-V", "-O", "hdf5.log"],
cwd=extract_dir,
)
except Exception as e:
print("-" * 70)
print(open(os.path.join(extract_dir, "hdf5.log")))
print("-" * 70)
raise e
run([tar, "-xf", kwargs["ctest_result"] + ".tar.gz"], cwd=os.path.join(extract_dir, "build"))
shutil.copytree(
os.path.join(extract_dir, "build", kwargs["ctest_result"], kwargs["ctest_result_path"]),
os.path.join(DEPS_DIR, "install", name),
)
elif mode != "bjam":
extract_build_dir = os.path.join(extract_dir, *([cmake_dir] if cmake_dir else []), "build")
if os.path.exists(extract_build_dir):
shutil.rmtree(extract_build_dir)
@@ -810,85 +787,6 @@ def build_dependency(
shutil.rmtree(build_dir, ignore_errors=True)
def get_qt6_aqt_config() -> tuple[str, str, str]:
if platform.system() != "Linux":
raise ValueError("Automatic Qt6 installation with aqtinstall is only configured for Linux builds.")
machine = platform.machine().lower()
if machine in {"x86_64", "amd64"}:
return "linux", "linux_gcc_64", "gcc_64"
if machine in {"aarch64", "arm64"}:
return "linux_arm64", "linux_gcc_arm64", "gcc_arm64"
raise ValueError(f"Automatic Qt6 installation is not configured for architecture '{platform.machine()}'.")
def install_qt6() -> str:
# If the caller pre-set QT_DIR (e.g. macOS CI using Homebrew-installed
# Qt6), validate it points at a real Qt6 install and skip aqtinstall
# entirely. The aqt download is only wired for Linux; on macOS/Windows
# the supported flow is a pre-installed Qt6 advertised via QT_DIR.
preset_qt_dir = os.environ.get("QT_DIR", "").strip()
if preset_qt_dir:
preset_qt_config = Path(preset_qt_dir) / "lib" / "cmake" / "Qt6" / "Qt6Config.cmake"
if preset_qt_config.exists():
logger.info(f"Using pre-set QT_DIR={preset_qt_dir}, skipping aqt install")
return preset_qt_dir
logger.warning(
f"QT_DIR={preset_qt_dir} is set but {preset_qt_config} not found; " f"falling through to aqtinstall"
)
host, qt_arch, install_suffix = get_qt6_aqt_config()
qt_install_root = INSTALL_DIR / f"qt6-{QT6_VERSION}-{install_suffix}"
qt_dir = qt_install_root / QT6_VERSION / install_suffix
os.environ["QT_DIR"] = str(qt_dir)
qt_config = qt_dir / "lib" / "cmake" / "Qt6" / "Qt6Config.cmake"
qt_core = qt_dir / "lib" / "libQt6Core.so.6"
qt_svg = qt_dir / "lib" / "cmake" / "Qt6Svg" / "Qt6SvgConfig.cmake"
if qt_config.exists() and qt_core.exists() and qt_svg.exists():
logger.info(f"Found existing Qt6 at {qt_dir}, skipping")
return str(qt_dir)
os.makedirs(qt_install_root, exist_ok=True)
try:
import aqt # ty:ignore[unresolved-import]
except ModuleNotFoundError:
logger.error(
"Could not find an existing Qt6 install, so aqtinstall is needed to fetch it automatically. "
"Install the `aqtinstall` PyPI package or set QT_DIR."
)
exit(1)
run(
[
sys.executable,
"-m",
"aqt",
"install-qt",
host,
"desktop",
QT6_VERSION,
qt_arch,
"-O",
str(qt_install_root),
# Keep the install lean by filtering archives: qtbase provides
# Core/Gui/Widgets (and the Qt6::CorePrivate target), qtsvg provides
# Qt6::Svg. Both are base-Qt archives, not add-on modules.
"--archives",
"icu",
"qtbase",
"qtsvg",
]
)
if not (qt_config.exists() and qt_core.exists() and qt_svg.exists()):
raise RuntimeError(f"Qt6 installation did not produce a usable Qt at {qt_dir}.")
return str(qt_dir)
cecho("Collecting dependencies:", GREEN)
# Set compiler flags for 32bit builds on 64bit system
@@ -942,16 +840,41 @@ os.environ["CPPFLAGS"] = CXXFLAGS
os.environ["CFLAGS"] = CFLAGS
os.environ["LDFLAGS"] = LDFLAGS
# Use colors in logs to make them more readable locally.
# Output from compiler used by cmake.
os.environ["CMAKE_COLOR_DIAGNOSTICS"] = "ON"
# Output from cmake itself.
os.environ["CLICOLOR_FORCE"] = "1"
# Some dependencies need a more recent CMake version than most distros provide
# @tfk: this is no longer needed
# build_dependency(name="cmake-%s" % (CMAKE_VERSION,), mode="autoconf", build_tool_args=[], download_url="https://cmake.org/files/v%s" % (CMAKE_VERSION_2,), download_name="cmake-%s.tar.gz" % (CMAKE_VERSION,))
if "hdf5" in targets:
# not supported
orig = [os.environ[f] for f in compiler_flags]
for f in compiler_flags:
os.environ[f] = re.sub(r"-flto(=\w+)?", "", os.environ[f])
HDF5_UNDERSCORE = "_".join(HDF5_VERSION.split("."))
HDF5_MAJOR = ".".join(HDF5_VERSION.split(".")[:-1])
dependency_name = f"hdf5-{HDF5_VERSION}"
build_dependency(
name=dependency_name,
mode="cmake",
build_tool_args=[
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}",
"-DHDF5_ENABLE_Z_LIB_SUPPORT=OFF",
"-DBUILD_TESTING=OFF",
"-DHDF5_BUILD_TOOLS=OFF",
"-DHDF5_BUILD_EXAMPLES=OFF",
"-DBUILD_SHARED_LIBS=OFF",
"-DHDF5_BUILD_UTILS=OFF",
"-DHDF5_BUILD_CPP_LIB=ON",
*MAC_CROSS_COMPILE_INTEL_ARGS,
],
download_url=f"https://github.com/HDFGroup/hdf5/archive/refs/tags/",
download_name=f"hdf5-{HDF5_UNDERSCORE}.tar.gz",
)
for f, o in zip(compiler_flags, orig):
os.environ[f] = o
if "json" in targets:
dependency_name = f"json-{JSON_VERSION}"
build_dependency(
@@ -1013,8 +936,8 @@ if "swig" in targets:
)
if USE_OCCT and "occ" in targets:
occt_args: list[str] = []
patches: list[str] = []
occt_args: "list[str]" = []
patches: "list[str]" = []
if OCCT_VERSION < "7.4":
patches.append("./patches/occt/enable-exception-handling.patch")
@@ -1070,33 +993,6 @@ elif "occ" in targets:
download_name=f"OCE-{OCE_VERSION}.tar.gz",
)
if "manifold" in targets:
dependency_name = f"manifold-{MANIFOLD_VERSION}"
patches = []
if WASM:
patches.append("./patches/manifold/install-metadata-for-emscripten.patch")
build_dependency(
name=dependency_name,
mode="cmake",
build_tool_args=[
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}",
"-DMANIFOLD_PAR=OFF",
"-DMANIFOLD_CROSS_SECTION=OFF",
"-DMANIFOLD_PYBIND=OFF",
"-DMANIFOLD_JSBIND=OFF",
"-DMANIFOLD_CBIND=OFF",
"-DMANIFOLD_TEST=OFF",
"-DMANIFOLD_EXPORT=OFF",
"-DMANIFOLD_DOWNLOADS=OFF",
*MAC_CROSS_COMPILE_INTEL_ARGS,
],
download_url="https://github.com/elalish/manifold.git",
download_name="manifold",
download_tool=download_tool_git,
revision=f"v{MANIFOLD_VERSION}",
patch=patches,
)
if "libxml2" in targets:
OLD_CC = ""
if MAC_CROSS_COMPILE_INTEL:
@@ -1128,9 +1024,9 @@ if "libxml2" in targets:
if "OpenCOLLADA" in targets:
patches = ["./patches/opencollada/pr622_and_disable_subdirs.patch"]
# OpenCOLLADAConfig.cmake.in hardcodes shared-lib targets on Unix regardless of
# whether shared libs were actually built. We make it follow `USE_SHARED` instead.
patches.append("./patches/opencollada/config_select_libs_by_use_shared.patch")
# This patch allows static libraries config on Unix,
# because the config is weird and doesn't allow non-shared libraries on Unix.
patches.append("./patches/opencollada/allow_static_libraries_config_on_unix.patch")
if "wasm" in flags:
# This is necessary for the WASM build, because recent versions of
@@ -1151,8 +1047,6 @@ if "OpenCOLLADA" in targets:
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/OpenCOLLADA/",
# OpenCOLLADA is ancient at this point and allows cmake 2.6+, which results in error in cmake 4.
f"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
f"-DUSE_STATIC={OFF_ON[BUILD_STATIC]}",
f"-DUSE_SHARED={OFF_ON[not BUILD_STATIC]}",
*MAC_CROSS_COMPILE_INTEL_ARGS,
],
download_url="https://github.com/KhronosGroup/OpenCOLLADA.git",
@@ -1173,7 +1067,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
# On OSX a dynamic python library is built or it would not be compatible
# with the system python because of some threading initialization
PYTHON_CONFIGURE_ARGS: list[str] = []
PYTHON_CONFIGURE_ARGS: "list[str]" = []
original_path = ""
if platform.system() == "Darwin":
PYTHON_CONFIGURE_ARGS = ["--enable-shared"]
@@ -1259,8 +1153,8 @@ if "boost" in targets:
)
if "cgal" in targets:
gmp_args: list[str] = []
mpfr_args: list[str] = []
gmp_args: "list[str]" = []
mpfr_args: "list[str]" = []
OLD_HOST_CC = None
if WASM:
@@ -1411,9 +1305,6 @@ if "rocksdb" in targets:
revision=f"v{ROCKSDB_VERSION}",
)
if "qt6" in targets:
install_qt6()
cecho("Building IfcOpenShell:", GREEN)
IFCOS_DIR = os.path.join(DEPS_DIR, "build", "ifcopenshell")
@@ -1421,30 +1312,29 @@ if os.environ.get("NO_CLEAN", "").lower() not in {"1", "on", "true"}:
if os.path.exists(IFCOS_DIR):
shutil.rmtree(IFCOS_DIR)
os.makedirs(IFCOS_DIR, exist_ok=True)
ifcos_build_dir = os.path.join(IFCOS_DIR, "build")
os.makedirs(ifcos_build_dir, exist_ok=True)
executables_dir = os.path.join(IFCOS_DIR, "executables")
os.makedirs(executables_dir, exist_ok=True)
cmake_args = [
"-DUSE_MMAP=OFF",
"-DBUILD_EXAMPLES=OFF",
"-DBUILD_SHARED_LIBS=" + OFF_ON[not IFCOPENSHELL_STATIC],
"-DBUILD_SHARED_LIBS=" + OFF_ON[not BUILD_STATIC],
"-DGLTF_SUPPORT=ON",
"-DBoost_NO_BOOST_CMAKE=On",
"-DCREATE_BUNDLE=On",
"-DADD_COMMIT_SHA=" + OFF_ON[ADD_COMMIT_SHA],
"-DVERSION_OVERRIDE=" + OFF_ON[ADD_COMMIT_SHA],
"-DADD_COMMIT_SHA=" + ("On" if ADD_COMMIT_SHA else "Off"),
"-DVERSION_OVERRIDE=" + ("On" if ADD_COMMIT_SHA else "Off"),
*MAC_CROSS_COMPILE_INTEL_ARGS,
]
"""Default CMake args to use for all CMake configs."""
cmake_args_prefix_path: list[str] = [
cmake_args_prefix_path: "list[str]" = [
f"{DEPS_DIR}/install/boost-{BOOST_VERSION}",
f"{DEPS_DIR}/install/eigen-install-{EIGEN_VERSION}",
f"{DEPS_DIR}/install/json-{JSON_VERSION}",
]
def get_cmake_args_prefix_path(additional_paths: Sequence[str] = ()) -> list[str]:
def get_cmake_args_prefix_path(additional_paths: "Sequence[str]" = ()) -> "list[str]":
args_prefix_path = cmake_args_prefix_path.copy()
args_prefix_path.extend(additional_paths)
prefix_path = ";".join(args_prefix_path)
@@ -1481,10 +1371,6 @@ elif "occ" in targets:
occ_library_dir = f"{DEPS_DIR}/install/oce-{OCE_VERSION}/lib"
cmake_args.extend(["-DOCC_INCLUDE_DIR=" + occ_include_dir, "-DOCC_LIBRARY_DIR=" + occ_library_dir])
if "manifold" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/manifold-{MANIFOLD_VERSION}")
cmake_args.append("-DWITH_MANIFOLD=On")
if "OpenCOLLADA" in targets:
# pcre is a dependency of OpenCOLLADA, but since we `find_package`,
# we don't need to add it explicitly here as cmake will find it from the config.
@@ -1499,6 +1385,11 @@ else:
if "libxml2" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}")
if "hdf5" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}")
else:
cmake_args.append("-DHDF5_SUPPORT=Off")
if "usd" in targets:
cmake_args.append("-DUSD_SUPPORT=ON")
cmake_args_prefix_path.extend(
@@ -1525,49 +1416,47 @@ if "rocksdb" in targets:
if "swig" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/swig-{SWIG_VERSION}")
if os.environ.get("QT_DIR"):
cmake_args_prefix_path.append(os.environ["QT_DIR"])
cmake_args.append(f"-DQT_DIR={os.environ['QT_DIR']}")
ifcos_build_args = [
f"-DBUILD_IFCGEOM={OFF_ON['IfcGeom' in targets]}",
f"-DBUILD_GEOMSERVER={OFF_ON['IfcGeomServer' in targets]}",
f"-DBUILD_CONVERT={OFF_ON['IfcConvert' in targets]}",
f"-DBUILD_BONSAIVIEWER={OFF_ON['BonsaiViewer' in targets]}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell",
]
if not WASM and (
"BonsaiViewer" in targets
or not explicit_targets
or {"IfcGeom", "IfcConvert", "IfcGeomServer", "BonsaiViewer"} & set(explicit_targets)
):
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
logger.info("\rConfiguring executables...")
exec_args = [
*ifcos_build_args,
f"-DBUILD_IFCGEOM={OFF_ON['IfcGeom' in targets]}",
f"-DBUILD_GEOMSERVER={OFF_ON['IfcGeomServer' in targets]}",
f"-DBUILD_CONVERT={OFF_ON['IfcConvert' in targets]}",
f"-DBUILD_IFCPYTHON=OFF",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell",
]
run_cmake("", exec_args + cmake_args + get_cmake_args_prefix_path(), cmake_dir=CMAKE_DIR, cwd=ifcos_build_dir)
run_cmake("", exec_args + cmake_args + get_cmake_args_prefix_path(), cmake_dir=CMAKE_DIR, cwd=executables_dir)
logger.info("\rBuilding executables... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=ifcos_build_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=ifcos_build_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=executables_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
if "IfcOpenShell-Python" in targets:
wrapper_ldflags = ""
# On OSX the actual Python library is not linked against.
ADDITIONAL_ARGS = ""
if platform.system() == "Darwin":
# On OSX the actual Python library is not linked against.
wrapper_ldflags = "-Wl,-undefined,dynamic_lookup"
ADDITIONAL_ARGS = "-Wl,-undefined,dynamic_lookup"
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
# So cache doesn't match and running build-all.py builds most of ifcopenshell libraries twice.
os.environ["CPPFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["LDFLAGS"] = f"{LDFLAGS} {ADDITIONAL_ARGS}"
python_dir = os.path.join(IFCOS_DIR, "pythonwrapper")
os.makedirs(python_dir, exist_ok=True)
def compile_python_wrapper(
python_version: str,
python_include: str | None = None,
python_executable: str | None = None,
python_path: Path | None = None,
) -> str | None:
python_include: Union[str, None] = None,
python_executable: Union[str, None] = None,
python_path: Union[Path, None] = None,
) -> Union[str, None]:
"""
:return: Path to module dir if ``python_executable`` was provided, otherwise ``None``.
"""
@@ -1575,6 +1464,10 @@ if "IfcOpenShell-Python" in targets:
logger.info(f"\rConfiguring python {python_version} wrapper...")
cache_path = os.path.join(python_dir, "CMakeCache.txt")
if os.path.exists(cache_path):
os.remove(cache_path)
if python_path:
# We couldn't just prefix PATH and have to provide all variables explicitly,
# see ifcwrap/cmake for the details.
@@ -1588,38 +1481,27 @@ if "IfcOpenShell-Python" in targets:
)
assert python_include
old_ldflags = os.environ["LDFLAGS"]
if wrapper_ldflags:
os.environ["LDFLAGS"] = f"{old_ldflags} {wrapper_ldflags}"
try:
run_cmake(
"",
ifcos_build_args
+ [
"-DBUILD_IFCPYTHON=ON",
]
+ cmake_args
+ get_cmake_args_prefix_path()
+ [
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
],
cmake_dir=CMAKE_DIR,
cwd=ifcos_build_dir,
)
finally:
os.environ["LDFLAGS"] = old_ldflags
run_cmake(
"",
cmake_args
+ get_cmake_args_prefix_path()
+ [
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
],
cmake_dir=CMAKE_DIR,
cwd=python_dir,
)
logger.info(f"\rBuilding python {python_version} wrapper... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=ifcos_build_dir)
run([make, "install/local"], cwd=os.path.join(ifcos_build_dir, "ifcwrap"))
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
if python_executable:
run([python_executable, "-m", "ensurepip"])
@@ -1634,14 +1516,12 @@ if "IfcOpenShell-Python" in targets:
if platform.system() != "Darwin":
if BUILD_CFG == "Release":
for so in glob.glob(os.path.join(module_dir, "*.so")):
if "wasm" in flags:
run(["wasm-strip", so, "-k", "dylink.0"])
elif os.path.basename(so).startswith("_ifcopenshell_wrapper"):
# TODO: This symbol name depends on the Python version?
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", so], cwd=module_dir)
else:
run([strip, "--strip-unneeded", so], cwd=module_dir)
# TODO: This symbol name depends on the Python version?
so = glob.glob(os.path.join(module_dir, "_ifcopenshell_wrapper*.so"))[0]
if "wasm" in flags:
run(["wasm-strip", so, "-k", "dylink.0"])
else:
run([strip, "-s", "-K", "PyInit__ifcopenshell_wrapper", so], cwd=module_dir)
return module_dir
@@ -1668,9 +1548,6 @@ if "IfcOpenShell-Python" in targets:
# cp: /Users/runner/work/IfcOpenShell/IfcOpenShell/build/Darwin/x86_64/10.15/install/ifcopenshell/python-3.9.11: No such file or directory
# D'oh this was just due to a missing f-string f but doesn't hurt to keep it in.
run(["mkdir", "-p", os.path.join(DEPS_DIR, "install", "ifcopenshell")])
dest = os.path.join(DEPS_DIR, "install", "ifcopenshell", f"python-{python_version}")
if os.path.exists(dest):
shutil.rmtree(dest)
run([cp, "-R", module_dir, dest])
run([cp, "-R", module_dir, os.path.join(DEPS_DIR, "install", "ifcopenshell", f"python-{python_version}")])
logger.info("\rBuilt IfcOpenShell...\n\n")
+1
View File
@@ -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
@@ -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
+112
View File
@@ -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
+1
View File
@@ -31,6 +31,7 @@ occt = { version = "*", build = "*novtk*" }
cgal-cpp = "*"
numpy = "*"
lark = "*"
hdf5 = "*"
eigen = "*"
mpfr = "*"
gmp = "*"
-5
View File
@@ -30,8 +30,3 @@ since it's pure cmake without any additional moving parts.
- it will produce Python package in `IfcOpenShell/ifcopenshell`
- run `pyodide build`
- it will produce a wheel in `IfcOpenShell/dist`
- testing:
- ensure you're in pyodide environment
- `cd IfcOpenshell/pyodide`
- `./run_pytest.py setup`
- `./run_pytest.py run`
+1 -8
View File
@@ -22,8 +22,7 @@ uv run pyodide xbuildenv install "${PYODIDE_VERSION}"
uv run pyodide xbuildenv install-emscripten
EMSDK_ROOT="${PYODIDE_XBUILDENV}/emsdk"
[ -f "${EMSDK_ROOT}/emsdk_env.sh" ] && source "${EMSDK_ROOT}/emsdk_env.sh"
[ -f "${EMSDK_ROOT}/../../emsdk_env.sh" ] && source "${EMSDK_ROOT}/../../emsdk_env.sh"
source "${EMSDK_ROOT}/emsdk_env.sh"
which emcc
emcc --version
@@ -36,12 +35,6 @@ sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
# 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,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\.[^.]+\.([^.]+)\.so$")
GEOMETRY_SERIALIZATION_PLUGIN_RE = re.compile(r"^ifcopenshell\.geometry\.serialization\.([^.]+)\.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())
-62
View File
@@ -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()
-311
View File
@@ -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())
-2
View File
@@ -1,2 +0,0 @@
pyodide
pyodide-modular
View File
+11 -24
View File
@@ -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)
""")
"""
)
+1 -2
View File
@@ -9,7 +9,6 @@ line-length = 120
include = '''
src/.*.pyi?$
|nix/.*.pyi?$
|pyodide/.*.pyi?$
'''
extend-exclude = '''
src/ifcopenshell-python/ifcopenshell/express/rules/*
@@ -70,7 +69,7 @@ select = [
ignore = [
"FA100", # Conflicts with Blender using annotations for props definitions.
# Maybe will enable later:
"UP007", # Union[X,Y] to X | Y
"UP007", # Optional to X | Y
"UP045", # Optional to X | None
"UP015", # Unnecessary mode argument
"UP028", # yield for -> yield from
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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()
+1 -1
View File
@@ -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
+79 -1
View File
@@ -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,8 +69,78 @@ 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.select = [
+6
View File
@@ -0,0 +1,6 @@
black
mypy
pylint
isort
xsdata
tox==3.27.1
+5
View File
@@ -370,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
View File
@@ -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.
+1
View File
@@ -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":
+3 -6
View File
@@ -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:
@@ -236,11 +236,8 @@ def import_attribute(
elif data_type == "integer":
new.int_value = 0 if new.is_null else int(data[attribute.name()])
elif data_type == "float":
attribute_type = attribute.type_of_attribute()
if attribute_type._is("IfcLengthMeasure"):
new.special_type = "LENGTH"
elif attribute_type._is("IfcForceMeasure"):
new.special_type = "FORCE"
measure_class = attribute.type_of_attribute().declared_type().name()
new.special_type = tool.Pset.get_special_type_for_measure_class(measure_class)
new.float_value = 0.0 if new.is_null else float(data[attribute.name()])
elif data_type == "enum":
attribute_type = attribute.type_of_attribute()
+5 -1
View File
@@ -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()
@@ -246,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:
+7
View File
@@ -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
@@ -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
@@ -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
+6 -1
View File
@@ -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"
@@ -37,6 +37,7 @@ classes = (
operator.PrintObjectPlacement,
operator.PrintUnusedElementStats,
operator.ProfileImportIFC,
operator.PurgeHdf5Cache,
operator.PurgeUnusedElementsByClass,
operator.PurgeUnusedObjects,
operator.RestartBlender,
+12 -1
View File
@@ -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"}
@@ -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"
+3
View File
@@ -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")
+1 -1
View File
@@ -652,7 +652,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:
@@ -1275,7 +1275,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):
@@ -1599,7 +1598,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
)
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import hashlib
import json
import logging
import multiprocessing
@@ -807,7 +808,6 @@ class CreateDrawing(bpy.types.Operator):
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges)
prev_co = None
if not usage:
sense_factor = 1 # Assume the extrusion vector points in the direction sense
no = tool.Drawing.get_extrusion_vector(element).normalized()
@@ -1003,7 +1003,10 @@ class CreateDrawing(bpy.types.Operator):
# All very hackish whilst prototyping
exporter = bonsai.bim.export_ifc.IfcExporter(None)
exporter.file = tool.Ifc.get()
exporter.sync_all_objects()
invalidated_elements = exporter.sync_all_objects()
invalidated_guids = [e.GlobalId for e in invalidated_elements if hasattr(e, "GlobalId")]
if cache := IfcStore.get_cache():
[cache.remove(guid) for guid in invalidated_guids]
# If we have already calculated it in the SVG in the past, don't recalculate
edited_guids = set()
@@ -1047,6 +1050,10 @@ class CreateDrawing(bpy.types.Operator):
for ifc_path, (ifc, link_matrix) in files.items():
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
ifc_cache_path = os.path.join(prefs.cache_dir, f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
@@ -1407,30 +1414,30 @@ class CreateDrawing(bpy.types.Operator):
pass
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
self.serialiser_settings.set("svg-without-storeys", True)
self.serialiser_settings.set("svg-write-poly", True)
self.serialiser_settings.set("svg-poly", True)
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
self.serialiser_settings.set("profile-threshold", 10000)
self.serialiser_settings.set("svg-xmlns", True)
self.serialiser_settings.set("svg-project", True)
self.serialiser_settings.set("auto-elevation", False)
self.serialiser_settings.set("auto-section", False)
self.serialiser_settings.set("print-space-names", False)
self.serialiser_settings.set("print-space-areas", False)
self.serialiser_settings.set("door-arcs", False)
self.serialiser_settings.set("svg-no-css", True)
self.serialiser_settings.set("elevation-ref-guid", self.camera_element.GlobalId)
self.serialiser_settings.set("scale", str(self.scale))
self.serialiser_settings.set("svg-subtract-before", "always")
self.serialiser_settings.set("svg-prefilter", True) # See #3359
self.serialiser_settings.set("svg-unify-inputs", True)
self.serialiser_settings.set("svg-segment-projection", True)
if target_view == "REFLECTED_PLAN_VIEW":
self.serialiser_settings.set("svg-mirror-y", True)
self.serialiser = ifcopenshell.geom.serializers.svg(
self.svg_buffer, self.svg_settings, self.serialiser_settings
)
self.serialiser.setWithoutStoreys(True)
self.serialiser.setPolygonal(True)
self.serialiser.setUseHlrPoly(True)
# Objects with more than these edges are rendered as wireframe instead of HLR for optimisation
self.serialiser.setProfileThreshold(10000)
self.serialiser.setUseNamespace(True)
self.serialiser.setAlwaysProject(True)
self.serialiser.setAutoElevation(False)
self.serialiser.setAutoSection(False)
self.serialiser.setPrintSpaceNames(False)
self.serialiser.setPrintSpaceAreas(False)
self.serialiser.setDrawDoorArcs(False)
self.serialiser.setNoCSS(True)
self.serialiser.setElevationRefGuid(self.camera_element.GlobalId)
self.serialiser.setScale(self.scale)
self.serialiser.setSubtractionSettings(ifcopenshell.ifcopenshell_wrapper.ALWAYS)
self.serialiser.setUsePrefiltering(True) # See #3359
self.serialiser.setUnifyInputs(True)
self.serialiser.setSegmentProjection(True)
if target_view == "REFLECTED_PLAN_VIEW":
self.serialiser.setMirrorY(True)
# tree = ifcopenshell.geom.tree()
# This instructs the tree to explode BReps into faces and return
# the style of the face when running tree.select_ray()
@@ -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() {
@@ -1445,12 +1445,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
@@ -1031,11 +1031,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)
@@ -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.
@@ -3531,10 +3534,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
)
@@ -156,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"
+1 -1
View File
@@ -484,8 +484,8 @@ class MEPGenerator:
if tool.Cad.is_x(port_local_position.length, 0.0):
start_port = port
break
assert start_port is not None
connected_port = tool.System.get_connected_port(start_port)
connected_element = tool.System.get_port_relating_element(connected_port)
element_type = ifcopenshell.util.element.get_type(connected_element)
@@ -1576,6 +1576,7 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
def link_ifc(self) -> Union[set[str], None]:
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
def should_clear_cache() -> bool:
@@ -374,6 +374,14 @@ class BIMProjectProperties(PropertyGroup):
),
default=False,
)
should_cache: BoolProperty(
name="Cache",
description=(
"Cache loaded geometry to .h5 file in your cache directory (see in preferences) "
"for faster imports and geometry reloads."
),
default=False,
)
deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.05)
angular_tolerance: FloatProperty(name="Angular Tolerance", default=0.5)
void_limit: IntProperty(
@@ -544,6 +552,7 @@ class BIMProjectProperties(PropertyGroup):
should_merge_materials_by_colour: bool
should_load_geometry: bool
should_clean_mesh: bool
should_cache: bool
deflection_tolerance: float
angular_tolerance: float
void_limit: int
+3 -31
View File
@@ -28,9 +28,8 @@ from bpy.types import Menu, Panel, UIList
import bonsai.bim
import bonsai.tool as tool
from bonsai.bim.helper import draw_attributes, prop_with_search
from bonsai.bim.ifc import IfcStore, is_cache_locked_by_other_process
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.project.data import LinksData, ProjectData
from bonsai.bim.ui import draw_multiline_text
if TYPE_CHECKING:
from bonsai.bim.module.project.prop import (
@@ -167,20 +166,6 @@ class BIM_PT_project(Panel):
if pprops.is_loading:
self.draw_advanced_loading_ui(context)
elif self.file or props.ifc_file:
if is_cache_locked_by_other_process():
box = self.layout.box()
box.alert = True
row = box.row(align=True)
row.label(text="IFC Already Open in Another Blender Instance", icon="ERROR")
row.operator("bim.dismiss_multi_instance_warning", text="", icon="CANCEL")
draw_multiline_text(
box.column(align=True),
"This file is open in another Blender instance. Editing the same "
"IFC from two instances at once can lose your work or display "
"outdated geometry. Close the other Blender instances to continue safely.",
context=context,
)
if props.has_blend_warning:
box = self.layout.box()
box.alert = True
@@ -190,21 +175,6 @@ class BIM_PT_project(Panel):
op.uri = "https://docs.bonsaibim.org/guides/troubleshooting.html#saving-and-loading-blend-files"
row.operator("bim.close_blend_warning", text="", icon="CANCEL")
if pending := pprops.pending_opening_recut:
box = self.layout.box()
box.alert = True
box.label(text="Opening Cuts Skipped", icon="ERROR")
draw_multiline_text(
box.column(align=True),
f"{len(pending)} element(s) had too many openings to cut during load. "
f"Apply to recompute their meshes, or dismiss to leave them as they are.",
context=context,
)
row = box.row(align=True)
row.operator("bim.select_pending_opening_cuts", text="Select Elements", icon="RESTRICT_SELECT_OFF")
row.operator("bim.apply_pending_opening_cuts", text="Apply Openings", icon="PLAY")
row.operator("bim.dismiss_pending_opening_cuts", text="", icon="CANCEL")
if props.ifc_file:
self.draw_loaded_project_ui(context)
else:
@@ -237,6 +207,8 @@ class BIM_PT_project(Panel):
row = self.layout.row()
row.prop(pprops, "should_clean_mesh")
row = self.layout.row()
row.prop(pprops, "should_cache")
row = self.layout.row()
row.prop(pprops, "should_load_geometry")
row = self.layout.row()
row.prop(pprops, "should_merge_materials_by_colour")
+17 -2
View File
@@ -54,7 +54,7 @@ class Data:
ifc_file = tool.Ifc.get()
results = []
psetqtos = ifcopenshell.util.element.get_psets(
element, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False
element, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False, verbose=True
)
for name, data in sorted(psetqtos.items()):
pset = ifc_file.by_id(data["id"])
@@ -69,13 +69,28 @@ class Data:
"id": data["id"],
"Name": name,
"is_expanded": is_expanded.get(data["id"], True),
"Properties": [{"Name": k, "NominalValue": v} for k, v in sorted(data.items()) if k != "id"],
"Properties": [
cls.property_display_data(ifc_file, k, v) for k, v in sorted(data.items()) if k != "id"
],
"shared_pset_uses": len(pset_uses),
"has_template": has_template,
}
)
return sorted(results, key=lambda v: v["Name"])
@classmethod
def property_display_data(cls, ifc_file: ifcopenshell.file, name: str, verbose_value: Any) -> dict[str, Any]:
# Predefined property sets (e.g. IfcDoorPanelProperties) expose plain
# attribute values even in verbose mode, since they're typed IFC
# attributes rather than IfcProperty entities with their own id/Unit.
if not isinstance(verbose_value, dict):
return {"Name": name, "NominalValue": verbose_value, "UnitSymbol": ""}
unit_symbol = ""
if (prop_id := verbose_value.get("id")) and (prop_entity := ifc_file.by_id(prop_id)):
unit_symbol = tool.Pset.get_unit_symbol_for_prop(prop_entity, ifc_file)
return {"Name": name, "NominalValue": verbose_value["value"], "UnitSymbol": unit_symbol}
@classmethod
def format_pset_enum(cls, psets):
enum_items = []
+24 -9
View File
@@ -120,13 +120,20 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator):
properties = json.loads(self.properties)
else:
for prop in props.properties:
metadata = prop.metadata
if prop.value_type == "IfcPropertySingleValue":
properties[prop.metadata.name] = prop.metadata.get_value()
value = metadata.get_value()
elif prop.value_type == "IfcPropertyEnumeratedValue":
value_name = prop.metadata.get_value_name()
properties[prop.metadata.name] = [
e[value_name] for e in prop.enumerated_value.enumerated_values if e.is_selected
]
value_name = metadata.get_value_name()
value = [e[value_name] for e in prop.enumerated_value.enumerated_values if e.is_selected]
else:
continue
# None (a purge/skip-creation signal, handled by edit_pset/edit_qto before any
# unit wrapping is unpacked) must stay bare -- only wrap real values.
if value is not None and tool.Pset.is_measurable_special_type(metadata.special_type):
unit = self.file.by_id(metadata.unit_id) if metadata.unit_id else None
value = {"NominalValue": value, "Unit": unit}
properties[metadata.name] = value
if pset.is_a() in ("IfcPropertySet", "IfcMaterialProperties", "IfcProfileProperties"):
ifcopenshell.api.pset.edit_pset(
@@ -140,10 +147,18 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator):
for key, value in properties.items():
if value is None:
continue
if isinstance(value, float):
properties[key] = round(value, 4)
elif not isinstance(value, int):
properties[key] = 0
is_wrapped = isinstance(value, dict) and "Unit" in value
raw = value["NominalValue"] if is_wrapped else value
if raw is None:
continue
if isinstance(raw, float):
raw = round(raw, 4)
elif not isinstance(raw, int):
raw = 0
if is_wrapped:
value["NominalValue"] = raw
else:
properties[key] = raw
ifcopenshell.api.pset.edit_qto(
self.file,
qto=pset,
+8 -3
View File
@@ -67,6 +67,10 @@ def draw_single_property(prop: IfcProperty, layout: bpy.types.UILayout, copy_ope
if prop.metadata.special_type == "URI":
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.attribute_data_path = tool.Blender.get_full_data_path(prop.metadata)
if tool.Pset.is_measurable_special_type(prop.metadata.special_type):
unit_row = layout.row(align=True)
unit_row.scale_x = 0.5
prop_with_search(unit_row, prop.metadata, "unit_id_enum", text="")
if prop.metadata.is_optional:
layout.prop(prop.metadata, "is_null", icon="RADIOBUT_OFF" if prop.metadata.is_null else "RADIOBUT_ON", text="")
if copy_operator:
@@ -203,9 +207,10 @@ def draw_psetqto_ui(
row = box.row(align=True)
row.scale_y = 0.8
row.label(text=prop["Name"])
op = row.operator(
"bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False
)
display_value = get_display_value(nominal_value)
if unit_symbol := prop["UnitSymbol"]:
display_value = f"{display_value} {unit_symbol}"
op = row.operator("bim.select_similar", text=display_value, icon="NONE", emboss=False)
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
# calculate sum of all selected objects
if active_operator:
+2 -2
View File
@@ -151,7 +151,7 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
with Profiler("Quantify function time:"):
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
ifc5d.qto.edit_qtos(ifc_file, results, target_units=tool.Qto.get_target_units(), rules=rules)
not_quantified_elements = elements - set(results.keys())
not_quantified_message = tool.Qto.get_not_quantified_elements_message(not_quantified_elements)
@@ -194,7 +194,7 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
with Profiler("Quantify function time:"):
results = ifc5d.qto.quantify(ifc_file, elements, rules)
ifc5d.qto.edit_qtos(ifc_file, results)
ifc5d.qto.edit_qtos(ifc_file, results, target_units=tool.Qto.get_target_units(), rules=rules)
not_quantified_elements = elements - set(results.keys())
return not_quantified_elements
+28
View File
@@ -27,10 +27,28 @@ from bpy.props import (
)
from bpy.types import PropertyGroup
import bonsai.bim.prop
import bonsai.tool as tool
CALCULATOR_FUNCTION_ENUM_ITEMS: list[Union[tuple[str, str, str], None]] = []
# Measure class (matching ifc5d.qto's Function.measure / SI2ProjectUnitConverter.project_units'
# keys) -> (BIMQtoProperties field name, tool.Pset special_type, UI label).
MEASURE_TO_TARGET_UNIT_FIELD: dict[str, tuple[str, str, str]] = {
"IfcLengthMeasure": ("target_unit_length", "LENGTH", "Length"),
"IfcAreaMeasure": ("target_unit_area", "AREA", "Area"),
"IfcVolumeMeasure": ("target_unit_volume", "VOLUME", "Volume"),
"IfcMassMeasure": ("target_unit_mass", "MASS", "Mass"),
"IfcTimeMeasure": ("target_unit_time", "TIME", "Time"),
}
def _target_unit_items(special_type: str):
def getter(self: "BIMQtoProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
return bonsai.bim.prop.get_unit_enum_items_for_special_type(special_type, tool.Ifc.get())
return getter
def get_qto_rule(self: "BIMQtoProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
results: list[tuple[str, str, str]] = []
@@ -92,6 +110,11 @@ class BIMQtoProperties(PropertyGroup):
),
default=False,
)
target_unit_length: EnumProperty(items=_target_unit_items("LENGTH"), name="Length Unit")
target_unit_area: EnumProperty(items=_target_unit_items("AREA"), name="Area Unit")
target_unit_volume: EnumProperty(items=_target_unit_items("VOLUME"), name="Volume Unit")
target_unit_mass: EnumProperty(items=_target_unit_items("MASS"), name="Mass Unit")
target_unit_time: EnumProperty(items=_target_unit_items("TIME"), name="Time Unit")
if TYPE_CHECKING:
qto_rule: str
@@ -101,3 +124,8 @@ class BIMQtoProperties(PropertyGroup):
qto_name: str
prop_name: str
fallback: bool
target_unit_length: str
target_unit_area: str
target_unit_volume: str
target_unit_mass: str
target_unit_time: str
+20
View File
@@ -17,9 +17,12 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifc5d.qto
import bonsai.tool as tool
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.qto.data import QtoData
from bonsai.bim.module.qto.prop import MEASURE_TO_TARGET_UNIT_FIELD
class BIM_PT_qto(bpy.types.Panel):
@@ -44,6 +47,14 @@ class BIM_PT_qto(bpy.types.Panel):
row = layout.row()
row.prop(props, "qto_rule", text="")
row.prop(props, "fallback", text="", icon="RADIOBUT_ON" if props.fallback else "RADIOBUT_OFF")
box = layout.box()
box.label(text="Target Units (optional, otherwise project default)")
for field_name, _special_type, label in MEASURE_TO_TARGET_UNIT_FIELD.values():
row = box.row(align=True)
row.label(text=label)
prop_with_search(row, props, field_name, text="")
row = layout.row()
row.operator("bim.perform_quantity_take_off")
@@ -66,6 +77,15 @@ class BIM_PT_qto_manual(bpy.types.Panel):
row = layout.row()
row.prop(props, "calculator_function", text="Function")
calculator = ifc5d.qto.calculators.get(props.calculator)
function = calculator.functions.get(props.calculator_function) if calculator else None
target_unit_field = MEASURE_TO_TARGET_UNIT_FIELD.get(function.measure) if function else None
if target_unit_field:
field_name, _special_type, label = target_unit_field
row = layout.row(align=True)
row.label(text=f"{label} Unit")
prop_with_search(row, props, field_name, text="")
row = layout.row(align=True)
row.prop(props, "qto_name", text="")
row.prop(props, "prop_name", text="")
@@ -631,12 +631,13 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
local_z = wall_matrix.to_3x3() @ Vector((0, 0, 1))
direction_sense = getattr(usage, "DirectionSense", "POSITIVE")
if usage.LayerSetDirection == "AXIS2":
layer_set_direction = usage.LayerSetDirection
if layer_set_direction == "AXIS2":
z_axis = tuple(local_y) if direction_sense == "POSITIVE" else tuple(-local_y)
elif usage.LayerSetDirection == "AXIS3":
elif layer_set_direction == "AXIS3":
z_axis = tuple(local_z) if direction_sense == "POSITIVE" else tuple(-local_z)
else:
assert False, usage.LayerSetDirection
assert False, layer_set_direction
item = builder.extrude(
profile,
+2 -2
View File
@@ -95,7 +95,7 @@ class ColourByPropertyData:
element = tool.Ifc.get_entity(obj)
if not element:
return default
keys = [a.name() for a in element.declaration.as_entity().all_attributes()]
keys = [a.name() for a in element.wrapped_data.declaration().as_entity().all_attributes()]
psets = ifcopenshell.util.element.get_psets(element)
for pset, properties in psets.items():
if pset.endswith("Common"):
@@ -126,7 +126,7 @@ class SelectSimilarData:
element = tool.Ifc.get_entity(obj)
if not element:
return []
keys = [a.name() for a in element.declaration.as_entity().all_attributes()]
keys = [a.name() for a in element.wrapped_data.declaration().as_entity().all_attributes()]
psets = ifcopenshell.util.element.get_psets(element, psets_only=True)
for pset, properties in psets.items():
if pset.endswith("Common"):
@@ -849,15 +849,16 @@ class ShaderInfo:
v = l1[1] + fac * (pos - l1[0])
return v
def interpolate(
self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int, key: Literal["load values"]
) -> np.ndarray:
def interpolate(self, pos: float, loadinfo: list[LoadConfigItem], start: int, end: int) -> np.ndarray:
"""interpolate the result vectors between load poits"""
result = np.zeros(6)
for i in range(6):
value1 = [loadinfo[start]["pos"], loadinfo[start][key][i]] # [position, force_component]
value2 = [loadinfo[end]["pos"], loadinfo[end][key][i]] # [position, force_component]
result[i] = self.interp1d(value1, value2, pos) # interpolated [position, force_component]
# [position, force_component]
value1 = [loadinfo[start]["pos"], loadinfo[start]["load values"][i]]
# [position, force_component]
value2 = [loadinfo[end]["pos"], loadinfo[end]["load values"][i]]
# interpolated [position, force_component]
result[i] = self.interp1d(value1, value2, pos)
return result
def get_before_and_after(self, pos: float, load_config_list: list[list[LoadConfigItem]]) -> dict[str, list[float]]:
@@ -901,8 +902,8 @@ class ShaderInfo:
load_before += config[end]["load values"]
elif end - start == 1:
load_before += self.interpolate(pos, config, start, end, "load values")
load_after += self.interpolate(pos, config, start, end, "load values")
load_before += self.interpolate(pos, config, start, end)
load_after += self.interpolate(pos, config, start, end)
start += 1
end -= 1
return_value = {"before": load_before.tolist(), "after": load_after.tolist()}
@@ -230,6 +230,7 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator):
assert representation
tool.Geometry.recut_host(building_obj, representation)
tool.Geometry.unlock_scale_object_with_openings(obj)
tool.Geometry.clear_cache(element)
return {"FINISHED"}
+79 -37
View File
@@ -21,7 +21,6 @@ import os
from typing import TYPE_CHECKING, Any, Literal, Union, assert_never, get_args
import bpy
import ifcopenshell.util.unit
from bpy.props import (
BoolProperty,
CollectionProperty,
@@ -34,6 +33,8 @@ from bpy.props import (
)
from bpy.types import PropertyGroup
import ifcopenshell.util.unit
import bonsai.bim
import bonsai.bim.handler
import bonsai.tool as tool
@@ -122,6 +123,57 @@ def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) ->
return items
def get_unit_enum_items_for_special_type(
special_type: str, ifc_file: Union[ifcopenshell.file, None]
) -> tool.Blender.BLENDER_ENUM_ITEMS:
"""Items for a unit-override picker: "Default (<symbol>)" plus every candidate unit
matching `special_type`, filtered per-caller since candidates depend on the measure type
in question (unlike the globally-shared lists in `bonsai.bim.ui.EnumData`).
"""
if not ifc_file or not tool.Pset.is_measurable_special_type(special_type):
return [(cache_string("0"), cache_string("Default"), "")]
default_symbol = tool.Pset.get_unit_symbol_for_special_type(special_type, ifc_file)
items: list[tuple[str, str, str]] = [
(cache_string("0"), cache_string(f"Default ({default_symbol})" if default_symbol else "Default"), "")
]
for unit in tool.Pset.get_candidate_units_for_special_type(special_type, ifc_file):
name = getattr(unit, "Name", None) or unit.is_a()
symbol = ifcopenshell.util.unit.get_unit_symbol(unit)
label = f"{name} ({symbol})" if symbol else name
items.append((cache_string(str(unit.id())), cache_string(label), ""))
return items
def get_attribute_unit_enum_items(prop: "Attribute", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
"""Items for `Attribute.unit_id_enum`. Wraps `get_unit_enum_items_for_special_type` with a
defensive addition: real-world files sometimes carry a Unit that doesn't cleanly match our
candidate-matching logic (e.g. a mismatched UnitType). Always keep the attribute's own
current override selectable/representable, however unusual, so setting unit_id_enum to
match an already-seeded unit_id can never raise "enum not found".
"""
ifc_file = tool.Ifc.get()
items = get_unit_enum_items_for_special_type(prop.special_type, ifc_file)
if prop.unit_id and prop.unit_id not in {int(i[0]) for i in items}:
own_unit = ifc_file.by_id(prop.unit_id)
name = getattr(own_unit, "Name", None) or own_unit.is_a()
symbol = ifcopenshell.util.unit.get_unit_symbol(own_unit)
label = f"{name} ({symbol})" if symbol else name
items.append((cache_string(str(prop.unit_id)), cache_string(label), ""))
return items
def update_attribute_unit_id(self: "Attribute", context: bpy.types.Context) -> None:
new_unit_id = int(tool.Blender.get_enum_safe(self, "unit_id_enum") or "0")
if ifc_file := tool.Ifc.get():
# Must run before self.unit_id is overwritten: convert_attribute_unit needs the OLD
# unit_id to know what unit the current value is expressed in.
tool.Pset.convert_attribute_unit(self, new_unit_id, ifc_file)
self.unit_id = new_unit_id
def update_schema_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
import bonsai.bim.schema
@@ -250,44 +302,33 @@ def set_numerical_value(self: "Attribute", value_name: str, new_value: Union[flo
self[value_name] = new_value
def get_length_value(self: "Attribute") -> float:
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
return self.float_value * si_conversion
def set_length_value(self: "Attribute", value: float) -> None:
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.float_value = value / si_conversion
def get_display_name(self: "Attribute") -> str:
DISPLAY_UNIT_TYPES = ("AREA", "VOLUME", "FORCE")
name = self.name
if not self.special_type or self.special_type not in DISPLAY_UNIT_TYPES:
if not self.unit_symbol:
return name
return f"{name}, {self.unit_symbol}"
unit_type = f"{self.special_type}UNIT"
project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), unit_type)
if not project_unit:
return name
unit_symbol = ifcopenshell.util.unit.get_unit_symbol(project_unit)
return f"{name}, {unit_symbol}"
def get_unit_symbol(self: "Attribute") -> str:
"""The symbol for whatever unit the value is currently expressed in: this property's own
override (`unit_id`) if set, else the project default for `special_type`. Computed fresh on
every access (rather than cached at import time) so it stays correct immediately after the
unit picker changes `unit_id`, and after the project's own default units are edited.
"""
if not tool.Pset.is_measurable_special_type(self.special_type):
return ""
if not (ifc_file := tool.Ifc.get()):
return ""
unit = tool.Pset.resolve_effective_unit(self.special_type, self.unit_id, ifc_file)
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
AttributeDataType = Literal["string", "integer", "float", "boolean", "enum", "file", "list[string]"]
AttributeSpecialType = Literal[
"",
"DATE",
"DATETIME",
"LENGTH",
"AREA",
"VOLUME",
"FORCE",
"LOGICAL",
"URI",
"DURATION",
]
# Either "", "DATE", "DATETIME", "LOGICAL", "URI", "DURATION", or an
# IfcUnitEnum/IfcDerivedUnitEnum value with the "UNIT" suffix stripped (e.g.
# "LENGTH", "PRESSURE", "MODULUSOFELASTICITY") as returned by
# tool.Pset.get_special_type_for_prop().
AttributeSpecialType = str
class Attribute(PropertyGroup):
@@ -318,9 +359,6 @@ class Attribute(PropertyGroup):
get=lambda self: float(self.get("float_value", 0.0)),
set=set_float_value,
)
length_value: FloatProperty(
name="Value", description=tooltip, get=get_length_value, set=set_length_value, unit="LENGTH"
)
enum_items: StringProperty(name="Value")
"""Json serialized mapping of enum items:
Typically a dictionary of string identifiers to item names.
@@ -342,6 +380,10 @@ class Attribute(PropertyGroup):
value_max: FloatProperty(description="This is used to validate int_value and float_value")
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
special_type: StringProperty(name="Special Value Type", default="")
unit_symbol: StringProperty(name="Unit Symbol", get=get_unit_symbol)
unit_id: IntProperty(name="Unit Override", default=0)
"""STEP id of this property/quantity's own Unit override. 0 means "use the project default"."""
unit_id_enum: EnumProperty(items=get_attribute_unit_enum_items, name="Unit", update=update_attribute_unit_id)
use_explorer_ui: BoolProperty()
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
update: StringProperty(name="Update", description="Custom update function to be executed")
@@ -357,7 +399,6 @@ class Attribute(PropertyGroup):
bool_value: bool
int_value: int
float_value: float
length_value: float
enum_items: str
enum_items_dynamic: str
enum_descriptions: bpy.types.bpy_prop_collection_idprop[StrProperty]
@@ -373,6 +414,9 @@ class Attribute(PropertyGroup):
value_min_constraint: bool
value_max: float
value_max_constraint: bool
unit_symbol: str
unit_id: int
unit_id_enum: str
use_explorer_ui: bool
metadata: str
update: str
@@ -430,8 +474,6 @@ class Attribute(PropertyGroup):
elif data_type == "integer":
return "int_value"
elif data_type == "float":
if display_only and self.special_type == "LENGTH":
return "length_value"
return "float_value"
elif data_type == "enum":
return "enum_value"
+6
View File
@@ -620,6 +620,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
default="PROMPT",
)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_always_cache: BoolProperty(
name="Always Cache Geometry",
description="Whether to always cache geometry regardless of 'Cache' setting during Advanced Project Load.",
)
occurrence_name_style: bpy.props.EnumProperty(
items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")],
name="Occurrence Name Style",
@@ -731,6 +735,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
autosave_interval_minutes: int
autosave_mode: Literal["PROMPT", "BACKUP"]
should_stream: bool
should_always_cache: bool
occurrence_name_style: Literal["CLASS", "TYPE", "CUSTOM"]
occurrence_name_function: str
gizmos: GizmoPreferences
@@ -884,6 +889,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "autosave_interval_minutes")
layout.prop(self, "autosave_mode")
layout.prop(self, "should_stream")
layout.prop(self, "should_always_cache")
layout.label(text="bSDD:")
layout.prop(self, "bsdd_load_preview_dictionaries")
layout.prop(self, "bsdd_load_inactive_dictionaries")
+3 -3
View File
@@ -76,9 +76,9 @@ def add_instance_ceiling_covering_from_cursor(
ceiling_height = None
if selected_objects and active_obj:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
x, y, z, _, _ = spatial.get_x_y_z_h_mat_from_obj(active_obj)
else:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
x, y, z, _, _ = spatial.get_x_y_z_h_mat_from_cursor()
ceiling_height = covering.get_z_from_ceiling_height()
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
@@ -102,7 +102,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
selected_objects = spatial.get_selected_objects()
if selected_objects and active_obj:
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
x, y, _, _, _ = spatial.get_x_y_z_h_mat_from_obj(active_obj)
else:
assert False, "Object has to be active and selected."
+4
View File
@@ -30,6 +30,10 @@ def parse_express(debug: type[tool.Debug], filename: str) -> None:
debug.add_schema_identifier(debug.load_express(filename))
def purge_hdf5_cache(debug: type[tool.Debug]) -> None:
debug.purge_hdf5_cache()
def purge_unused_elements(ifc: type[tool.Ifc], debug: type[tool.Debug], ifc_class: str) -> int:
ifc_file = ifc.get()
unused_elements = [i for i in ifc_file.by_type(ifc_class) if ifc_file.get_total_inverses(i) == 0]
+1 -1
View File
@@ -526,9 +526,9 @@ def add_annotation(
context = drawing_tool.create_annotation_context(target_view, object_type)
drawing_tool.show_decorations()
relating_type_rep = None
obj = drawing_tool.create_annotation_object(drawing, object_type)
element = ifc.get_entity(obj)
relating_type_rep = None
if not element: # Brand new annotation
relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None
element = drawing_tool.run_root_assign_class(
+2
View File
@@ -44,6 +44,7 @@ def edit_object_placement(
element = ifc.get_entity(obj)
if not element:
return
geometry.clear_cache(element)
if apply_scale:
geometry.clear_scale(obj)
geometry.get_blender_offset_type(obj)
@@ -124,6 +125,7 @@ def switch_representation(
element = ifc.get_entity(obj)
assert element
geometry.clear_cache(element)
geometry.reimport_element_representations(obj, representation, apply_openings=apply_openings)
+3
View File
@@ -21,6 +21,7 @@ import inspect
from typing import Optional
# fmt: off
# pylint: skip-file
# This interface class and decorator is magic syntactic sugar to allow concise interface definitions
# If we didn't do this, Python is unnecessarily verbose, which I find distracting. Don't black this file :)
@@ -289,6 +290,7 @@ class Cost:
class Debug:
def add_schema_identifier(cls, schema): pass
def load_express(cls, filename): pass
def purge_hdf5_cache(cls): pass
@interface
@@ -444,6 +446,7 @@ class Feature:
@interface
class Geometry:
def change_object_data(cls, obj, data, is_global=False): pass
def clear_cache(cls, element): pass
def clear_modifiers(cls, obj): pass
def clear_scale(cls, obj): pass
def copy_data_links(cls, data, copied_entities) -> None: pass
-59
View File
@@ -2581,65 +2581,6 @@ class Blender(bonsai.core.tool.Blender):
t += dash_period
return new_verts, new_edges
@classmethod
def draw_bmesh_face_tris(
cls,
bm: bmesh.types.BMesh,
world_vert_coords: list,
color: Any,
draw_batch: Callable[[str, list, Any, list], None],
) -> None:
"""Submit a non-mutating beauty-triangulated TRIS batch for ``bm``'s faces.
``world_vert_coords`` must be indexed by ``bm.verts`` index. Never call
``bmesh.ops.triangulate`` on a live bmesh to compute draw indices it
mutates the input and produces ear-clip fans that render as visible
streaks at low alpha.
"""
tris = [[loop.vert.index for loop in tri] for tri in bm.calc_loop_triangles()]
draw_batch("TRIS", world_vert_coords, color, tris)
@classmethod
def build_dashed_line_segments(
cls,
world_verts: Sequence[Sequence[float]],
edges_indices: Sequence[Sequence[int]],
dash_period: float,
dash_width: float,
) -> tuple[list[tuple[float, float, float]], list[tuple[int, int]]]:
"""Pre-segment edges into world-space dash chunks for a vanilla LINES batch.
Each input edge is sliced into segments of length ``dash_width`` spaced
``dash_period`` apart (dash phase resets per-edge). The result is a fresh
``(verts, edges)`` pair that draws as dashes through any standard line
shader letting both passes of a visible/occluded outline reuse the
same shader so depth values match exactly across passes.
"""
new_verts: list[tuple[float, float, float]] = []
new_edges: list[tuple[int, int]] = []
if dash_period <= 0 or dash_width <= 0:
return new_verts, new_edges
n = len(world_verts)
for i, j in edges_indices:
if not (0 <= i < n and 0 <= j < n) or i == j:
continue
v0 = world_verts[i]
v1 = world_verts[j]
dx, dy, dz = v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]
edge_length = math.sqrt(dx * dx + dy * dy + dz * dz)
if edge_length == 0.0:
continue
ux, uy, uz = dx / edge_length, dy / edge_length, dz / edge_length
t = 0.0
while t < edge_length:
t_end = min(t + dash_width, edge_length)
idx = len(new_verts)
new_verts.append((v0[0] + ux * t, v0[1] + uy * t, v0[2] + uz * t))
new_verts.append((v0[0] + ux * t_end, v0[1] + uy * t_end, v0[2] + uz * t_end))
new_edges.append((idx, idx + 1))
t += dash_period
return new_verts, new_edges
@classmethod
def extract_error_reports(cls, exception: RuntimeError) -> list[str]:
"""Extracts error report lines from a runtime exception during operator execution.
+1 -1
View File
@@ -1085,8 +1085,8 @@ class Cad:
break
v1 = v2
assert new_verts is not None
return new_verts
@classmethod
+13 -1
View File
@@ -19,6 +19,7 @@
from __future__ import annotations
import json
import os
import re
from collections import defaultdict
from collections.abc import Iterable
@@ -58,6 +59,17 @@ class Debug(bonsai.core.tool.Debug):
ifcopenshell.register_schema(schema)
return schema.schema
@classmethod
def purge_hdf5_cache(cls) -> None:
prefs = tool.Blender.get_addon_preferences()
cache_dir = prefs.cache_dir
filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")]
for f in filelist:
try:
os.remove(os.path.join(cache_dir, f))
except PermissionError:
pass
@classmethod
def debug_bmesh(cls, bm: bmesh.types.BMesh, name: str = "Debug") -> bpy.types.Object:
mesh = bpy.data.meshes.new("Debug")
@@ -152,7 +164,7 @@ class Debug(bonsai.core.tool.Debug):
return name
def get_hash(element: ifcopenshell.entity_instance) -> int:
data = element.get_info(include_identifier=False, recursive=True)
data = element.get_info_2(include_identifier=False, recursive=True)
if object_type == "APPLICATION":
# To avoid disruption let user merge organizations separately.
data["ApplicationDeveloper"] = element.ApplicationDeveloper.id()
+1 -4
View File
@@ -2609,12 +2609,10 @@ class Drawing(bonsai.core.tool.Drawing):
subcontext = current_representation.ContextOfItems
current_representation_subcontext = tool.Geometry.get_subcontext_parameters(subcontext)
has_context = False
for subcontext in subcontexts:
# prioritize already active representation if it matches the subcontext
# (element could have multiple representations in the same subcontext)
if current_representation and subcontext == current_representation_subcontext:
has_context = True
if current_representation_subcontext and subcontext == current_representation_subcontext:
break
priority_representation = ifcopenshell.util.representation.get_representation(element, *subcontext)
if priority_representation:
@@ -2624,7 +2622,6 @@ class Drawing(bonsai.core.tool.Drawing):
obj=obj,
representation=priority_representation,
)
has_context = True
break
linked_handles: set[bpy.types.Object] = set()
+1 -3
View File
@@ -23,7 +23,6 @@ from typing import TYPE_CHECKING
import bpy
import ifcopenshell.api.feature
import ifcopenshell.util.representation
import bonsai.core.geometry
import bonsai.core.tool
@@ -45,12 +44,12 @@ class Feature(bonsai.core.tool.Feature):
featured_element = tool.Ifc.get_entity(featured_obj)
has_visible_openings = False
element_had_openings = None
for opening in [r.RelatedOpeningElement for r in featured_element.HasOpenings]:
if tool.Ifc.get_object(opening):
has_visible_openings = True
break
element_had_openings = None
for feature_obj in feature_objs:
feature_element = tool.Ifc.get_entity(feature_obj)
@@ -59,7 +58,6 @@ class Feature(bonsai.core.tool.Feature):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=featured_obj)
element_had_openings = tool.Geometry.has_openings(featured_element)
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body")
ifcopenshell.api.feature.add_feature(tool.Ifc.get(), feature=feature_element, element=featured_element)
if tool.Ifc.is_moved(feature_obj):
+6 -15
View File
@@ -74,7 +74,7 @@ import bonsai.core.style
import bonsai.core.system
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.bim.ifc import get_cache_or_detect_lock
from bonsai.bim.ifc import IfcStore, get_cache_or_detect_lock
if TYPE_CHECKING:
from bonsai.bim.module.geometry.prop import (
@@ -111,18 +111,8 @@ class Geometry(bonsai.core.tool.Geometry):
old_data.user_remap(new_data)
@classmethod
def has_axis_representation(cls, element: ifcopenshell.entity_instance) -> bool:
"""True if the element carries a shape representation whose
RepresentationIdentifier is 'Axis'. Elements without one cannot be
projected to an unambiguous 1D path; callers that draw schematic axis
overlays must skip them rather than fall back to mesh-derived geometry."""
product_rep = getattr(element, "Representation", None)
if product_rep is None:
return False
for rep in product_rep.Representations:
if getattr(rep, "RepresentationIdentifier", None) == "Axis":
return True
return False
def get_cache(cls) -> Union[ifcopenshell.geom.serializers.hdf5, None]:
return IfcStore.get_cache()
@classmethod
def clear_cache(cls, element: ifcopenshell.entity_instance) -> None:
@@ -1110,6 +1100,7 @@ class Geometry(bonsai.core.tool.Geometry):
if not cls.has_data_users(old_data):
cls.delete_data(old_data)
cls.clear_modifiers(obj)
cls.clear_cache(element)
# Import swept disk solids as Blender curves if possible.
elements_without_openings = {e for e in elements if not getattr(e, "HasOpenings", False)}
@@ -1916,11 +1907,11 @@ class Geometry(bonsai.core.tool.Geometry):
previous_shape_aspect = inverse.OfShapeAspect[0]
else:
base_representation = inverse
assert base_representation
# remove item from previous shape aspect
if previous_shape_aspect:
cls.remove_representation_items_from_shape_aspect(representation_items, previous_shape_aspect)
assert base_representation
shape_aspect_representation = cls.get_shape_aspect_representation(
shape_aspect, base_representation, create_new=True
)
@@ -2179,7 +2170,7 @@ class Geometry(bonsai.core.tool.Geometry):
item = tool.Ifc.get().by_id(props.ifc_definition_id)
allowed_attributes = [
a.name()
for a in item.declaration().as_entity.all_attributes()
for a in item.wrapped_data.declaration().as_entity().all_attributes()
if a.type_of_attribute()._is("IfcLengthMeasure")
]
+1 -3
View File
@@ -548,9 +548,7 @@ class IfcGit(bonsai.core.tool.IfcGit):
@classmethod
def config_push(cls, repo: git.Repo) -> None:
"""Set push.autoSetupRemote"""
# Repository level only - a global push.* setting must not stop us
# configuring this repo.
config_reader = repo.config_reader("repository")
config_reader = repo.config_reader()
if not config_reader.has_section("push"):
with repo.config_writer() as config_writer:
config_writer.set_value("push", "default", "current")
+5 -4
View File
@@ -1087,20 +1087,21 @@ class Loader(bonsai.core.tool.Loader):
bm = bmesh.new()
bm.from_mesh(mesh)
prev_co = None
if usage.LayerSetDirection == "AXIS2":
layer_set_direction = usage.LayerSetDirection
if layer_set_direction == "AXIS2":
co = Vector((0.0, offset, 0.0))
no = cls.get_extrusion_vector(element).normalized()
no = no.cross(Vector([1.0, 0.0, 0.0]))
elif usage.LayerSetDirection == "AXIS3":
elif layer_set_direction == "AXIS3":
co = Vector((0.0, 0.0, offset))
no = cls.get_extrusion_vector(element).normalized()
no = Vector([0.0, 0.0, 1.0])
elif usage.LayerSetDirection == "AXIS1":
elif layer_set_direction == "AXIS1":
co = Vector((0.0, 0.0, offset))
no = cls.get_extrusion_vector(element).normalized()
no = Vector([1.0, 0.0, 0.0])
else:
assert False, usage.LayerSetDirection
assert False, layer_set_direction
no *= sense_factor
# Cache this
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
+6 -5
View File
@@ -370,20 +370,21 @@ class Project(bonsai.core.tool.Project):
props = cls.get_project_props()
active_library_breadcrumb = props.get_active_library_breadcrumb()
change_back = False
name = breadcrumb_type = library_id = None
breadcrumb = None
if active_library_breadcrumb:
name = active_library_breadcrumb.name
breadcrumb_type = active_library_breadcrumb.breadcrumb_type
library_id = active_library_breadcrumb.library_id
breadcrumb = (name, breadcrumb_type, library_id)
change_back = True
bpy.ops.bim.rewind_library()
if change_back:
assert name is not None and breadcrumb_type is not None and library_id is not None
assert breadcrumb
bpy.ops.bim.change_library_element(
element_name=name,
breadcrumb_type=breadcrumb_type,
library_id=library_id,
element_name=breadcrumb[0],
breadcrumb_type=breadcrumb[1],
library_id=breadcrumb[2],
)
@classmethod
+147 -33
View File
@@ -26,6 +26,7 @@ import ifcopenshell
import ifcopenshell.api.pset
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import ifcopenshell.util.unit
import bonsai.bim.helper
import bonsai.bim.schema
@@ -168,42 +169,144 @@ class Pset(bonsai.core.tool.Pset):
pset_id=0, pset_name=cls.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
)
# Templates for quantities can specify their kind via TemplateType (e.g.
# "Q_LENGTH") instead of PrimaryMeasureType. IfcQuantityCount has no
# associated measure/unit, so it is intentionally absent here.
QUANTITY_TEMPLATE_TYPE_TO_SPECIAL_TYPE = {
"Q_LENGTH": "LENGTH",
"Q_AREA": "AREA",
"Q_VOLUME": "VOLUME",
"Q_WEIGHT": "MASS",
"Q_TIME": "TIME",
}
@classmethod
def get_special_type_for_prop(
cls, prop_or_prop_template: ifcopenshell.entity_instance
) -> Literal["LENGTH"] | Literal["AREA"] | Literal["VOLUME"] | Literal["URI"] | Literal[""]:
special_type = ""
def get_special_type_for_measure_class(cls, measure_class: str) -> str:
"""Get the ``special_type`` (an IfcUnitEnum value with "UNIT" stripped) for an IFC measure class.
:param measure_class: An IFC measure class name, e.g. "IfcLengthMeasure".
:return: E.g. "LENGTH", or "" if the class has no associated unit type.
"""
if not measure_class.endswith("Measure"):
return ""
unit_type = ifcopenshell.util.unit.get_measure_unit_type(measure_class)
return unit_type[: -len("UNIT")] if unit_type.endswith("UNIT") else ""
@classmethod
def get_special_type_for_unit(cls, unit: ifcopenshell.entity_instance) -> str:
"""Get the ``special_type`` (an IfcUnitEnum value with "UNIT" stripped) directly from
a Unit entity, for properties whose NominalValue is a generic numeric type (e.g.
IfcReal) rather than a proper measure class, but which still carry a real Unit.
"""
unit_type = getattr(unit, "UnitType", None)
if unit_type and unit_type != "USERDEFINED":
return unit_type[: -len("UNIT")] if unit_type.endswith("UNIT") else ""
dimension_type = ifcopenshell.util.unit.identify_unit_dimensions(unit)
return dimension_type[: -len("UNIT")] if dimension_type else ""
@classmethod
def get_special_type_for_prop(cls, prop_or_prop_template: ifcopenshell.entity_instance) -> str:
"""Classify a property/quantity/template by its measure type.
:return: An IfcUnitEnum value with the "UNIT" suffix stripped (e.g.
"LENGTH", "PRESSURE"), "URI" for IfcURIReference, or "" if the
value has no associated unit type.
"""
if prop_or_prop_template.is_a("IfcPropertyTemplate"):
primary_measure_type = prop_or_prop_template.PrimaryMeasureType
template_type = prop_or_prop_template.TemplateType
if primary_measure_type in ("IfcPositiveLengthMeasure", "IfcLengthMeasure") or template_type == "Q_LENGTH":
special_type = "LENGTH"
elif primary_measure_type == "IfcAreaMeasure" or template_type == "Q_AREA":
special_type = "AREA"
elif primary_measure_type == "IfcVolumeMeasure" or template_type == "Q_VOLUME":
special_type = "VOLUME"
elif primary_measure_type == "IfcURIReference":
special_type = "URI"
else:
if prop_or_prop_template.is_a("IfcPropertySingleValue"):
value = prop_or_prop_template.NominalValue
if value is not None:
value_type = value.is_a()
if value_type in ("IfcLengthMeasure", "IfcPositiveLengthMeasure"):
special_type = "LENGTH"
elif value_type == "IfcAreaMeasure":
special_type = "AREA"
elif value_type == "IfcVolumeMeasure":
special_type = "VOLUME"
elif prop_or_prop_template.is_a("IfcPhysicalSimpleQuantity"):
prop_class = prop_or_prop_template.is_a()
if prop_class == "IfcQuantityArea":
special_type = "AREA"
elif prop_class == "IfcQuantityVolume":
special_type = "VOLUME"
elif prop_class == "IfcQuantityLength":
special_type = "LENGTH"
return special_type
if primary_measure_type == "IfcURIReference":
return "URI"
if primary_measure_type:
return cls.get_special_type_for_measure_class(primary_measure_type)
return cls.QUANTITY_TEMPLATE_TYPE_TO_SPECIAL_TYPE.get(prop_or_prop_template.TemplateType, "")
elif prop_or_prop_template.is_a("IfcPropertySingleValue"):
value = prop_or_prop_template.NominalValue
if value is not None:
special_type = cls.get_special_type_for_measure_class(value.is_a())
if special_type:
return special_type
# Some property sets declare a generic numeric type (e.g. IfcReal) rather
# than a proper measure class, relying on an explicit Unit attribute alone to
# convey the dimension. Still measurable -- derive special_type from the Unit
# itself rather than (fruitlessly) from NominalValue's declared type.
if value.is_a() in ("IfcReal", "IfcInteger"):
if unit := getattr(prop_or_prop_template, "Unit", None):
return cls.get_special_type_for_unit(unit)
elif prop_or_prop_template.is_a("IfcPhysicalSimpleQuantity"):
entity = prop_or_prop_template.wrapped_data.declaration().as_entity()
measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name()
return cls.get_special_type_for_measure_class(measure_class)
return ""
@classmethod
def get_unit_symbol_for_special_type(cls, special_type: str, ifc_file: ifcopenshell.file) -> str:
"""Get the project's default unit symbol for a `special_type` (see `get_special_type_for_prop`).
Used where there's no property instance to check for a `Unit` override
(e.g. a template, or a native IFC entity attribute, neither of which
can carry one).
"""
if not special_type or special_type == "URI":
return ""
unit = ifcopenshell.util.unit.get_project_unit(ifc_file, f"{special_type}UNIT")
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
@classmethod
def get_unit_symbol_for_prop(cls, prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file) -> str:
"""Get the unit symbol for an existing property/quantity, respecting its own `Unit` override.
Gated on the property being classified as measurable (see `get_special_type_for_prop`,
which already accounts for a Unit attached to a generic numeric value) -- this only
excludes a Unit attached to a property whose value has no numeric/measure semantics at
all (e.g. text), where a stray Unit shouldn't be surfaced as a resolved unit.
"""
if not cls.is_measurable_special_type(cls.get_special_type_for_prop(prop)):
return ""
unit = ifcopenshell.util.unit.get_property_unit(prop, ifc_file)
return ifcopenshell.util.unit.get_unit_symbol(unit) if unit else ""
# special_type values that don't denote a real unit-bearing measure (see get_special_type_for_prop).
NON_MEASURABLE_SPECIAL_TYPES = frozenset({"", "DATE", "DATETIME", "LOGICAL", "URI", "DURATION"})
@classmethod
def is_measurable_special_type(cls, special_type: str) -> bool:
"""True if `special_type` (see `get_special_type_for_prop`) denotes a real unit-bearing measure."""
return special_type not in cls.NON_MEASURABLE_SPECIAL_TYPES
@classmethod
def get_candidate_units_for_special_type(
cls, special_type: str, ifc_file: ifcopenshell.file
) -> list[ifcopenshell.entity_instance]:
"""All units in the file usable as an override for a `special_type` (see `get_special_type_for_prop`)."""
if not cls.is_measurable_special_type(special_type):
return []
return ifcopenshell.util.unit.get_candidate_units(ifc_file, f"{special_type}UNIT")
@classmethod
def resolve_effective_unit(
cls, special_type: str, unit_id: int, ifc_file: ifcopenshell.file
) -> Union[ifcopenshell.entity_instance, None]:
"""The unit a value is currently expressed in: its own override (`unit_id`, a STEP id,
0 meaning "no override"), or the project default for `special_type` otherwise."""
if unit_id:
return ifc_file.by_id(unit_id)
return ifcopenshell.util.unit.get_project_unit(ifc_file, f"{special_type}UNIT")
@classmethod
def convert_attribute_unit(cls, metadata: "Attribute", new_unit_id: int, ifc_file: ifcopenshell.file) -> None:
"""Rescale `metadata.float_value` in place so its physical quantity is preserved when
switching from its current effective unit to the unit named by `new_unit_id` (0 = project
default). No-op for non-measurable attributes or when old and new resolve to the same unit.
"""
if not cls.is_measurable_special_type(metadata.special_type):
return
old_unit = cls.resolve_effective_unit(metadata.special_type, metadata.unit_id, ifc_file)
new_unit = cls.resolve_effective_unit(metadata.special_type, new_unit_id, ifc_file)
if old_unit is None or new_unit is None or old_unit == new_unit:
return
old_scale = ifcopenshell.util.unit.get_unit_scale(old_unit)
new_scale = ifcopenshell.util.unit.get_unit_scale(new_unit)
metadata.float_value = metadata.float_value * old_scale / new_scale
@classmethod
def import_pset_from_existing(
@@ -283,6 +386,15 @@ class Pset(bonsai.core.tool.Pset):
metadata.is_null = value is None
metadata.is_optional = True
metadata.special_type = cls.get_special_type_for_prop(prop)
# The prop's OWN Unit override only -- metadata.unit_symbol is computed fresh from
# special_type/unit_id on every access (see Attribute.get_unit_symbol), so it
# already accounts for the project-default fallback once unit_id is set below.
# Some real-world files (e.g. certain exporters) set Unit on properties that
# aren't actually measures -- ignore it there, since we only ever treat Unit as
# meaningful for measurable special_types (matching the UI picker's own gating).
own_unit = getattr(prop, "Unit", None) if cls.is_measurable_special_type(metadata.special_type) else None
metadata.unit_id = own_unit.id() if own_unit else 0
metadata.unit_id_enum = str(metadata.unit_id)
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
process_prop_description(metadata)
@@ -409,6 +521,8 @@ class Pset(bonsai.core.tool.Pset):
cls.import_single_value_from_template(pset_template, prop_template, simplified_data, props)
elif prop_template.TemplateType.startswith("Q_"):
if prop_data:
continue # Existing quantity will be added later by import_pset_from_existing.
cls.import_single_value_from_template(pset_template, prop_template, simplified_data, props)
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
+13
View File
@@ -176,6 +176,19 @@ class Qto(bonsai.core.tool.Qto):
is_ifc4x3 = ifc_file.schema == "IFC4X3"
return {rule_id: rule for rule_id, rule in ifc5d.qto.rules.items() if rule_id.startswith("IFC4X3") == is_ifc4x3}
@classmethod
def get_target_units(cls) -> dict[str, ifcopenshell.entity_instance]:
from bonsai.bim.module.qto.prop import MEASURE_TO_TARGET_UNIT_FIELD
props = cls.get_qto_props()
ifc_file = tool.Ifc.get()
target_units: dict[str, ifcopenshell.entity_instance] = {}
for measure_class, (field_name, _special_type, _label) in MEASURE_TO_TARGET_UNIT_FIELD.items():
unit_id = int(tool.Blender.get_enum_safe(props, field_name) or "0")
if unit_id:
target_units[measure_class] = ifc_file.by_id(unit_id)
return target_units
@classmethod
def get_not_quantified_elements_message(cls, not_quantified_elements: set[ifcopenshell.entity_instance]) -> str:
not_quantified_message = ""

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