mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-29 08:13:14 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e3d4486ca | |||
| 89523999b3 | |||
| 51ab38de27 | |||
| fbe36532a0 | |||
| 2f1b2f9638 | |||
| 9001cca078 | |||
| ec558dc57e | |||
| 2db5658386 | |||
| 88c8bd032f | |||
| 3d8654acfd | |||
| 4a20b67038 | |||
| b14df627d7 | |||
| 0828c6ba92 | |||
| a586c7f695 | |||
| 0bad5a9389 | |||
| 0ce400cace | |||
| 91ed59311b | |||
| 1906481a01 | |||
| 73bf238232 | |||
| a85d5cc990 | |||
| c16aec2cb0 | |||
| 76be31561e | |||
| a111c68d44 | |||
| 62f627ecc6 | |||
| 279e16f2ab | |||
| 3d68d0f0e5 | |||
| e759135608 | |||
| 1df738d968 | |||
| 98c28a1f30 | |||
| 21122c0d28 |
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
|
||||
@@ -9,13 +9,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
path: IfcOpenShell
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ifcopenshell_build
|
||||
|
||||
@@ -35,12 +35,12 @@ jobs:
|
||||
aws --version
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
|
||||
@@ -35,12 +35,12 @@ jobs:
|
||||
aws --version
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ./build
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ${{ matrix.deps_dir }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6 # https://github.com/actions/checkout
|
||||
- uses: actions/checkout@v7 # https://github.com/actions/checkout
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout bonsai_unstable_repo repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/bonsai_unstable_repo
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
config:
|
||||
short_name: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
date: ${{ steps.date.outputs.date }}
|
||||
verdate: ${{ steps.verdate.outputs.verdate }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set env
|
||||
run: echo ok go
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
echo "ARTIFACTS_DIR=/home/runner/work/artifacts" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: activate
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
name: Docker Build, Tag, Push
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
short_name: macosm164
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
@@ -10,9 +10,9 @@ jobs:
|
||||
publish_website:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Checkout ifctester_org_static_html
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/ifctester_org_static_html
|
||||
token: ${{ secrets.IFCOPENBOT_TOKEN }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
|
||||
with:
|
||||
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
MIN_BLENDER_PY_VERSION: "3.11"
|
||||
steps:
|
||||
- name: Action - checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Action - install python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout IfcOpenShell
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
echo "name=$(basename $WHEEL)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout wasm-wheels
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/wasm-wheels
|
||||
path: wasm-wheels
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
CLICOLOR_FORCE: "1"
|
||||
CMAKE_COLOR_DIAGNOSTICS: "ON"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
cd ../ifcdiff && make test || ERROR=1
|
||||
cd ../ifcpatch && make test || ERROR=1
|
||||
pip install -e ../ifc5d --no-deps
|
||||
pip install odfpy xlsxwriter
|
||||
pip install odfpy openpyxl
|
||||
cd ../ifc5d && make test || ERROR=1
|
||||
pip install -e ../ifcquery --no-deps
|
||||
cd ../ifcquery && make test || ERROR=1
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (recursive)
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Checkout intermediate Pages repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/aichat_ifcopenshell_org_static_html
|
||||
ref: gh-pages
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (recursive)
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Checkout intermediate Pages repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: IfcOpenShell/wasm_ifcopenshell_org_static_html
|
||||
ref: gh-pages
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install C++ dependencies
|
||||
|
||||
+5
-2
@@ -121,7 +121,7 @@ import tarfile
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
ssl._create_default_https_context = ssl._create_unverified_context # ty:ignore[invalid-assignment]
|
||||
|
||||
import time
|
||||
from collections.abc import Generator, Sequence
|
||||
@@ -697,7 +697,10 @@ def build_dependency(
|
||||
compr = "xz"
|
||||
else:
|
||||
raise RuntimeError("fix source for new download type")
|
||||
download_tarfile = tarfile.open(name=download_tarfile_path, mode=f"r:{compr}")
|
||||
# ty: false positive bug upstream.
|
||||
download_tarfile = tarfile.open(
|
||||
name=download_tarfile_path, mode=f"r:{compr}"
|
||||
) # ty:ignore[no-matching-overload]
|
||||
# tarfile seriously doesn't have a function to retrieve the root directory more easily
|
||||
extract_dir_name = os.path.commonprefix([x for x in download_tarfile.getnames() if x != "."])
|
||||
# run([tar, "--exclude=\"*/*\"", "-tf", download_name], cwd=build_dir).strip() no longer works
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
black==26.3.1
|
||||
ruff==0.15.22
|
||||
ruff==0.16.0
|
||||
poethepoet
|
||||
ty==0.0.61
|
||||
gersemi==0.26.1
|
||||
ty==0.0.63
|
||||
gersemi==0.28.0
|
||||
|
||||
@@ -10,6 +10,7 @@ if bonsai_lib_path:
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pystache
|
||||
@@ -18,8 +19,53 @@ from aiohttp import web
|
||||
|
||||
sio_port = 8080 # default port
|
||||
|
||||
|
||||
def get_asset_version() -> str:
|
||||
"""A cache-busting token appended to locally served static asset URLs.
|
||||
|
||||
Browsers otherwise keep serving a stale cached copy of static/js and
|
||||
static/css after Bonsai ships a code change, until the user does a hard
|
||||
refresh. Using the Bonsai version (which includes the build's commit
|
||||
hash) means the token changes on every shipped update.
|
||||
"""
|
||||
if bonsai_version:
|
||||
return urllib.parse.quote(bonsai_version, safe="")
|
||||
# Fallback for standalone runs without BONSAI_VERSION set (e.g. running
|
||||
# sioserver.py directly outside of Blender): derive a token from the
|
||||
# newest mtime among the static assets, so it still changes whenever the
|
||||
# shipped files change.
|
||||
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
||||
latest_mtime = 0
|
||||
for root, _dirs, files in os.walk(static_dir):
|
||||
for name in files:
|
||||
latest_mtime = max(latest_mtime, int(os.path.getmtime(os.path.join(root, name))))
|
||||
return f"dev-{latest_mtime}"
|
||||
|
||||
|
||||
asset_version = get_asset_version()
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def no_cache_static_middleware(request: web.Request, handler):
|
||||
"""Force revalidation of locally served static assets.
|
||||
|
||||
Query-string version stamping (see `asset_version`) busts the cache for
|
||||
the HTML-referenced entry points, but JS files that statically import
|
||||
other local modules (e.g. cost.js/gantt.js importing utilities/costui.js)
|
||||
reference those modules by an un-stamped relative path. Marking all
|
||||
/static/ and /jsgantt/ responses as no-cache makes browsers always
|
||||
revalidate with the server (a cheap conditional GET / 304 when nothing
|
||||
changed), so nested imports also pick up shipped changes without
|
||||
requiring a hard refresh.
|
||||
"""
|
||||
response = await handler(request)
|
||||
if request.path.startswith("/static/") or request.path.startswith("/jsgantt/"):
|
||||
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||
return response
|
||||
|
||||
|
||||
sio = socketio.AsyncServer(cors_allowed_origins="*", async_mode="aiohttp", max_http_buffer_size=10000000)
|
||||
app = web.Application()
|
||||
app = web.Application(middlewares=[no_cache_static_middleware])
|
||||
sio.attach(app)
|
||||
|
||||
|
||||
@@ -199,28 +245,28 @@ class BlenderNamespace(socketio.AsyncNamespace):
|
||||
async def schedules(request):
|
||||
with open("templates/index.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
async def costing(request):
|
||||
with open("templates/costing.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
async def sequencing(request):
|
||||
with open("templates/gantt.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
async def documentation(request):
|
||||
with open("templates/drawings.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
@@ -229,7 +275,7 @@ async def documentation(request):
|
||||
async def demo(request):
|
||||
with open("templates/demo.html", "r") as f:
|
||||
template = f.read()
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||
return web.Response(text=html_content, content_type="text/html")
|
||||
|
||||
|
||||
|
||||
@@ -250,6 +250,14 @@ export class CostUI {
|
||||
},
|
||||
});
|
||||
|
||||
CostUI.addRibbonButton({
|
||||
text: "Download CSV",
|
||||
icon: "fa-solid fa-file-csv",
|
||||
callback: () => {
|
||||
CostUI.downloadCsv();
|
||||
},
|
||||
});
|
||||
|
||||
CostUI.addRibbonButton({
|
||||
text: "Hide Schedules",
|
||||
icon: "fa-regular fa-eye-slash",
|
||||
@@ -523,6 +531,81 @@ export class CostUI {
|
||||
}
|
||||
}
|
||||
|
||||
static downloadCsv() {
|
||||
const tables = document.querySelectorAll("table[id^='cost-items-']");
|
||||
if (tables.length === 0) {
|
||||
alert("No cost schedule loaded to export!");
|
||||
return;
|
||||
}
|
||||
tables.forEach((table) => {
|
||||
const scheduleId = table.id.split("-").pop();
|
||||
const csv = CostUI.tableToCsv(table);
|
||||
if (csv === null) {
|
||||
return;
|
||||
}
|
||||
const nameEl = document.querySelector(
|
||||
"#cost-schedule-container-" + scheduleId + " .form-header span"
|
||||
);
|
||||
const scheduleName = nameEl
|
||||
? nameEl.textContent
|
||||
: "cost_schedule_" + scheduleId;
|
||||
CostUI.triggerCsvDownload(csv, scheduleName + ".csv");
|
||||
});
|
||||
}
|
||||
|
||||
static tableToCsv(table) {
|
||||
const escapeCsvCell = (value) => {
|
||||
const text = (value === null || value === undefined ? "" : value)
|
||||
.toString()
|
||||
.trim();
|
||||
if (/[",\n]/.test(text)) {
|
||||
return '"' + text.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const cellText = (cell) => {
|
||||
const input = cell.querySelector("input");
|
||||
return input ? input.value : cell.innerText;
|
||||
};
|
||||
|
||||
// The Actions column only holds buttons (edit/delete/etc), not data.
|
||||
const isDataColumn = (column) => column && column !== "Actions";
|
||||
|
||||
const headerCells = Array.from(table.querySelectorAll("thead th")).filter(
|
||||
(th) => isDataColumn(th.getAttribute("data-column"))
|
||||
);
|
||||
if (headerCells.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rows = [headerCells.map((th) => escapeCsvCell(th.textContent)).join(",")];
|
||||
|
||||
table.querySelectorAll("tbody tr").forEach((row) => {
|
||||
const cells = Array.from(row.children).filter((cell) =>
|
||||
isDataColumn(cell.getAttribute("data-column"))
|
||||
);
|
||||
if (cells.length === 0) {
|
||||
return; // e.g. the "No cost items found" placeholder row.
|
||||
}
|
||||
rows.push(cells.map((cell) => escapeCsvCell(cellText(cell))).join(","));
|
||||
});
|
||||
|
||||
return rows.join("\n");
|
||||
}
|
||||
|
||||
static triggerCsvDownload(csvContent, filename) {
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
static createCostTable({ costSchedule, currency, callbacks }) {
|
||||
const preferences = CostUI.getColumnPreferences();
|
||||
const isScheduleOfRates = costSchedule.PredefinedType === "SCHEDULEOFRATES";
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/static/css/cost.css"
|
||||
href="/static/css/cost.css?v={{v}}"
|
||||
id="index-stylesheet"
|
||||
/>
|
||||
<link
|
||||
@@ -21,7 +21,7 @@
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
@@ -34,7 +34,7 @@
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script type="module" defer src="./static/js/cost.js"></script>
|
||||
<script type="module" defer src="./static/js/cost.js?v={{v}}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
/>
|
||||
<!-- here we request the CSS file from the server, -->
|
||||
<!-- using registered static path in the server -->
|
||||
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/demo.css?v={{v}}" id="demo-stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
@@ -33,7 +33,7 @@
|
||||
</script>
|
||||
<!-- here we request the JS file from the server-->
|
||||
<!-- using registered static path in the server -->
|
||||
<script defer src="./static/js/demo.js"></script>
|
||||
<script defer src="./static/js/demo.js?v={{v}}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- the navigation bar at the top of the page. -->
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/css/drawings.css"
|
||||
href="/static/css/drawings.css?v={{v}}"
|
||||
id="drawings-stylesheet"
|
||||
/>
|
||||
<link
|
||||
@@ -24,7 +24,7 @@
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
@@ -45,7 +45,7 @@
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script defer src="./static/js/drawings.js"></script>
|
||||
<script defer src="./static/js/drawings.js?v={{v}}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
|
||||
@@ -9,25 +9,25 @@
|
||||
type="image/x-icon"
|
||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||
/>
|
||||
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
|
||||
<link rel="stylesheet" href="/static/css/gantt.css" id="gantt-stylesheet" />
|
||||
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css?v={{v}}" />
|
||||
<link rel="stylesheet" href="/static/css/gantt.css?v={{v}}" id="gantt-stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
|
||||
></script>
|
||||
<script type="text/javascript" src="./jsgantt/jsgantt.js"></script>
|
||||
<script type="text/javascript" src="./jsgantt/jsgantt.js?v={{v}}"></script>
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script type="module" defer src="./static/js/gantt.js"></script>
|
||||
<script type="module" defer src="./static/js/gantt.js?v={{v}}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="no-print">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
type="image/x-icon"
|
||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||
/>
|
||||
<link rel="stylesheet" href="/static/css/index.css" id="index-stylesheet" />
|
||||
<link rel="stylesheet" href="/static/css/index.css?v={{v}}" id="index-stylesheet" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
id="tabulator-stylesheet"
|
||||
@@ -21,7 +21,7 @@
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/static/js/jquery.min.js"
|
||||
src="/static/js/jquery.min.js?v={{v}}"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
@@ -34,7 +34,7 @@
|
||||
<script>
|
||||
var SOCKET_PORT = {{port}};
|
||||
</script>
|
||||
<script defer src="./static/js/index.js"></script>
|
||||
<script defer src="./static/js/index.js?v={{v}}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
|
||||
@@ -168,12 +168,12 @@ class Polyline(bonsai.core.tool.Polyline):
|
||||
distance = (mouse_vector - last_point).length
|
||||
if distance < 0:
|
||||
return
|
||||
angle, orientation_angle, angle_round_threshold = None, None, None
|
||||
angle, orientation_angle = None, None
|
||||
angle_round_threshold = 1000 # Avoids rounding when distance is too big
|
||||
if distance > 0:
|
||||
angle = tool.Cad.angle_3_vectors(
|
||||
second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
|
||||
)
|
||||
angle_round_threshold = 1000 # Avoids rounding when distance is too big
|
||||
|
||||
# Round angle to the nearest 0.05
|
||||
angle = round(angle / 0.05) * 0.05 if distance < angle_round_threshold else angle
|
||||
@@ -189,7 +189,7 @@ class Polyline(bonsai.core.tool.Polyline):
|
||||
angle = 0
|
||||
orientation_angle = 0
|
||||
if input_ui:
|
||||
assert angle is not None and orientation_angle is not None and angle_round_threshold is not None
|
||||
assert angle is not None and orientation_angle is not None
|
||||
if should_round:
|
||||
angle_snap = tool.Snap.get_angle_snap_value(context)
|
||||
angle = angle_snap * round(angle / angle_snap) if distance < angle_round_threshold else angle
|
||||
|
||||
@@ -644,9 +644,9 @@ class Style(bonsai.core.tool.Style):
|
||||
) -> tuple[tuple[float, float, float], float]:
|
||||
if material.node_tree:
|
||||
nodes = material.node_tree.nodes
|
||||
output_node = next(
|
||||
(n for n in nodes if n.type == "OUTPUT_MATERIAL" and n.is_active_output), None
|
||||
) or next((n for n in nodes if n.type == "OUTPUT_MATERIAL"), None)
|
||||
output_node = next((n for n in nodes if n.type == "OUTPUT_MATERIAL" and n.is_active_output), None) or next(
|
||||
(n for n in nodes if n.type == "OUTPUT_MATERIAL"), None
|
||||
)
|
||||
if output_node:
|
||||
result = cls._color_from_shader_socket(output_node.inputs["Surface"])
|
||||
if result:
|
||||
|
||||
@@ -497,6 +497,8 @@ class TestGitMergetool:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
ifc_path = os.path.join(tmpdir, "model.ifc")
|
||||
mock_repo = mock.MagicMock()
|
||||
# A clean mergetool resolution leaves no unmerged blobs in the index.
|
||||
mock_repo.index.unmerged_blobs.return_value = {}
|
||||
IfcGitRepo.repo = mock_repo
|
||||
result = IfcGit.git_mergetool("ifcmerge", ifc_path)
|
||||
assert result is None
|
||||
@@ -511,6 +513,8 @@ class TestGitMergetool:
|
||||
report_path = ifc_path + ".ifcmerge"
|
||||
open(report_path, "w").close()
|
||||
mock_repo = mock.MagicMock()
|
||||
# A clean mergetool resolution leaves no unmerged blobs in the index.
|
||||
mock_repo.index.unmerged_blobs.return_value = {}
|
||||
IfcGitRepo.repo = mock_repo
|
||||
result = IfcGit.git_mergetool("ifcmerge", ifc_path)
|
||||
assert result is None
|
||||
|
||||
@@ -56,3 +56,31 @@ class TestValidateInput(NewFile):
|
||||
|
||||
# Angle.
|
||||
assert subject.validate_input("25", "A") == (True, "25.0")
|
||||
|
||||
|
||||
class TestCalculateDistanceAndAngle(NewFile):
|
||||
def test_it_does_not_crash_when_distance_is_zero_and_should_round(self, monkeypatch):
|
||||
# Regression test for #8597: right after placing the first polyline
|
||||
# point, the initial mouse sample can equal the last placed point
|
||||
# (distance == 0), e.g. entering the viewport on a YZ plane wall.
|
||||
# angle_round_threshold used to only be assigned in the
|
||||
# `distance > 0` branch, crashing when should_round reads it here.
|
||||
# get_increment_snap_value requires a real 3D viewport rv3d, which
|
||||
# is unrelated to this bug, so it's stubbed out for a headless run.
|
||||
monkeypatch.setattr(tool.Snap, "get_increment_snap_value", classmethod(lambda cls, context: 1.0))
|
||||
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
mouse_point = polyline_props.snap_mouse_point.add()
|
||||
mouse_point.x, mouse_point.y, mouse_point.z = 0, 0, 0
|
||||
|
||||
tool_state = subject.create_tool_state()
|
||||
tool_state.is_input_on = False
|
||||
tool_state.use_default_container = False
|
||||
tool_state.plane_method = "YZ"
|
||||
|
||||
input_ui = subject.create_input_ui(input_options=["D", "A", "X", "Y", "Z"])
|
||||
|
||||
subject.calculate_distance_and_angle(bpy.context, input_ui, tool_state, should_round=True)
|
||||
|
||||
assert input_ui.get_number_value("D") == 0
|
||||
assert input_ui.get_number_value("A") == 0
|
||||
|
||||
@@ -386,12 +386,71 @@ class Ifc5Dwriter:
|
||||
"PredefinedType": cost_schedule.PredefinedType,
|
||||
}
|
||||
|
||||
# Presentation formats (.ods / .xlsx) mirror exactly what the Bonsai cost
|
||||
# panel shows for a cost item: ID (Identification), Name, Quantity,
|
||||
# Value (RateSubtotal) and the calculated Total Cost. Everything else
|
||||
# (internal bookkeeping columns, Description, Unit, per-category cost
|
||||
# breakdowns) is bonsai/csv2ifc round-trip plumbing and stays out of the
|
||||
# presentation formats. The .csv format keeps the full column set since
|
||||
# csv2ifc reads those extra columns back in on import.
|
||||
PRESENTATION_COLUMNS = ("Identification", "Name", "Quantity", "RateSubtotal", "TotalPrice")
|
||||
|
||||
# Header text as shown in presentation formats, matching the Bonsai cost
|
||||
# panel's own column labels (see BIM_UL_cost_items_trait.draw_header).
|
||||
PRESENTATION_LABELS = {
|
||||
"Identification": "ID",
|
||||
"RateSubtotal": "Value",
|
||||
"TotalPrice": "Total Cost",
|
||||
}
|
||||
|
||||
def multiply_cells(self, cell1, cell2):
|
||||
return "={}*{}".format(cell1, cell2)
|
||||
|
||||
def sum_cells(self, list_of_cells):
|
||||
return "=SUM({})".format(",".join(list_of_cells))
|
||||
|
||||
def get_visible_headers(self, schedule_id: int) -> list[str]:
|
||||
"""Internal column keys shown in presentation formats, in panel order."""
|
||||
headers = self.sheet_data[schedule_id]["headers"]
|
||||
return [h for h in self.PRESENTATION_COLUMNS if h in headers]
|
||||
|
||||
def get_display_label(self, column: str) -> str:
|
||||
"""Header text to write for a column in presentation formats."""
|
||||
return self.PRESENTATION_LABELS.get(column, column)
|
||||
|
||||
def is_numeric_column(self, column: str) -> bool:
|
||||
return column in ("Quantity", "RateSubtotal", "TotalPrice") or column.endswith(" Cost")
|
||||
|
||||
def get_total_price_formula(self, schedule_id: int, cost_item_index: int, first_data_row: int) -> Union[str, None]:
|
||||
"""Spreadsheet formula for the TotalPrice cell of a cost item, or None for a plain value.
|
||||
|
||||
Sum items get ``=SUM(...)`` over the TotalPrice cells of their direct
|
||||
children, leaf items with a quantity and a rate get ``=Quantity*RateSubtotal``.
|
||||
Assumes one cost item per row, in ``cost_items`` order, starting at
|
||||
``first_data_row`` (1-based).
|
||||
"""
|
||||
items = self.sheet_data[schedule_id]["cost_items"]
|
||||
headers = self.get_visible_headers(schedule_id)
|
||||
if "TotalPrice" not in headers:
|
||||
return None
|
||||
item = items[cost_item_index]
|
||||
col = lambda name: self.column_indexes[headers.index(name)]
|
||||
if item["ItemIsASum"]:
|
||||
prefix = item["Hierarchy"] + "."
|
||||
child_rows = [
|
||||
first_data_row + i
|
||||
for i, other in enumerate(items)
|
||||
if other["Hierarchy"].startswith(prefix) and "." not in other["Hierarchy"][len(prefix) :]
|
||||
]
|
||||
if child_rows:
|
||||
total_col = col("TotalPrice")
|
||||
return self.sum_cells(["{}{}".format(total_col, r) for r in child_rows])
|
||||
return None
|
||||
if "Quantity" in headers and "RateSubtotal" in headers and item.get("Quantity") and item.get("RateSubtotal"):
|
||||
row = first_data_row + cost_item_index
|
||||
return self.multiply_cells("{}{}".format(col("Quantity"), row), "{}{}".format(col("RateSubtotal"), row))
|
||||
return None
|
||||
|
||||
def get_cell_position(self, schedule_id, attribute):
|
||||
def get_position_in_list(item, item_list):
|
||||
try:
|
||||
@@ -482,32 +541,25 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
|
||||
assert False, type
|
||||
row.addElement(cell)
|
||||
|
||||
def add_cost_item_rows(table, cost_data):
|
||||
first_data_row = 6 # 3 metadata rows, 1 blank row, 1 header row.
|
||||
|
||||
def add_cost_item_rows(table, cost_data, cost_item_index):
|
||||
row = TableRow()
|
||||
self.row_count += 1
|
||||
style = self.colours.get(cost_data["Index"])
|
||||
|
||||
for i, column in enumerate(self.sheet_data[cost_schedule.id()]["headers"]):
|
||||
if column == "Total Price" and cost_data["Quantity"] != 0 and cost_data["Rate Subtotal"]:
|
||||
cell_quantity = self.get_cell_position(cost_schedule.id(), "Quantity")
|
||||
cell_subtotal_rate = self.get_cell_position(cost_schedule.id(), "Rate Subtotal")
|
||||
value = self.multiply_cells(cell_quantity, cell_subtotal_rate)
|
||||
cell = TableCell(formula=value, stylename=self.colours.get(cost_data["Index"]))
|
||||
for column in self.get_visible_headers(cost_schedule.id()):
|
||||
value = cost_data.get(column, "")
|
||||
formula = None
|
||||
if column == "TotalPrice":
|
||||
formula = self.get_total_price_formula(cost_schedule.id(), cost_item_index, first_data_row)
|
||||
if formula:
|
||||
cell = TableCell(formula=formula, stylename=style)
|
||||
elif self.is_numeric_column(column) and isinstance(value, (int, float)):
|
||||
cell = TableCell(valuetype="float", value=value, stylename=style)
|
||||
else:
|
||||
value = cost_data.get(column, "")
|
||||
cell = TableCell(valuetype="string", stylename=self.colours.get(cost_data["Index"]))
|
||||
cell = TableCell(valuetype="string", stylename=style)
|
||||
cell.addElement(P(text=value))
|
||||
# TODO:FIX QUANTITY AND COST TO SHOW AS NUMBERS AND CURRENCIES
|
||||
# elif "Cost" in column or "Rate" in column:
|
||||
# value = cost_data.get(column, "")
|
||||
# cell = TableCell(valuetype="string", stylename=self.colours.get(cost_data["Index"]))
|
||||
# cell.addElement(P(text=value))
|
||||
# # cell.addElement(P(text=u"${}".format(value))) # The current displayed value
|
||||
# print("Should add rate ", "${}".format(value))
|
||||
# elif "Quantity" in column:
|
||||
# value = cost_data.get(column, "")
|
||||
# cell = TableCell(valuetype="float", stylename=self.colours.get(cost_data["Index"]))
|
||||
# print("Should add quantity",value)
|
||||
# cell.addElement(P(text=value))
|
||||
row.addElement(cell)
|
||||
table.addElement(row)
|
||||
|
||||
@@ -534,20 +586,22 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
|
||||
table.addElement(new)
|
||||
|
||||
header_row = TableRow()
|
||||
for header in self.sheet_data[cost_schedule.id()]["headers"]:
|
||||
add_cell(type="text", value=header, row=header_row, style="fed8b1")
|
||||
for header in self.get_visible_headers(cost_schedule.id()):
|
||||
add_cell(type="text", value=self.get_display_label(header), row=header_row, style="fed8b1")
|
||||
table.addElement(header_row)
|
||||
|
||||
self.row_count = 5
|
||||
for cost_item_data in self.sheet_data[cost_schedule.id()]["cost_items"]:
|
||||
add_cost_item_rows(table, cost_item_data)
|
||||
for i, cost_item_data in enumerate(self.sheet_data[cost_schedule.id()]["cost_items"]):
|
||||
add_cost_item_rows(table, cost_item_data, i)
|
||||
|
||||
self.doc.spreadsheet.addElement(table)
|
||||
|
||||
|
||||
class Ifc5DXlsxWriter(Ifc5Dwriter):
|
||||
def write(self) -> None:
|
||||
import xlsxwriter
|
||||
# openpyxl rather than xlsxwriter: it is what ifccsv already uses and
|
||||
# what ships with Bonsai, so XLSX export works out of the box there.
|
||||
import openpyxl
|
||||
|
||||
super().write()
|
||||
os.makedirs(self.output, exist_ok=True)
|
||||
@@ -558,24 +612,31 @@ class Ifc5DXlsxWriter(Ifc5Dwriter):
|
||||
else:
|
||||
file_name += cost_schedule.Name or ""
|
||||
self.file_path = os.path.join(self.output, "{}.xlsx".format(file_name))
|
||||
self.workbook = xlsxwriter.Workbook(self.file_path)
|
||||
self.workbook = openpyxl.Workbook()
|
||||
self.workbook.remove(self.workbook.active)
|
||||
for cost_schedule in self.cost_schedules:
|
||||
self.write_table(cost_schedule)
|
||||
self.workbook.close()
|
||||
self.workbook.save(self.file_path)
|
||||
|
||||
def write_table(self, cost_schedule):
|
||||
worksheet = self.workbook.add_worksheet(self.sheet_data[cost_schedule.id()]["Name"])
|
||||
headers = self.sheet_data[cost_schedule.id()]["headers"]
|
||||
for i, header in enumerate(headers):
|
||||
worksheet.write(0, i, header)
|
||||
import re
|
||||
|
||||
row = 1
|
||||
for cost_item_data in self.sheet_data[cost_schedule.id()]["cost_items"]:
|
||||
col = 0
|
||||
sheet_id = cost_schedule.id()
|
||||
title = re.sub(r"[\[\]:*?/\\]", "_", self.sheet_data[sheet_id]["Name"])[:31]
|
||||
worksheet = self.workbook.create_sheet(title)
|
||||
headers = self.get_visible_headers(sheet_id)
|
||||
worksheet.append([self.get_display_label(h) for h in headers])
|
||||
|
||||
first_data_row = 2 # Row 1 is the header.
|
||||
for i, cost_item_data in enumerate(self.sheet_data[sheet_id]["cost_items"]):
|
||||
row = []
|
||||
for header in headers:
|
||||
worksheet.write(row, col, cost_item_data.get(header, ""))
|
||||
col += 1
|
||||
row += 1
|
||||
formula = None
|
||||
if header == "TotalPrice":
|
||||
formula = self.get_total_price_formula(sheet_id, i, first_data_row)
|
||||
# openpyxl treats strings starting with "=" as formulas.
|
||||
row.append(formula if formula else cost_item_data.get(header, None))
|
||||
worksheet.append(row)
|
||||
|
||||
|
||||
class Ifc5DPdfWriter(Ifc5Dwriter):
|
||||
|
||||
@@ -20,10 +20,14 @@ dependencies = [
|
||||
"typing_extensions",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
advanced = [
|
||||
"typst",
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
advanced = [
|
||||
"typst",
|
||||
]
|
||||
spreadsheet = [
|
||||
"odfpy",
|
||||
"openpyxl",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "http://ifcopenshell.org"
|
||||
|
||||
@@ -120,6 +120,34 @@ class TestCsv2Ifc:
|
||||
assert len(list(Path(temp_csv_dir).glob("*.ods"))) == 1
|
||||
assert len(list(Path(temp_csv_dir).glob("*.xlsx"))) == 1
|
||||
|
||||
def test_xlsx_columns_match_cost_panel(self):
|
||||
"""ODS/XLSX are presentation formats: they must show exactly what the
|
||||
Bonsai cost panel shows (ID, Name, Quantity, Value, Total Cost), no
|
||||
internal bookkeeping columns, no Description/Unit, no per-category
|
||||
cost breakdown. See #6251."""
|
||||
import openpyxl
|
||||
|
||||
ifc_file = self.setup_ifc_file()
|
||||
csv_filepath = Path(__file__).parent.parent / "sample_cost_schedule_house_FR.csv"
|
||||
ifc5d.csv2ifc.Csv2Ifc(str(csv_filepath), ifc_file).execute()
|
||||
|
||||
with tempfile.TemporaryDirectory("w") as temp_dir:
|
||||
writer = ifc5d.ifc5Dspreadsheet.Ifc5DXlsxWriter(ifc_file, temp_dir)
|
||||
writer.write()
|
||||
workbook = openpyxl.load_workbook(next(Path(temp_dir).glob("*.xlsx")))
|
||||
worksheet = workbook.active
|
||||
|
||||
headers = [cell.value for cell in next(worksheet.iter_rows())]
|
||||
assert headers == ["ID", "Name", "Quantity", "Value", "Total Cost"]
|
||||
|
||||
# A leaf item (has quantity and value) gets Quantity * Value.
|
||||
leaf_row = next(row for row in worksheet.iter_rows(min_row=2) if row[0].value == "DB.1.1")
|
||||
assert leaf_row[4].value == "=C{}*D{}".format(leaf_row[0].row, leaf_row[0].row)
|
||||
|
||||
# A parent/sum item gets the sum of its direct children's Total Cost.
|
||||
parent_row = next(row for row in worksheet.iter_rows(min_row=2) if row[0].value == "DB.1")
|
||||
assert parent_row[4].value.startswith("=SUM(")
|
||||
|
||||
|
||||
class TestSerialiseCostQuantities:
|
||||
def test_quantity_name_with_special_characters_round_trips_as_json(self):
|
||||
|
||||
@@ -36,7 +36,7 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.selector
|
||||
import numpy as np
|
||||
from deepdiff import DeepDiff
|
||||
from orderly_set import OrderedSet
|
||||
from orderly_set import StableSet
|
||||
|
||||
__version__ = version = "0.0.0"
|
||||
|
||||
@@ -257,7 +257,7 @@ class IfcDiff:
|
||||
|
||||
def json_dump_default(self, obj):
|
||||
# result of DeepDiff may contain ordered sets
|
||||
if isinstance(obj, (OrderedSet, set)):
|
||||
if isinstance(obj, (StableSet, set)):
|
||||
return list(obj)
|
||||
return json.JSONEncoder.default(None, obj)
|
||||
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.context
|
||||
import ifcopenshell.api.geometry
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.unit
|
||||
import ifcopenshell.util.representation
|
||||
@@ -94,6 +99,29 @@ class TestIfcDiff:
|
||||
assert ifc_diff.deleted_elements == set()
|
||||
assert ifc_diff.change_register == {wall.GlobalId: {"attributes_changed": True}}
|
||||
|
||||
def test_property_diff_exports_to_json(self):
|
||||
# Regression test for #8905: comparing "property" relationships makes
|
||||
# DeepDiff report a dictionary_item_added as a SetOrdered, which
|
||||
# json.dump couldn't serialise, crashing export() with no results.
|
||||
ifc_file = setup_project()
|
||||
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo")
|
||||
|
||||
new_file = ifc_file.from_string(ifc_file.to_string())
|
||||
wall_new = new_file.by_id(wall.id())
|
||||
pset = ifcopenshell.api.pset.add_pset(new_file, product=wall_new, name="Pset_WallCommon")
|
||||
ifcopenshell.api.pset.edit_pset(new_file, pset=pset, properties={"FireRating": "2HR"})
|
||||
|
||||
ifc_diff = ifcdiff.IfcDiff(ifc_file, new_file, relationships=["property"])
|
||||
ifc_diff.diff()
|
||||
assert ifc_diff.change_register[wall.GlobalId]["properties_changed"]
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
output = os.path.join(tmp_dir, "diff.json")
|
||||
ifc_diff.export(output)
|
||||
with open(output) as f:
|
||||
results = json.load(f)
|
||||
assert "Pset_WallCommon" in str(results["changed"][wall.GlobalId]["properties_changed"])
|
||||
|
||||
def test_changed_geometry(self):
|
||||
ifc_file = setup_project()
|
||||
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo")
|
||||
|
||||
@@ -21,66 +21,110 @@
|
||||
#define mapping POSTFIX_SCHEMA(mapping)
|
||||
using namespace ifcopenshell::geometry;
|
||||
|
||||
#include "../profile_helper.h"
|
||||
|
||||
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCenterLineProfileDef* inst) {
|
||||
return nullptr;
|
||||
|
||||
/*
|
||||
const double d = inst->Thickness() * length_unit_ / 2.;
|
||||
auto f = taxonomy::make<taxonomy::face>();
|
||||
auto ofc = taxonomy::make<taxonomy::offset_curve>();
|
||||
ofc->basis = map(inst->Curve());
|
||||
ofc->offset = d;
|
||||
// @todo
|
||||
// f->children.push_back(ofc);
|
||||
return f;
|
||||
*/
|
||||
const double eps = settings_.get<settings::Precision>().get();
|
||||
|
||||
// @todo we still need to handle this in the geometry libraries
|
||||
|
||||
/*
|
||||
TopoDS_Wire wire;
|
||||
if (!convert_wire(inst->Curve(), wire)) return false;
|
||||
|
||||
// BRepOffsetAPI_MakeOffset insists on creating circular arc
|
||||
// segments for joining the curves that constitute the center
|
||||
// line. This is probably not in accordance with the IFC spec.
|
||||
// Although it does not specify a method to join segments
|
||||
// explicitly, it does dictate 'a constant thickness along the
|
||||
// curve'. Therefore for simple singular wires a quick
|
||||
// alternative is provided that uses a straight join.
|
||||
|
||||
TopExp_Explorer exp(wire, TopAbs_EDGE);
|
||||
TopoDS_Edge edge = TopoDS::Edge(exp.Current());
|
||||
exp.Next();
|
||||
|
||||
if (!exp.More()) {
|
||||
double u1, u2;
|
||||
Handle(Geom_Curve) curve = BRep_Tool::Curve(edge, u1, u2);
|
||||
|
||||
Handle(Geom_TrimmedCurve) trim = new Geom_TrimmedCurve(curve, u1, u2);
|
||||
|
||||
Handle(Geom_OffsetCurve) c1 = new Geom_OffsetCurve(trim, d, gp::DZ());
|
||||
Handle(Geom_OffsetCurve) c2 = new Geom_OffsetCurve(trim, -d, gp::DZ());
|
||||
|
||||
gp_Pnt c1a, c1b, c2a, c2b;
|
||||
c1->D0(c1->FirstParameter(), c1a);
|
||||
c1->D0(c1->LastParameter(), c1b);
|
||||
c2->D0(c2->FirstParameter(), c2a);
|
||||
c2->D0(c2->LastParameter(), c2b);
|
||||
|
||||
BRepBuilderAPI_MakeWire mw;
|
||||
mw.Add(BRepBuilderAPI_MakeEdge(c1));
|
||||
mw.Add(BRepBuilderAPI_MakeEdge(c1a, c2a));
|
||||
mw.Add(BRepBuilderAPI_MakeEdge(c2));
|
||||
mw.Add(BRepBuilderAPI_MakeEdge(c2b, c1b));
|
||||
|
||||
face = BRepBuilderAPI_MakeFace(mw.Wire());
|
||||
} else {
|
||||
BRepOffsetAPI_MakeOffset offset(BRepBuilderAPI_MakeFace(gp_Pln(gp::Origin(), gp::DZ())));
|
||||
offset.AddWire(wire);
|
||||
offset.Perform(d);
|
||||
face = BRepBuilderAPI_MakeFace(TopoDS::Wire(offset));
|
||||
if (d < eps) {
|
||||
logger_.Warning("GEO", 403, "Thickness below precision for:", inst);
|
||||
return nullptr;
|
||||
}
|
||||
return true;
|
||||
*/
|
||||
|
||||
auto mapped = map(inst->Curve());
|
||||
if (!mapped) {
|
||||
return nullptr;
|
||||
}
|
||||
auto crv = taxonomy::dcast<taxonomy::loop>(mapped);
|
||||
if (!crv || crv->children.empty()) {
|
||||
logger_.Warning("GEO", 404, "Unsupported centerline curve for:", inst);
|
||||
return nullptr;
|
||||
}
|
||||
if (!crv->is_polyhedron()) {
|
||||
logger_.Warning("GEO", 404, "Unsupported curved centerline segments for:", inst);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<Eigen::Vector2d> pts;
|
||||
pts.reserve(crv->children.size() + 1);
|
||||
for (auto& e : crv->children) {
|
||||
if (e->start.which() != 1 || e->end.which() != 1) {
|
||||
logger_.Warning("GEO", 404, "Unsupported parametric trims on centerline for:", inst);
|
||||
return nullptr;
|
||||
}
|
||||
Eigen::Vector2d p = boost::get<taxonomy::point3::ptr>(e->start)->ccomponents().head<2>();
|
||||
Eigen::Vector2d q = boost::get<taxonomy::point3::ptr>(e->end)->ccomponents().head<2>();
|
||||
if (pts.empty()) {
|
||||
pts.push_back(p);
|
||||
} else if ((pts.back() - p).norm() > eps) {
|
||||
logger_.Warning("GEO", 405, "Unsupported discontinuous centerline for:", inst);
|
||||
return nullptr;
|
||||
}
|
||||
pts.push_back(q);
|
||||
}
|
||||
|
||||
if (pts.size() < 2 || (pts.front() - pts.back()).norm() < eps) {
|
||||
logger_.Warning("GEO", 405, "Unsupported closed or degenerate centerline for:", inst);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const size_t n = pts.size();
|
||||
std::vector<Eigen::Vector2d> normals(n - 1);
|
||||
for (size_t i = 0; i < n - 1; ++i) {
|
||||
Eigen::Vector2d t = pts[i + 1] - pts[i];
|
||||
const double l = t.norm();
|
||||
if (l < eps) {
|
||||
logger_.Warning("GEO", 405, "Degenerate centerline segment for:", inst);
|
||||
return nullptr;
|
||||
}
|
||||
t /= l;
|
||||
normals[i] = Eigen::Vector2d(-t.y(), t.x());
|
||||
}
|
||||
|
||||
// Straight miter joins at the vertices, because the profile prescribes a
|
||||
// constant thickness along the curve, which the arc joins of a generic
|
||||
// offset algorithm would violate.
|
||||
std::vector<Eigen::Vector2d> miters(n);
|
||||
miters.front() = normals.front();
|
||||
miters.back() = normals.back();
|
||||
for (size_t i = 1; i < n - 1; ++i) {
|
||||
const double denom = 1. + normals[i - 1].dot(normals[i]);
|
||||
if (denom < 1.e-9) {
|
||||
logger_.Warning("GEO", 405, "Centerline reverses onto itself for:", inst);
|
||||
return nullptr;
|
||||
}
|
||||
miters[i] = (normals[i - 1] + normals[i]) / denom;
|
||||
}
|
||||
|
||||
std::vector<Eigen::Vector2d> polygon;
|
||||
polygon.reserve(2 * n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
polygon.push_back(pts[i] - d * miters[i]);
|
||||
}
|
||||
for (size_t i = n; i-- > 0;) {
|
||||
polygon.push_back(pts[i] + d * miters[i]);
|
||||
}
|
||||
|
||||
double twice_area = 0.;
|
||||
for (size_t i = 0; i < polygon.size(); ++i) {
|
||||
const auto& a = polygon[i];
|
||||
const auto& b = polygon[(i + 1) % polygon.size()];
|
||||
twice_area += a.x() * b.y() - b.x() * a.y();
|
||||
}
|
||||
if (twice_area < 0.) {
|
||||
std::reverse(polygon.begin(), polygon.end());
|
||||
}
|
||||
|
||||
std::vector<taxonomy::point3::ptr> ps;
|
||||
ps.reserve(polygon.size() + 1);
|
||||
for (auto& p : polygon) {
|
||||
ps.push_back(taxonomy::make<taxonomy::point3>(p.x(), p.y(), 0.));
|
||||
}
|
||||
ps.push_back(ps.front());
|
||||
|
||||
auto loop = polygon_from_points(ps);
|
||||
auto face = taxonomy::make<taxonomy::face>();
|
||||
face->children = { loop };
|
||||
return face;
|
||||
}
|
||||
|
||||
@@ -73,13 +73,6 @@ FILTERED_CARTESIAN_QUOTIENT: Any
|
||||
EXACT_PREDICATES: Any
|
||||
EXACT_CONSTRUCTIONS: Any
|
||||
|
||||
FT_AUTODETECT: Any
|
||||
FT_IFCSPF: Any
|
||||
FT_IFCXML: Any
|
||||
FT_IFCZIP: Any
|
||||
FT_ROCKSDB: Any
|
||||
FT_UNKNOWN: Any
|
||||
|
||||
CURVES: Any
|
||||
SURFACES_AND_SOLIDS: Any
|
||||
CURVES_SURFACES_AND_SOLIDS: Any
|
||||
@@ -133,7 +126,6 @@ class BRepElement(Element):
|
||||
def calculate_projected_surface_area(self, along_x, along_y, along_z): ...
|
||||
@property
|
||||
def geometry(self) -> BRep: ...
|
||||
def geometry_pointer(self): ...
|
||||
@property
|
||||
def surface_area(self): ...
|
||||
@property
|
||||
@@ -625,7 +617,6 @@ class TriangulationElement(Element):
|
||||
def __init__(self, *args): ...
|
||||
@property
|
||||
def geometry(self) -> Triangulation: ...
|
||||
def geometry_pointer(self): ...
|
||||
|
||||
class TtlWktSerializer(WriteOnlyGeometrySerializer):
|
||||
def __init__(self, filename, geometry_settings, settings, logger=None): ...
|
||||
@@ -1799,21 +1790,20 @@ def flatten(deep): ...
|
||||
def get_feature(x): ...
|
||||
def get_info_cpp(v, include_identifier=True): ...
|
||||
def get_log(): ...
|
||||
def guess_file_type(fn): ...
|
||||
def helmert_curve_point(A0, A1, A2, s): ...
|
||||
def kind_to_string(k): ...
|
||||
def less(arg1, arg2): ...
|
||||
def line_segments_to_polygons(s, eps, segments): ...
|
||||
def map_shape(settings, instance): ...
|
||||
def logger_or_root(logger) -> logger: ...
|
||||
def logger_or_root(logger: logger | None) -> logger: ...
|
||||
def nary_union(sequence): ...
|
||||
def new_IfcBaseClass(schema_identifier: str, name: str) -> entity_instance: ...
|
||||
def open(fn: str, readonly: bool = False, logger=None) -> file: ...
|
||||
def parse_ifcxml(filename, logger=None): ...
|
||||
def polygons_to_svg(*args): ...
|
||||
def read(data): ...
|
||||
def register_schema(arg1): ...
|
||||
def schema_by_name(arg1: str) -> schema_definition: ...
|
||||
def register_schema(schema: schema_definition) -> None: ...
|
||||
def schema_by_name(schema_name: str) -> schema_definition: ...
|
||||
def schema_names() -> tuple[str, ...]: ...
|
||||
def serialise(schema_name, shape_str, advanced=True): ...
|
||||
def set_feature(x, v): ...
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
lxml==4.9.1
|
||||
lxml==6.1.0
|
||||
numpy==1.22.0
|
||||
regex==2021.4.4
|
||||
xmlschema==1.6.4
|
||||
@@ -540,8 +540,15 @@ def _get_element_value(element: ifcopenshell.entity_instance, keys: list[str]) -
|
||||
value = results or None
|
||||
if value and len(value) == 1:
|
||||
value = value[0]
|
||||
elif key in value:
|
||||
value = value[key]
|
||||
else:
|
||||
value = value.get(key, None)
|
||||
# A nested complex quantity/property (IfcPhysicalComplexQuantity /
|
||||
# IfcComplexProperty) is represented as a dict whose nested members
|
||||
# live under a "properties" sub-dict. Descend into it so that nested
|
||||
# values are reachable with the natural "Qto.Complex.Nested" path.
|
||||
subprops = value.get("properties")
|
||||
value = subprops.get(key, None) if isinstance(subprops, dict) else None
|
||||
elif isinstance(value, (list, tuple, set)): # If we use regex
|
||||
if isinstance(key, str) and key.isnumeric():
|
||||
try:
|
||||
|
||||
@@ -195,6 +195,37 @@ class TestGetElementValue(test.bootstrap.IFC4):
|
||||
assert subject.get_element_value(element, "/Pset_.*Common/.Status") == ["New"]
|
||||
assert subject.get_element_value(element, "/Pset_.*Common/.Status.0") == "New"
|
||||
|
||||
def test_selecting_a_nested_complex_quantity(self):
|
||||
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||
complex_quantity = self.file.create_entity(
|
||||
"IfcPhysicalComplexQuantity",
|
||||
Name="Layer1",
|
||||
Discrimination="layer",
|
||||
HasQuantities=[
|
||||
self.file.create_entity("IfcQuantityLength", Name="Width", LengthValue=0.1),
|
||||
self.file.create_entity("IfcQuantityLength", Name="Height", LengthValue=2.5),
|
||||
],
|
||||
)
|
||||
quantity = self.file.create_entity(
|
||||
"IfcElementQuantity",
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
Name="Qto_Custom",
|
||||
Quantities=[complex_quantity, self.file.create_entity("IfcQuantityArea", Name="NetArea", AreaValue=5.0)],
|
||||
)
|
||||
self.file.create_entity(
|
||||
"IfcRelDefinesByProperties",
|
||||
GlobalId=ifcopenshell.guid.new(),
|
||||
RelatedObjects=[element],
|
||||
RelatingPropertyDefinition=quantity,
|
||||
)
|
||||
# A simple quantity in the same set still resolves normally.
|
||||
assert subject.get_element_value(element, "Qto_Custom.NetArea") == 5.0
|
||||
# Nested quantities of a complex quantity are reachable with the natural path.
|
||||
assert subject.get_element_value(element, "Qto_Custom.Layer1.Width") == 0.1
|
||||
assert subject.get_element_value(element, "Qto_Custom.Layer1.Height") == 2.5
|
||||
# The explicit "properties" path is preserved for backwards compatibility.
|
||||
assert subject.get_element_value(element, "Qto_Custom.Layer1.properties.Width") == 0.1
|
||||
|
||||
|
||||
class TestFilterElements(test.bootstrap.IFC4):
|
||||
def test_selecting_by_globalid(self):
|
||||
|
||||
@@ -513,11 +513,11 @@ class IFC_PARSE_API schema_definition {
|
||||
IfcUtil::IfcBaseClass* instantiate(const IfcParse::declaration* decl, IfcEntityInstanceData&& data) const;
|
||||
};
|
||||
|
||||
IFC_PARSE_API const schema_definition* schema_by_name(const std::string&);
|
||||
IFC_PARSE_API const schema_definition* schema_by_name(const std::string& schema_name);
|
||||
|
||||
IFC_PARSE_API std::vector<std::string> schema_names();
|
||||
|
||||
IFC_PARSE_API void register_schema(schema_definition*);
|
||||
IFC_PARSE_API void register_schema(schema_definition* schema);
|
||||
|
||||
IFC_PARSE_API void clear_schemas();
|
||||
} // namespace IfcParse
|
||||
|
||||
Generated
+50
-76
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"hyperid": "^3.3.0",
|
||||
"hyperid": "^4.0.0",
|
||||
"lucide-svelte": "^0.542.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"svelecte": "^5.2.0",
|
||||
@@ -25,7 +25,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"sass-embedded": "^1.89.0",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte": "^5.55.8",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
@@ -1605,9 +1605,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1625,7 +1625,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-builder": {
|
||||
@@ -1715,41 +1715,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
|
||||
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
|
||||
"integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||
"version": "6.6.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz",
|
||||
"integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"ws": "~8.21.0",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
@@ -1821,12 +1804,20 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"version": "2.2.9",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz",
|
||||
"integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/types": "^8.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@typescript-eslint/types": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
@@ -1883,14 +1874,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hyperid": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz",
|
||||
"integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hyperid/-/hyperid-4.0.0.tgz",
|
||||
"integrity": "sha512-vjhh0CG44/iGjcNEjebL0BpRDR+C7d1KZ1Yqf92ralTOO5JjZiGHQT0bjDaKuNu1kk3jB+5jGFhNbxkT0wcQmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.2.1",
|
||||
"uuid": "^8.3.2",
|
||||
"uuid-parse": "^1.1.0"
|
||||
"buffer": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
@@ -1914,9 +1903,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
||||
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2326,9 +2315,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2365,9 +2354,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
|
||||
"integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
|
||||
"version": "8.5.22",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
||||
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2385,7 +2374,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -2977,9 +2966,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.53.6",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.6.tgz",
|
||||
"integrity": "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==",
|
||||
"version": "5.55.8",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.8.tgz",
|
||||
"integrity": "sha512-4D6lyrMHmDaZalQOEBMCWCCidyZjSnec14/oPn0k627G6goxcck9xqMwz1tFLlQz+ZFvtTTHfFOlUayuAz0z6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
@@ -2991,9 +2980,9 @@
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.3",
|
||||
"devalue": "^5.8.1",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.2.2",
|
||||
"esrap": "^2.2.4",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
@@ -3177,9 +3166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.16",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
|
||||
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
|
||||
"version": "7.5.21",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.21.tgz",
|
||||
"integrity": "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
@@ -3241,21 +3230,6 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid-parse": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz",
|
||||
"integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/varint": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
||||
@@ -3358,9 +3332,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"sass-embedded": "^1.89.0",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte": "^5.55.8",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"hyperid": "^3.3.0",
|
||||
"hyperid": "^4.0.0",
|
||||
"lucide-svelte": "^0.542.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"svelecte": "^5.2.0",
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
%ignore stream_or_filename::stream;
|
||||
%ignore boost::hash_value;
|
||||
%ignore IfcGeom::BRepElement::geometry_pointer;
|
||||
%ignore IfcGeom::TriangulationElement::geometry_pointer;
|
||||
|
||||
// This is only used for RGB colours, hence the size of 3
|
||||
%typemap(out) const double* {
|
||||
@@ -254,6 +256,22 @@ namespace {
|
||||
%include "../ifcgeom/Converter.h"
|
||||
%include "../ifcgeom/ConversionResult.h"
|
||||
%include "../ifcgeom/ConversionSettings.h"
|
||||
|
||||
// Keep the owning element alive while its geometry is referenced (#1124).
|
||||
%define GEOMETRY_WITH_BACKREF(cls)
|
||||
%feature("shadow") cls::geometry %{
|
||||
@property
|
||||
def geometry(self):
|
||||
result = $action(self)
|
||||
result._parent = self
|
||||
return result
|
||||
%}
|
||||
%enddef
|
||||
|
||||
GEOMETRY_WITH_BACKREF(IfcGeom::TriangulationElement)
|
||||
GEOMETRY_WITH_BACKREF(IfcGeom::SerializedElement)
|
||||
GEOMETRY_WITH_BACKREF(IfcGeom::BRepElement)
|
||||
|
||||
%include "../ifcgeom/IfcGeomElement.h"
|
||||
%include "../ifcgeom/IfcGeomRepresentation.h"
|
||||
%include "../ifcgeom/Iterator.h"
|
||||
@@ -791,27 +809,9 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
|
||||
};
|
||||
|
||||
%extend IfcGeom::TriangulationElement {
|
||||
%pythoncode %{
|
||||
# Hide the getters with read-only property implementations
|
||||
# Keep the owning element alive while its geometry is referenced (#1124).
|
||||
def _geometry_with_backref(self, _f=geometry):
|
||||
result = _f(self)
|
||||
result._parent = self
|
||||
return result
|
||||
geometry = property(_geometry_with_backref)
|
||||
%}
|
||||
};
|
||||
|
||||
%extend IfcGeom::SerializedElement {
|
||||
%pythoncode %{
|
||||
# Hide the getters with read-only property implementations
|
||||
# Keep the owning element alive while its geometry is referenced (#1124).
|
||||
def _geometry_with_backref(self, _f=geometry):
|
||||
result = _f(self)
|
||||
result._parent = self
|
||||
return result
|
||||
geometry = property(_geometry_with_backref)
|
||||
%}
|
||||
};
|
||||
|
||||
%extend IfcGeom::BRepElement {
|
||||
@@ -834,13 +834,6 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
|
||||
}
|
||||
|
||||
%pythoncode %{
|
||||
# Hide the getters with read-only property implementations
|
||||
# Keep the owning element alive while its geometry is referenced (#1124).
|
||||
def _geometry_with_backref(self, _f=geometry):
|
||||
result = _f(self)
|
||||
result._parent = self
|
||||
return result
|
||||
geometry = property(_geometry_with_backref)
|
||||
volume = property(calc_volume_)
|
||||
surface_area = property(calc_surface_area_)
|
||||
%}
|
||||
|
||||
@@ -163,10 +163,19 @@
|
||||
%ignore MaxOffset;
|
||||
%ignore MaxOffsetDeviation;
|
||||
%ignore ApplyOffset;
|
||||
%ignore SvgRidgeAngleMinDegrees;
|
||||
%ignore SvgValleyAngleMinDegrees;
|
||||
%ignore SvgEmitFlushEdges;
|
||||
%ignore SvgUseEdgeClassification;
|
||||
%ignore SvgRenderCreaseEdges;
|
||||
%ignore SvgRenderSharpEdges;
|
||||
|
||||
%ignore XmlSerializerFactory;
|
||||
%ignore JsonSerializerFactory;
|
||||
|
||||
%ignore filetype;
|
||||
%ignore guess_file_type;
|
||||
|
||||
// Triangulated representation helper struct
|
||||
%ignore EdgeKey;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user