diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b92956a264..1633fa254b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,13 +19,23 @@ jobs:
uses: actions/checkout@v2
with:
submodules: recursive
- -
- name: Update ubuntu
+ - name: Install C++ dependencies
run: |
sudo apt update
- sudo apt-get install git cmake gcc g++ libboost1.67-all-dev python3-all-dev swig libpcre3-dev libxml2-dev
- sudo apt-get install libocct-foundation-dev libocct-modeling-algorithms-dev libocct-modeling-data-dev libocct-ocaf-dev libocct-visualization-dev libocct-data-exchange-dev \
- libhdf5-dev libcgal-dev
+ sudo apt-get install --no-install-recommends \
+ git cmake gcc g++ \
+ libboost-date-time-dev \
+ libboost-filesystem-dev \
+ libboost-iostreams-dev \
+ libboost-program-options-dev \
+ libboost-regex-dev \
+ libboost-system-dev \
+ libboost-thread-dev \
+ python3-all-dev python3-pip \
+ 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 \
+ libhdf5-dev libcgal-dev opencollada-dev
- name: Build
env:
@@ -44,10 +54,18 @@ jobs:
-DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \
-DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \
-DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \
- -DCOLLADA_SUPPORT=Off \
-DLIBXML2_INCLUDE_DIR=/usr/include/libxml2 \
-DLIBXML2_LIBRARIES=/usr/lib/x86_64-linux-gnu/libxml2.so \
+ -DGLTF_SUPPORT=On \
+ -DJSON_INCLUDE_DIR=/usr/include \
+ -DCGAL_INCLUDE_DIR=/usr/include \
+ -DGMP_INCLUDE_DIR=/usr/include \
+ -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
make -j $(nproc)
make install
diff --git a/.gitignore b/.gitignore
index c49cfab341..b1783fad92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,15 @@
/_installed-vs*-x*/
/build/
+# output directories
+/cmake/out/
+/docs/out/
+/src/examples/out/
+/src/ifcmax/out/
+/src/ifcwrap/out/
+/src/qtviewer/out/
+
+
/win/BuildDepsCache*.txt
# IfcExpressParser residue
/src/ifcexpressparser/express_parser.py
@@ -13,6 +22,7 @@ __pycache__
*.py.bak
# Visual Studio Code files
.vscode
+.vs
# PyCharm files
.idea
@@ -57,3 +67,9 @@ htmlcov
# Blender
*.blend1
+
+# Sphinx
+_build/
+
+# IDS Docs
+src/ifcopenshell-python/test/build
diff --git a/.gitmodules b/.gitmodules
index 12fecc6668..ccec1ac990 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -8,9 +8,6 @@
[submodule "src/svgfill"]
path = src/svgfill
url = https://github.com/IfcOpenShell/svgfill
-[submodule "src/ifcopenshell-python/ifcopenshell/Sample-BIM-Files"]
- path = src/ifcopenshell-python/ifcopenshell/Sample-BIM-Files
- url = https://github.com/atomczak/Sample-BIM-Files
[submodule "src/ifcopenshell-python/test/Sample-BIM-Files"]
path = src/ifcopenshell-python/test/Sample-BIM-Files
url = https://github.com/atomczak/Sample-BIM-Files
diff --git a/README.md b/README.md
index 1ef82d777d..c3100e86db 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,14 @@ For more information, see
* [http://ifcopenshell.org](http://ifcopenshell.org)
* [http://academy.ifcopenshell.org](http://academy.ifcopenshell.org)
-[](https://travis-ci.org/IfcOpenShell/IfcOpenShell)
+[](https://travis-ci.com/IfcOpenShell/IfcOpenShell)
+
+[](https://opencollective.com/opensourcebim/)
Prerequisites
-------------
* Git
-* CMake (2.6 or newer)
+* CMake (3.1.3 or newer)
* Windows: [Visual Studio] 2008 to 2019 (2022 not yet supported by dependency CMake) with C++ toolset (or [Visual C++ Build Tools]) or [MSYS2] + MinGW
* *nix: GCC 4.7 or newer, or Clang (any version)
diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index 549575dff1..1fed2f1189 100644
--- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt
@@ -38,7 +38,7 @@ MESSAGE(STATUS "Autodesk 3ds Max SDK found at ${max_sdk}")
set(HAS_MAX TRUE)
endif()
endforeach()
-
+OPTION(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
OPTION(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
OPTION(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
OPTION(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF)
@@ -53,6 +53,7 @@ OPTION(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
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(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)" ON)
if (${HAS_MAX})
OPTION(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
endif()
@@ -66,8 +67,12 @@ IF(NOT CMAKE_BUILD_TYPE)
SET(CMAKE_BUILD_TYPE "Release")
ENDIF()
-# TODO QtViewer is deprecated ATM as it uses the 0.4 API
-# OPTION(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer (requires Qt 4 framework)." OFF)
+if(MSVC AND MSVC_PARALLEL_BUILD)
+ add_definitions("/MP")
+endif()
+
+# QtViewer requires Qt5
+OPTION(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF)
include(GNUInstallDirs)
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND (NOT BUILD_IFCGEOM))
message(STATUS "'IfcGeom' is required with current outputs")
@@ -121,6 +126,7 @@ ENDMACRO()
UNIFY_ENVVARS_AND_CACHE(OCC_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OCC_LIBRARY_DIR)
+if (NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(OPENCOLLADA_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
@@ -130,17 +136,20 @@ UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE)
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARIES)
+endif()
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
+if (NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(CGAL_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(CGAL_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(GMP_LIBRARY_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_INCLUDE_DIR)
UNIFY_ENVVARS_AND_CACHE(MPFR_LIBRARY_DIR)
+endif()
-if (GLTF_SUPPORT AND BUILD_CONVERT)
+if (NOT MINIMAL_BUILD AND GLTF_SUPPORT AND BUILD_CONVERT)
UNIFY_ENVVARS_AND_CACHE(JSON_INCLUDE_DIR)
FIND_FILE(json_hpp "json.hpp" ${JSON_INCLUDE_DIR}/nlohmann)
IF(json_hpp)
@@ -201,10 +210,12 @@ if(USE_MMAP)
add_definitions(-DUSE_MMAP)
endif()
+if (NOT MINIMAL_BUILD)
# libxml2 is required for IFCXML (optional) and SVGFILL (mandatory)
-find_package(LibXml2 REQUIRED)
+ find_package(LibXml2 REQUIRED)
+endif()
-if (IFCXML_SUPPORT)
+if (NOT MINIMAL_BUILD AND IFCXML_SUPPORT)
add_definitions(-DWITH_IFCXML)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_IFCXML)
endif()
@@ -346,7 +357,7 @@ endif()
endif(BUILD_IFCGEOM)
-IF(COLLADA_SUPPORT)
+IF(NOT MINIMAL_BUILD AND COLLADA_SUPPORT)
# Find OpenCOLLADA
IF("${OPENCOLLADA_INCLUDE_DIR}" STREQUAL "")
MESSAGE(STATUS "No OpenCOLLADA include directory specified")
@@ -426,7 +437,7 @@ IF(COLLADA_SUPPORT)
ENDIF()
ENDIF()
-if(HDF5_SUPPORT)
+if(NOT MINIMAL_BUILD AND HDF5_SUPPORT)
IF("${HDF5_INCLUDE_DIR}" STREQUAL "")
MESSAGE(STATUS "No HDF5 include directory specified")
ElSE()
@@ -804,7 +815,7 @@ INSTALL(TARGETS IfcConvert
endif(BUILD_CONVERT)
# IfcGeomServer
-if(BUILD_GEOMSERVER)
+if(NOT MINIMAL_BUILD AND BUILD_GEOMSERVER)
file(GLOB CPP_FILES ../src/ifcgeomserver/*.cpp)
file(GLOB H_FILES ../src/ifcgeomserver/*.h)
@@ -858,7 +869,7 @@ IF(BUILD_DOCUMENTATION)
ADD_SUBDIRECTORY(../docs docs)
ENDIF()
-IF(BUILD_IFCPYTHON)
+IF(NOT MINIMAL_BUILD AND BUILD_IFCPYTHON)
ADD_SUBDIRECTORY(../src/ifcwrap ifcwrap)
ENDIF()
@@ -866,11 +877,12 @@ IF(BUILD_EXAMPLES)
ADD_SUBDIRECTORY(../src/examples examples)
ENDIF()
-IF(BUILD_IFCMAX)
+IF(NOT MINIMAL_BUILD AND BUILD_IFCMAX)
ADD_SUBDIRECTORY(../src/ifcmax ifcmax)
ENDIF()
-
+if (NOT MINIMAL_BUILD)
ADD_SUBDIRECTORY(../src/svgfill svgfill)
+endif()
# CMake installation targets
INSTALL(FILES ${IFCPARSE_H_FILES}
@@ -915,6 +927,9 @@ INSTALL(FILES ${SERIALIZERS_S_FILES}
)
endif()
+IF(BUILD_QTVIEWER)
+ ADD_SUBDIRECTORY(../src/qtviewer qtviewer)
+endif()
list(APPEND CPACK_SOURCE_IGNORE_FILES
.git
diff --git a/nix/build-all.py b/nix/build-all.py
index 3ed99e8382..f26a09b944 100644
--- a/nix/build-all.py
+++ b/nix/build-all.py
@@ -508,7 +508,7 @@ if USE_OCCT and "occ" in targets:
"-DBUILD_MODULE_Draw=0",
"-DBUILD_RELEASE_DISABLE_EXCEPTIONS=Off"
],
- download_url = "https://git.dev.opencascade.org/repos/occt.git",
+ download_url = "https://github.com/Open-Cascade-SAS/OCCT",
download_name = "occt",
download_tool=download_tool_git,
patch=None if OCCT_VERSION >= "7.4" else "./patches/occt/enable-exception-handling.patch",
diff --git a/src/bcf/src/bcf/v3/bcfxml.py b/src/bcf/src/bcf/v3/bcfxml.py
index 429bb2258a..0aa5925bdf 100644
--- a/src/bcf/src/bcf/v3/bcfxml.py
+++ b/src/bcf/src/bcf/v3/bcfxml.py
@@ -646,7 +646,7 @@ class BcfXml:
def get_comments(self, guid):
comments = {}
data = self._read_xml(os.path.join(guid, "markup.bcf"), "markup.xsd")
- if "Comments" not in data["Topics"]:
+ if "Comments" not in data["Topic"]:
return comments
for item in data["Topic"]["Comments"].get("Comment", []):
diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile
index a18b59d45e..fc2c644ea3 100644
--- a/src/blenderbim/Makefile
+++ b/src/blenderbim/Makefile
@@ -115,7 +115,7 @@ endif
cp -r blenderbim/* dist/blenderbim/
# Provides IfcOpenShell Python functionality
- cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-2985bba-$(PLATFORM)64.zip
+ cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.0-cb8cfd7-$(PLATFORM)64.zip
cd dist/working && unzip ifcopenshell-python*
cp -r dist/working/ifcopenshell dist/blenderbim/libs/site/packages/
@@ -534,11 +534,19 @@ test-core:
.PHONY: test-bim
test-bim:
+ifndef MODULE
pytest test/bim
+else
+ pytest test/bim -m "$(MODULE)" ./ --maxfail=1
+endif
.PHONY: test-tool
test-tool:
+ifndef MODULE
pytest test/tool
+else
+ pytest test/tool/test_$(MODULE).py
+endif
.PHONY: qa
qa:
diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py
index da0194dcec..c29a3f9298 100644
--- a/src/blenderbim/blenderbim/bim/__init__.py
+++ b/src/blenderbim/blenderbim/bim/__init__.py
@@ -27,6 +27,7 @@ modules = {
"root": None,
"unit": None,
"model": None,
+ "cad": None,
"georeference": None,
"context": None,
"drawing": None,
@@ -130,7 +131,8 @@ for mod in modules.values():
def on_register(scene):
handler.setDefaultProperties(scene)
- bpy.app.handlers.depsgraph_update_post.remove(on_register)
+ if not bpy.app.background:
+ bpy.app.handlers.depsgraph_update_post.remove(on_register)
def register():
diff --git a/src/blenderbim/blenderbim/bim/data/pset/EPset_Annotation.ifc b/src/blenderbim/blenderbim/bim/data/pset/EPset_Annotation.ifc
new file mode 100644
index 0000000000..78742d16b2
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/data/pset/EPset_Annotation.ifc
@@ -0,0 +1,13 @@
+ISO-10303-21;
+HEADER;
+FILE_DESCRIPTION((),'2;1');
+FILE_NAME('EPset_Annotation.ifc','2020-01-01T00:00:00',(),(),'EPset_Annotation','EPset_Annotation',$);
+FILE_SCHEMA(('IFC4'));
+ENDSEC;
+DATA;
+#1=IFCPROPERTYSETTEMPLATE('3VuPUwdCD2Qx3XDDRs0R1N',$,'EPset_Annotation','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3));
+#2=IFCSIMPLEPROPERTYTEMPLATE('2P7JN79n96Q9pElZ83LKe4',$,'ZIndex','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.);
+#3=IFCSIMPLEPROPERTYTEMPLATE('1Wpx_r2xj1_9w5JpI0QRJy',$,'Symbol','',.P_ENUMERATEDVALUE.,'IfcLabel',$,#4,$,$,$,.READWRITE.);
+#4=IFCPROPERTYENUMERATION('Symbol',(IFCLABEL('rectangle-tag'), IFCLABEL('door-tag')),$);
+ENDSEC;
+END-ISO-10303-21;
diff --git a/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc b/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc
index 49a45176d1..3883e8ef5f 100644
--- a/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc
+++ b/src/blenderbim/blenderbim/bim/data/pset/EPset_Drawing.ifc
@@ -5,12 +5,20 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',(),(),'EPset_Drawing','EPset
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
-#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7));
+#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation',(#2,#3,#4,#5,#6,#7,#8,#9,#10,#11,#12,#13,#14,#15));
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
#5=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#6=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
+#8=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
+#9=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
+#10=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
+#11=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
+#12=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
+#13=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
+#14=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
+#15=IFCSIMPLEPROPERTYTEMPLATE('2sHDBuW7P4TROy$hL2w7ct',$,'Patterns','',.P_SINGLEVALUE.,'IfcURIReference',$,$,$,$,$,.READWRITE.);
ENDSEC;
END-ISO-10303-21;
diff --git a/src/blenderbim/blenderbim/bim/data/styles/default.css b/src/blenderbim/blenderbim/bim/data/styles/default.css
index 2a5aadf779..e87a177590 100644
--- a/src/blenderbim/blenderbim/bim/data/styles/default.css
+++ b/src/blenderbim/blenderbim/bim/data/styles/default.css
@@ -23,21 +23,27 @@ text { fill: black; stroke: none; }
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
-.solid {}
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
-.PredefinedType-MISC { fill: red; stroke: blue; stroke-width: 1; }
+.PredefinedType-LINEWORK { stroke: black; stroke-width: 0.25; }
+.PredefinedType-BACKGROUND { stroke: black; stroke-width: 0.18; }
.PredefinedType-GRID { marker-start: url(#grid-marker); marker-end: url(#grid-marker); }
+.PredefinedType-SECTION { stroke-dasharray: 12.5, 3, 3, 3; }
.PredefinedType-SECTIONLEVEL { marker-start: url(#section-level-marker); stroke-dasharray: 12.5, 3, 3, 3; }
.PredefinedType-PLANLEVEL { marker-end: url(#plan-level-marker); }
.PredefinedType-DIMENSION { marker-start: url(#dimension-marker-start); marker-end: url(#dimension-marker-end); }
+.PredefinedType-ANGLE { marker-start: url(#angle-marker-start); marker-end: url(#angle-marker-end); }
+.PredefinedType-RADIUS { marker-end: url(#radius-marker-end); }
+.PredefinedType-DIAMETER { marker-start: url(#diameter-marker-start); marker-end: url(#diameter-marker-end); }
.PredefinedType-HIDDENLINE { stroke-dasharray: 3, 2; }
-.PredefinedType-STAIR { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
+.PredefinedType-STAIRARROW { marker-start: url(#stair-marker-start); marker-end: url(#stair-marker-end); }
.PredefinedType-BOUNDARY { fill: none; stroke: red; stroke-width: 1; stroke-dasharray: 12, 4, 3, 4, 3, 4; }
+.PredefinedType-BATTING { marker-start: url(#batting); stroke: none; }
+.PredefinedType-SEALANT { fill: url(#crosshatch1); stroke-width: 0.25; }
+.PredefinedType-FILLAREA { fill: white; stroke: none; }
+.PredefinedType-BREAKLINE { fill: none; stroke: black; stroke-width: 0.25; marker-mid: url(#breakline-marker); }
.PredefinedType-TEXT { fill: black; stroke: none; }
path.PredefinedType-TEXTLEADER { marker-end: url(#leader-marker); }
text.PredefinedType-TEXTLEADER { fill: black; stroke: none; }
-.break { fill: white; }
-.breakline { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; marker-mid: url(#breakline-marker); }
.material-blank { fill: white; }
.material-diagonal1 { fill: url(#diagonal1); }
.material-diagonal2 { fill: url(#diagonal2); }
@@ -54,3 +60,8 @@ text.PredefinedType-TEXTLEADER { fill: black; stroke: none; }
.material-sand { fill: url(#sand); }
.material-concrete { fill: url(#concrete); stroke-width: 0.5; }
.IfcSpace { fill: none; stroke: none; }
+.PredefinedType-STUD { stroke: black; stroke-width: 1; }
+.PredefinedType-WOOD { fill: url(#wood); stroke: black; stroke-width: 0.5; }
+.PredefinedType-STEEL { fill: url(#steel); stroke: black; stroke-width: 0.5; }
+.PredefinedType-CONCRETE { fill: url(#concrete); stroke: black; stroke-width: 0.5; }
+.PredefinedType-PLASTERBOARD { fill: url(#sand); stroke: black; stroke-width: 0.25; }
diff --git a/src/blenderbim/blenderbim/bim/data/templates/markers.svg b/src/blenderbim/blenderbim/bim/data/templates/markers.svg
index 3ba0be893a..87ba553db2 100644
--- a/src/blenderbim/blenderbim/bim/data/templates/markers.svg
+++ b/src/blenderbim/blenderbim/bim/data/templates/markers.svg
@@ -1,5 +1,9 @@
", ""))
if underlay:
with open(underlay) as infile:
- for line in infile:
- if "" in line:
+ for i, line in enumerate(infile):
+ if i < 2:
+ continue
+ elif "" in line:
continue
outfile.write(line)
shutil.copyfile(underlay[0:-4] + ".png", svg_path[0:-4] + "-underlay.png")
- has_boilerplate = True
if linework:
with open(linework) as infile:
should_skip = False
for i, line in enumerate(infile):
- if has_boilerplate and i == 0:
+ if i == 0:
continue
if "" in line:
continue
@@ -174,11 +197,10 @@ class CreateDrawing(bpy.types.Operator):
elif should_skip:
continue
outfile.write(line)
- has_boilerplate = True
if annotation:
with open(annotation) as infile:
for i, line in enumerate(infile):
- if has_boilerplate and i in [0, 1]:
+ if i < 2:
continue
if "" in line:
continue
@@ -191,9 +213,7 @@ class CreateDrawing(bpy.types.Operator):
return
svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-underlay.svg")
context.scene.render.filepath = svg_path[0:-4] + ".png"
- drawing_style = context.scene.DocProperties.drawing_styles[
- self.cprops.active_drawing_style_index
- ]
+ drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True)
@@ -225,25 +245,7 @@ class CreateDrawing(bpy.types.Operator):
for name, value in previous_visibility.items():
bpy.data.objects[name].hide_set(value)
- svg_writer = svgwriter.SvgWriter()
- render = context.scene.render
- if self.is_landscape(render):
- width = self.camera.data.ortho_scale
- height = width / render.resolution_x * render.resolution_y
- else:
- height = self.camera.data.ortho_scale
- width = height / render.resolution_y * render.resolution_x
- svg_writer.human_scale = self.human_scale
- svg_writer.scale = self.scale
- svg_writer.output = svg_path
- svg_writer.data_dir = context.scene.BIMProperties.data_dir
- svg_writer.vector_style = drawing_style.vector_style
- svg_writer.camera = self.camera
- svg_writer.camera_width = width
- svg_writer.camera_height = height
- svg_writer.camera_projection = tuple(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
- svg_writer.background_image = context.scene.render.filepath
- svg_writer.write("underlay")
+ self.svg_writer.create_blank_svg(svg_path).draw_underlay(context.scene.render.filepath).save()
return svg_path
def generate_linework(self, context):
@@ -304,7 +306,7 @@ class CreateDrawing(bpy.types.Operator):
body_contexts.append(rep_context.id())
continue
- elements = set(ifc.by_type("IfcElement")) - set(ifc.by_type("IfcOpeningElement"))
+ elements = tool.Drawing.get_drawing_elements(self.camera_element)
self.setup_serialiser(ifc)
cache = IfcStore.get_cache()
@@ -417,72 +419,13 @@ class CreateDrawing(bpy.types.Operator):
if os.path.isfile(svg_path) and self.props.should_use_annotation_cache:
return svg_path
- camera = self.camera
- svg_writer = svgwriter.SvgWriter()
+ elements = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(self.camera_element))
+ annotations = sorted(elements, key=lambda a: tool.Drawing.get_annotation_z_index(a))
- drawing_style = context.scene.DocProperties.drawing_styles[
- self.cprops.active_drawing_style_index
- ]
+ self.svg_writer.metadata = tool.Drawing.get_drawing_metadata(self.camera_element)
+ self.svg_writer.create_blank_svg(svg_path).draw_annotations(annotations).save()
- render = context.scene.render
- if self.is_landscape(render):
- width = camera.data.ortho_scale
- height = width / render.resolution_x * render.resolution_y
- else:
- height = camera.data.ortho_scale
- width = height / render.resolution_y * render.resolution_x
-
- svg_writer.human_scale = self.human_scale
- svg_writer.scale = self.scale
- svg_writer.output = svg_path
- svg_writer.data_dir = context.scene.BIMProperties.data_dir
- svg_writer.vector_style = drawing_style.vector_style
- svg_writer.camera = camera
- svg_writer.camera_width = width
- svg_writer.camera_height = height
- svg_writer.camera_projection = tuple(camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)))
-
- for obj in camera.users_collection[0].objects:
- if "IfcAnnotation/" not in obj.name:
- continue
- element = tool.Ifc.get_entity(obj)
- if element.ObjectType == "GRID":
- svg_writer.annotations.setdefault("grid_objs", []).append(obj)
- elif obj.type == "CAMERA":
- continue
- elif element.ObjectType == "TEXT_LEADER":
- svg_writer.annotations.setdefault("leader_objs", []).append(obj)
- elif element.ObjectType == "STAIR_ARROW":
- svg_writer.annotations["stair_obj"] = obj
- elif element.ObjectType == "EQUAL_DIMENSION":
- svg_writer.annotations.setdefault("equal_objs", []).append(obj)
- elif element.ObjectType == "DIMENSION":
- svg_writer.annotations.setdefault("dimension_objs", []).append(obj)
- elif element.ObjectType == "ELEVATION":
- svg_writer.annotations.setdefault("elevation_objs", []).append(obj)
- elif element.ObjectType == "SECTION":
- svg_writer.annotations.setdefault("section_objs", []).append(obj)
- elif element.ObjectType == "BREAKLINE":
- svg_writer.annotations["break_obj"] = obj
- elif element.ObjectType == "HIDDEN_LINE":
- svg_writer.annotations.setdefault("hidden_objs", []).append((obj, obj.data))
- elif element.ObjectType == "SOLID_LINE":
- svg_writer.annotations.setdefault("solid_objs", []).append((obj, obj.data))
- elif element.ObjectType == "PLAN_LEVEL":
- svg_writer.annotations.setdefault("plan_level_objs", []).append(obj)
- elif element.ObjectType == "SECTION_LEVEL":
- svg_writer.annotations["section_level_obj"] = obj
- elif element.ObjectType == "TEXT":
- svg_writer.annotations.setdefault("text_objs", []).append(obj)
- else:
- svg_writer.annotations.setdefault("misc_objs", []).append(obj)
-
- svg_writer.annotations["attributes"] = [a.name for a in drawing_style.attributes]
- # TODO: This was the old 2D annotation box checking system, prepare to deprecate
- svg_writer.annotations["annotation_objs"] = []
-
- svg_writer.write("annotation")
- return svg_writer.output
+ return svg_path
def get_scale(self):
if self.camera.data.BIMCameraProperties.diagram_scale == "CUSTOM":
@@ -594,17 +537,11 @@ class AddDrawingToSheet(bpy.types.Operator):
has_drawing = False
for reference in references:
- new = props.sheets.add()
- new.ifc_definition_id = reference.id()
- new.is_sheet = False
-
if tool.Ifc.get_schema() == "IFC2X3":
- new.identification = reference.ItemReference or "X"
element = [r for r in tool.Ifc.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference][
0
].RelatedObjects[0]
else:
- new.identification = reference.Identification or "X"
element = reference.DocumentRefForObjects[0].RelatedObjects[0]
if element == drawing:
has_drawing = True
@@ -622,7 +559,7 @@ class AddDrawingToSheet(bpy.types.Operator):
tool.Ifc.run("document.assign_document", product=drawing, document=reference)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
- sheet_builder.add_drawing(drawing, sheet)
+ sheet_builder.add_drawing(reference, drawing, sheet)
tool.Drawing.import_sheets()
return {"FINISHED"}
@@ -636,15 +573,18 @@ class RemoveDrawingFromSheet(bpy.types.Operator):
def execute(self, context):
reference = tool.Ifc.get().by_id(self.reference)
- sheet = tool.Drawing.get_reference_sheet(reference)
- drawing = tool.Drawing.get_reference_element(reference)
-
- tool.Ifc.run("document.unassign_document", product=drawing, document=reference)
- tool.Ifc.run("document.remove_reference", reference=reference)
+ sheet = tool.Drawing.get_reference_document(reference)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
- sheet_builder.remove_drawing(drawing, sheet)
+ sheet_builder.remove_drawing(reference, sheet)
+
+ drawing = tool.Drawing.get_reference_element(reference)
+ if drawing:
+ tool.Ifc.run("document.unassign_document", product=drawing, document=reference)
+
+ tool.Ifc.run("document.remove_reference", reference=reference)
+
tool.Drawing.import_sheets()
return {"FINISHED"}
@@ -663,7 +603,11 @@ class CreateSheets(bpy.types.Operator):
props = scene.DocProperties
active_sheet = props.sheets[props.active_sheet_index]
sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
- name = tool.Drawing.get_sheet_filename(sheet)
+
+ if not sheet.is_a("IfcDocumentInformation"):
+ return {"FINISHED"}
+
+ name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = scene.BIMProperties.data_dir
sheet_builder.build(sheet)
@@ -773,68 +717,6 @@ class SelectDocIfcFile(bpy.types.Operator):
return {"RUNNING_MODAL"}
-class GenerateReferences(bpy.types.Operator):
- bl_idname = "bim.generate_references"
- bl_label = "Generate References"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- self.camera = context.scene.camera
- self.filter_potential_references()
- if self.camera.data.BIMCameraProperties.target_view == "PLAN_VIEW":
- self.generate_grids()
- if self.camera.data.BIMCameraProperties.target_view == "ELEVATION_VIEW":
- self.generate_grids()
- self.generate_levels(context)
- if self.camera.data.BIMCameraProperties.target_view == "SECTION_VIEW":
- self.generate_grids()
- self.generate_levels(context)
- return {"FINISHED"}
-
- def filter_potential_references(self):
- for name in ["grids", "levels"]:
- setattr(self, name, [])
- for obj in bpy.data.objects:
- if "IfcGridAxis/" in obj.name:
- self.grids.append(obj)
- if "IfcBuildingStorey/" in obj.name:
- self.levels.append(obj)
-
- def generate_grids(self):
- # TODO
- pass
-
- def generate_levels(self, context):
- if self.camera.data.BIMCameraProperties.raster_x > self.camera.data.BIMCameraProperties.raster_y:
- width = self.camera.data.ortho_scale
- height = (
- width / self.camera.data.BIMCameraProperties.raster_x * self.camera.data.BIMCameraProperties.raster_y
- )
- else:
- height = self.camera.data.ortho_scale
- width = (
- height / self.camera.data.BIMCameraProperties.raster_y * self.camera.data.BIMCameraProperties.raster_x
- )
- level_obj = annotation.Annotator.get_annotation_obj("Section Level", "curve", context)
-
- width_in_mm = width * 1000
- real_world_width_in_mm = width_in_mm * scale
- offset_in_mm = 20
- offset_percentage = offset_in_mm / real_world_width_in_mm
-
- for obj in self.levels:
- projection = self.project_point_onto_camera(obj.location)
- co1 = self.camera.matrix_world @ Vector((width / 2 - (offset_percentage * width), projection[1], -1))
- co2 = self.camera.matrix_world @ Vector((-(width / 2), projection[1], -1))
- annotation.Annotator.add_line_to_annotation(level_obj, context, co1, co2)
-
- def project_point_onto_camera(self, point):
- projection = self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
- return self.camera.matrix_world.inverted() @ geometry.intersect_line_plane(
- point.xyz, point.xyz - projection, self.camera.location, projection
- )
-
-
class ResizeText(bpy.types.Operator):
bl_idname = "bim.resize_text"
bl_label = "Resize Text"
@@ -1014,63 +896,56 @@ class RemoveSheet(bpy.types.Operator, Operator):
core.remove_sheet(tool.Ifc, tool.Drawing, sheet=tool.Ifc.get().by_id(self.sheet))
-class AddSchedule(bpy.types.Operator):
+class AddSchedule(bpy.types.Operator, Operator):
bl_idname = "bim.add_schedule"
bl_label = "Add Schedule"
bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- new = context.scene.DocProperties.schedules.add()
- new.name = "SCHEDULE {}".format(len(context.scene.DocProperties.schedules))
- return {"FINISHED"}
-
-
-class RemoveSchedule(bpy.types.Operator):
- bl_idname = "bim.remove_schedule"
- bl_label = "Remove Schedule"
- bl_options = {"REGISTER", "UNDO"}
- index: bpy.props.IntProperty()
-
- def execute(self, context):
- props = context.scene.DocProperties
- props.schedules.remove(self.index)
- return {"FINISHED"}
-
-
-class SelectScheduleFile(bpy.types.Operator):
- bl_idname = "bim.select_schedule_file"
- bl_label = "Select Documentation IFC File"
- bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
- filter_glob: bpy.props.StringProperty(default="*.ods", options={"HIDDEN"})
- index: bpy.props.IntProperty()
+ filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
+ use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
- def execute(self, context):
- props = context.scene.DocProperties
- props.active_schedule.file = self.filepath
- return {"FINISHED"}
+ def _execute(self, context):
+ filepath = self.filepath
+ if self.use_relative_path:
+ filepath = os.path.relpath(filepath, bpy.path.abspath("//"))
+ core.add_schedule(
+ tool.Ifc,
+ tool.Drawing,
+ uri=filepath,
+ )
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
-class BuildSchedule(bpy.types.Operator):
+class RemoveSchedule(bpy.types.Operator, Operator):
+ bl_idname = "bim.remove_schedule"
+ bl_label = "Remove Schedule"
+ bl_options = {"REGISTER", "UNDO"}
+ schedule: bpy.props.IntProperty()
+
+ def _execute(self, context):
+ core.remove_schedule(tool.Ifc, tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
+
+
+class OpenSchedule(bpy.types.Operator, Operator):
+ bl_idname = "bim.open_schedule"
+ bl_label = "Open Schedule"
+ bl_options = {"REGISTER", "UNDO"}
+ schedule: bpy.props.IntProperty()
+
+ def _execute(self, context):
+ core.open_schedule(tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
+
+
+class BuildSchedule(bpy.types.Operator, Operator):
bl_idname = "bim.build_schedule"
bl_label = "Build Schedule"
+ schedule: bpy.props.IntProperty()
- @classmethod
- def poll(cls, context):
- return context.scene.DocProperties.active_schedule.file
-
- def execute(self, context):
- props = context.scene.DocProperties
- schedule = props.active_schedule
- schedule_creator = scheduler.Scheduler()
- outfile = os.path.join(context.scene.BIMProperties.data_dir, "schedules", schedule.name + ".svg")
- schedule_creator.schedule(schedule.file, outfile)
- open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, outfile)
- return {"FINISHED"}
+ def _execute(self, context):
+ core.build_schedule(tool.Drawing, schedule=tool.Ifc.get().by_id(self.schedule))
class AddScheduleToSheet(bpy.types.Operator):
@@ -1086,9 +961,40 @@ class AddScheduleToSheet(bpy.types.Operator):
def execute(self, context):
props = context.scene.DocProperties
+ active_schedule = props.schedules[props.active_schedule_index]
+ active_sheet = props.sheets[props.active_sheet_index]
+ schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
+ sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id)
+ if not sheet.is_a("IfcDocumentInformation"):
+ return {"FINISHED"}
+
+ if tool.Ifc.get_schema() == "IFC2X3":
+ references = sheet.DocumentReferences or []
+ else:
+ references = sheet.HasDocumentReferences or []
+
+ has_schedule = False
+ for reference in references:
+ if reference.Location == tool.Drawing.get_schedule_location(schedule):
+ has_schedule = True
+ break
+
+ if has_schedule:
+ return {"FINISHED"}
+
+ reference = tool.Ifc.run("document.add_reference", information=sheet)
+ if tool.Ifc.get_schema() == "IFC2X3":
+ attributes = {"ItemReference": str(len(sheet.DocumentReferences or []))}
+ else:
+ attributes = {"Identification": str(len(sheet.HasDocumentReferences or []))}
+ attributes["Location"] = tool.Drawing.get_schedule_location(schedule)
+ tool.Ifc.run("document.edit_reference", reference=reference, attributes=attributes)
+
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = context.scene.BIMProperties.data_dir
- sheet_builder.add_schedule(props.active_schedule.name, props.active_sheet.name)
+ sheet_builder.add_schedule(reference, schedule, sheet)
+
+ tool.Drawing.import_sheets()
return {"FINISHED"}
@@ -1322,34 +1228,34 @@ class DisableEditingText(bpy.types.Operator, Operator):
core.disable_editing_text(tool.Drawing, obj=context.active_object)
-class EditTextProduct(bpy.types.Operator, Operator):
- bl_idname = "bim.edit_text_product"
+class EditAssignedProduct(bpy.types.Operator, Operator):
+ bl_idname = "bim.edit_assigned_product"
bl_label = "Edit Text Product"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
product = None
- if context.active_object.BIMTextProperties.relating_product:
- product = tool.Ifc.get_entity(context.active_object.BIMTextProperties.relating_product)
- core.edit_text_product(tool.Ifc, tool.Drawing, obj=context.active_object, product=product)
+ if context.active_object.BIMAssignedProductProperties.relating_product:
+ product = tool.Ifc.get_entity(context.active_object.BIMAssignedProductProperties.relating_product)
+ core.edit_assigned_product(tool.Ifc, tool.Drawing, obj=context.active_object, product=product)
-class EnableEditingTextProduct(bpy.types.Operator, Operator):
- bl_idname = "bim.enable_editing_text_product"
+class EnableEditingAssignedProduct(bpy.types.Operator, Operator):
+ bl_idname = "bim.enable_editing_assigned_product"
bl_label = "Enable Editing Text Product"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- core.enable_editing_text_product(tool.Drawing, obj=context.active_object)
+ core.enable_editing_assigned_product(tool.Drawing, obj=context.active_object)
-class DisableEditingTextProduct(bpy.types.Operator, Operator):
- bl_idname = "bim.disable_editing_text_product"
+class DisableEditingAssignedProduct(bpy.types.Operator, Operator):
+ bl_idname = "bim.disable_editing_assigned_product"
bl_label = "Disable Editing Text Product"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- core.disable_editing_text_product(tool.Drawing, obj=context.active_object)
+ core.disable_editing_assigned_product(tool.Drawing, obj=context.active_object)
class LoadSheets(bpy.types.Operator, Operator):
@@ -1370,6 +1276,24 @@ class DisableEditingSheets(bpy.types.Operator, Operator):
core.disable_editing_sheets(tool.Drawing)
+class LoadSchedules(bpy.types.Operator, Operator):
+ bl_idname = "bim.load_schedules"
+ bl_label = "Load Schedules"
+ bl_options = {"REGISTER", "UNDO"}
+
+ def _execute(self, context):
+ core.load_schedules(tool.Drawing)
+
+
+class DisableEditingSchedules(bpy.types.Operator, Operator):
+ bl_idname = "bim.disable_editing_schedules"
+ bl_label = "Disable Editing Text Product"
+ bl_options = {"REGISTER", "UNDO"}
+
+ def _execute(self, context):
+ core.disable_editing_schedules(tool.Drawing)
+
+
class LoadDrawings(bpy.types.Operator, Operator):
bl_idname = "bim.load_drawings"
bl_label = "Load Drawings"
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py
index a959f4e7a5..2016f23dd2 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py
@@ -99,7 +99,8 @@ def get_diagram_scales(self, context):
diagram_scales_enum = [
("CUSTOM", "Custom", ""),
("1'=1'-0\"|1/1", "1'=1'-0\"", ""),
- ('6"=1\'-0"|1/6', '6"=1\'-0"', ""),
+ ('6"=1\'-0"|1/2', '6"=1\'-0"', ""),
+ ('3"=1\'-0"|1/4', '3"=1\'-0"', ""),
('1-1/2"=1\'-0"|1/8', '1-1/2"=1\'-0"', ""),
('1"=1\'-0"|1/12', '1"=1\'-0"', ""),
('3/4"=1\'-0"|1/16', '3/4"=1\'-0"', ""),
@@ -153,6 +154,11 @@ def update_drawing_name(self, context):
core.update_drawing_name(tool.Ifc, tool.Drawing, drawing=drawing, name=self.name)
+def update_schedule_name(self, context):
+ schedule = tool.Ifc.get().by_id(self.ifc_definition_id)
+ core.update_schedule_name(tool.Ifc, tool.Drawing, schedule=schedule, name=self.name)
+
+
def update_has_underlay(self, context):
update_layer(self, context, "HasUnderlay", self.has_underlay)
@@ -231,8 +237,9 @@ class Drawing(PropertyGroup):
class Schedule(PropertyGroup):
- name: StringProperty(name="Name")
- file: StringProperty(name="File")
+ ifc_definition_id: IntProperty(name="IFC Definition ID")
+ name: StringProperty(name="Name", update=update_schedule_name)
+ identification: StringProperty(name="Identification")
class Sheet(PropertyGroup):
@@ -305,6 +312,7 @@ class DocProperties(PropertyGroup):
should_use_annotation_cache: BoolProperty(name="Use Annotation Cache", default=False)
should_extract: BoolProperty(name="Should Extract", default=True)
is_editing_drawings: BoolProperty(name="Is Editing Drawings", default=False)
+ is_editing_schedules: BoolProperty(name="Is Editing Schedules", default=False)
target_view: EnumProperty(
items=[
("PLAN_VIEW", "Plan", ""),
@@ -335,10 +343,6 @@ class DocProperties(PropertyGroup):
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
- @property
- def active_schedule(self):
- return self.schedules[self.active_schedule_index]
-
class BIMCameraProperties(PropertyGroup):
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
@@ -351,19 +355,6 @@ class BIMCameraProperties(PropertyGroup):
raster_x: IntProperty(name="Raster X", default=1000)
raster_y: IntProperty(name="Raster Y", default=1000)
is_nts: BoolProperty(name="Is NTS")
- cut_objects: EnumProperty(
- items=[
- (
- ".IfcWall|.IfcSlab|.IfcCurtainWall|.IfcStair|.IfcStairFlight|.IfcColumn|.IfcBeam|.IfcMember|.IfcCovering|.IfcSpace",
- "Overall Plan / Section",
- "",
- ),
- (".IfcElement", "Detail Drawing", ""),
- ("CUSTOM", "Custom", ""),
- ],
- name="Cut Objects",
- )
- cut_objects_custom: StringProperty(name="Custom Cut")
active_drawing_style_index: IntProperty(name="Active Drawing Style Index")
# For now, this JSON dump are all the parameters that determine a camera's "Block representation"
@@ -386,7 +377,6 @@ class BIMCameraProperties(PropertyGroup):
class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
- is_editing_product: BoolProperty(name="Is Editing Product", default=False)
attributes: CollectionProperty(name="Attributes", type=Attribute)
value: StringProperty(name="Value", default="TEXT")
font_size: EnumProperty(
@@ -401,13 +391,7 @@ class BIMTextProperties(PropertyGroup):
update=refreshFontSize,
name="Font Size",
)
- symbol: EnumProperty(
- items=[
- ("None", "None", ""),
- ("rectangle-tag", "Rectangle Tag", ""),
- ("door-tag", "Door Tag", ""),
- ],
- update=refreshFontSize,
- name="Symbol",
- )
+
+class BIMAssignedProductProperties(PropertyGroup):
+ is_editing_product: BoolProperty(name="Is Editing Product", default=False)
relating_product: PointerProperty(name="Relating Product", type=bpy.types.Object)
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py
index 2a8229b660..7855e6067c 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py
@@ -31,8 +31,7 @@ class SheetBuilder:
self.data_dir = None
self.scale = "NTS"
- def create(self, name, titleblock_name):
- sheet_path = os.path.join(self.data_dir, "sheets", f"{name}.svg")
+ def create(self, sheet_path, titleblock_name):
root = ET.Element("svg")
root.attrib["xmlns"] = "http://www.w3.org/2000/svg"
root.attrib["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
@@ -60,9 +59,9 @@ class SheetBuilder:
with open(sheet_path, "w") as f:
f.write(minidom.parseString(ET.tostring(root)).toprettyxml(indent=" "))
- def add_drawing(self, drawing, sheet):
+ def add_drawing(self, reference, drawing, sheet):
filename = drawing.Name
- sheet_name = tool.Drawing.get_sheet_filename(sheet)
+ sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
sheet_dir = os.path.join(self.data_dir, "sheets")
drawing_dir = os.path.join(self.data_dir, "diagrams")
sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
@@ -86,7 +85,8 @@ class SheetBuilder:
# here to accommodate browsers which do not nest images.
view = ET.SubElement(sheet_root, "g")
view.attrib["data-type"] = "drawing"
- view.attrib["data-guid"] = drawing.GlobalId
+ view.attrib["data-id"] = str(reference.id())
+ view.attrib["data-drawing"] = drawing.GlobalId
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
@@ -111,26 +111,24 @@ class SheetBuilder:
self.add_view_title(30, view_height + 35, view)
sheet_tree.write(sheet_path)
- def remove_drawing(self, drawing, sheet):
- sheet_name = tool.Drawing.get_sheet_filename(sheet)
- sheet_dir = os.path.join(self.data_dir, "sheets")
- sheet_path = os.path.join(sheet_dir, sheet_name + ".svg")
-
+ def remove_drawing(self, reference, sheet):
ET.register_namespace("", "http://www.w3.org/2000/svg")
+ sheet_path = tool.Drawing.get_document_uri(sheet)
sheet_tree = ET.parse(sheet_path)
sheet_root = sheet_tree.getroot()
- for g in sheet_root.findall("{http://www.w3.org/2000/svg}g"):
- if g.attrib["data-type"] == "drawing" and g.attrib["data-guid"] == drawing.GlobalId:
+ for g in sheet_root.findall('{http://www.w3.org/2000/svg}g'):
+ if g.attrib.get("data-id") == str(reference.id()):
sheet_root.remove(g)
break
sheet_tree.write(sheet_path)
- def add_schedule(self, schedule_name, sheet_name):
- sheet_path = os.path.join(self.data_dir, "sheets", sheet_name + ".svg")
- view_path = os.path.join(self.data_dir, "schedules", schedule_name + ".svg")
+ def add_schedule(self, reference, schedule, sheet):
+ view_path = tool.Drawing.get_document_uri(schedule)
+ schedule_name = os.path.splitext(os.path.basename(view_path))[0]
+ sheet_path = tool.Drawing.get_document_uri(sheet)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
@@ -143,17 +141,20 @@ class SheetBuilder:
view_width = self.convert_to_mm(view_root.attrib.get("width"))
view_height = self.convert_to_mm(view_root.attrib.get("height"))
- group = ET.SubElement(sheet_root, "g")
- group.attrib["data-type"] = "schedule"
+ view = ET.SubElement(sheet_root, "g")
+ view.attrib["data-type"] = "schedule"
+ view.attrib["data-id"] = str(reference.id())
+ view.attrib["data-schedule"] = str(schedule.id())
- foreground = ET.SubElement(group, "image")
+ foreground = ET.SubElement(view, "image")
+ foreground.attrib["data-type"] = "table"
foreground.attrib["xlink:href"] = "../schedules/{}.svg".format(schedule_name)
foreground.attrib["x"] = "30"
foreground.attrib["y"] = "30"
foreground.attrib["width"] = str(view_width)
foreground.attrib["height"] = str(view_height)
- self.add_view_title(30, view_height + 35, group)
+ self.add_view_title(30, view_height + 35, view)
sheet_tree.write(sheet_path)
def add_view_title(self, x, y, parent):
@@ -168,7 +169,7 @@ class SheetBuilder:
title.attrib["height"] = str(self.convert_to_mm(title_root.attrib.get("height")))
def build(self, sheet):
- sheet_name = tool.Drawing.get_sheet_filename(sheet)
+ sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
os.makedirs(os.path.join(self.data_dir, "build", sheet_name), exist_ok=True)
sheet_path = os.path.join(self.data_dir, "sheets", f"{sheet_name}.svg")
@@ -181,7 +182,7 @@ class SheetBuilder:
self.build_titleblock(root, sheet)
self.build_drawings(root, sheet)
- self.build_schedules(root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'))
+ self.build_schedules(root)
with open(os.path.join(self.data_dir, "build", sheet_name, f"{sheet_name}.svg"), "wb") as output:
tree.write(output)
@@ -193,13 +194,11 @@ class SheetBuilder:
titleblock.remove(image)
def build_drawings(self, root, sheet):
- sheet_name = tool.Drawing.get_sheet_filename(sheet)
- references = tool.Drawing.get_document_references(sheet)
- drawing_references = {tool.Drawing.get_reference_element(r): r for r in references}
+ sheet_name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0]
for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="drawing"]'):
- drawing = [d for d in drawing_references.keys() if d and d.GlobalId == view.attrib["data-guid"]][0]
- reference = drawing_references[drawing]
+ reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
+ drawing = tool.Ifc.get().by_id(view.attrib["data-drawing"])
images = view.findall("{http://www.w3.org/2000/svg}image")
@@ -233,24 +232,34 @@ class SheetBuilder:
for image in images:
view.remove(image)
- def build_schedules(self, schedules):
- for group in schedules:
- images = group.findall("{http://www.w3.org/2000/svg}image")
- schedule = images[0]
- group_title = images[1]
- self.scale = "NTS"
+ def build_schedules(self, root):
+ for view in root.findall('{http://www.w3.org/2000/svg}g[@data-type="schedule"]'):
+ reference = tool.Ifc.get().by_id(int(view.attrib["data-id"]))
+ schedule = tool.Ifc.get().by_id(int(view.attrib["data-schedule"]))
- group.append(self.parse_embedded_svg(schedule, {}))
+ images = view.findall("{http://www.w3.org/2000/svg}image")
- path = self.get_href(schedule)
- group.append(
- self.parse_embedded_svg(
- group_title, {"no": 1, "name": ntpath.basename(path)[0:-4], "scale": self.scale}
- )
- )
+ table = None
+ view_title = None
for image in images:
- group.remove(image)
+ if image.attrib["data-type"] == "table":
+ table = image
+ elif image.attrib["data-type"] == "view-title":
+ view_title = image
+
+ if table is not None:
+ view.append(self.parse_embedded_svg(table, {}))
+
+ if view_title is not None:
+ path = self.get_href(table)
+ data = reference.get_info()
+ if not data["Name"]:
+ data["Name"] = schedule.Name or "Unnamed"
+ view.append(self.parse_embedded_svg(view_title, data))
+
+ for image in images:
+ view.remove(image)
def get_href(self, element):
return urllib.parse.unquote(element.attrib.get("{http://www.w3.org/1999/xlink}href"))
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
index b6e3407daf..891b8f2092 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py
@@ -20,7 +20,9 @@ import os
import re
import bpy
import math
+import bmesh
import pystache
+import mathutils
import xml.etree.ElementTree as ET
import svgwrite
import ifcopenshell
@@ -34,14 +36,6 @@ from mathutils import geometry
from blenderbim.bim.ifc import IfcStore
-def load_occ():
- # Don't import until we really need to, as a temporary step before we can purge OCC
- try:
- from OCC.Core import BRep, BRepTools, TopExp, TopAbs
- except ImportError:
- from OCC import BRep, BRepTools, TopExp, TopAbs
-
-
class External(svgwrite.container.Group):
def __init__(self, xml, **extra):
self.xml = xml
@@ -61,257 +55,233 @@ class External(svgwrite.container.Group):
class SvgWriter:
def __init__(self):
- self.output = "out.svg"
self.data_dir = None
- self.vector_style = None
self.human_scale = "NTS"
- self.annotations = {}
- self.background_image = None
+ self.metadata = []
self.scale = 1 / 100 # 1:100
+ self.camera_width = None
+ self.camera_height = None
- def write(self, layer):
+ def create_blank_svg(self, output_path):
self.calculate_scale()
self.svg = svgwrite.Drawing(
- self.output,
- debug=False,
+ output_path,
size=("{}mm".format(self.width), "{}mm".format(self.height)),
viewBox=("0 0 {} {}".format(self.width, self.height)),
id="root",
data_scale=self.human_scale,
+ debug=False, # Disable validation so that we can insert the IFC namespace
)
+ self.svg.attribs["xmlns:ifc"] = "http://www.ifcopenshell.org/ns"
+ return self
- if layer == "underlay":
- self.draw_background_image()
- elif layer == "annotation":
- self.add_stylesheet()
- self.add_markers()
- self.add_symbols()
- self.add_patterns()
- # self.draw_background_elements()
- # self.draw_cut_polygons()
- self.draw_annotations()
+ def save(self):
self.svg.save(pretty=True)
- def calculate_scale(self):
- self.scale *= 1000 # IFC is in meters, SVG is in mm
- self.raw_width = self.camera_width
- self.raw_height = self.camera_height
- self.width = self.raw_width * self.scale
- self.height = self.raw_height * self.scale
-
- def add_stylesheet(self):
- with open(os.path.join(self.data_dir, "styles", f"{self.vector_style}.css"), "r") as stylesheet:
- self.svg.defs.add(self.svg.style(stylesheet.read()))
-
- def add_markers(self):
- tree = ET.parse(os.path.join(self.data_dir, "templates", "markers.svg"))
- root = tree.getroot()
- for child in root:
- self.svg.defs.add(External(child))
-
- def add_symbols(self):
- tree = ET.parse(os.path.join(self.data_dir, "templates", "symbols.svg"))
- root = tree.getroot()
- for child in root:
- self.svg.defs.add(External(child))
-
- def add_patterns(self):
- tree = ET.parse(os.path.join(self.data_dir, "templates", "patterns.svg"))
- root = tree.getroot()
- for child in root:
- self.svg.defs.add(External(child))
-
- def draw_background_image(self):
+ def draw_underlay(self, image):
self.svg.add(
self.svg.image(
- os.path.join("..", "diagrams", os.path.basename(self.background_image)),
- **{"width": self.width, "height": self.height},
+ os.path.join("..", "diagrams", os.path.basename(image)), width=self.width, height=self.height,
)
)
+ return self
- def draw_background_elements(self):
- return # TODO purge?
- for element in self.ifc_cutter.background_elements:
- if element["type"] == "polygon":
- self.draw_polygon(element, "background")
- elif element["type"] == "polyline":
- self.draw_polyline(element, "background")
- elif element["type"] == "line":
- self.draw_line(element, "background")
+ def define_boilerplate(self, stylesheet=None, markers=None, symbols=None, patterns=None):
+ self.add_stylesheet(stylesheet)
+ self.add_markers(markers)
+ self.add_symbols(symbols)
+ self.add_patterns(patterns)
+ return self
- def draw_annotations(self):
- x_offset = self.raw_width / 2
- y_offset = self.raw_height / 2
+ def calculate_scale(self):
+ self.svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
+ self.raw_width = self.camera_width
+ self.raw_height = self.camera_height
+ self.width = self.raw_width * self.svg_scale
+ self.height = self.raw_height * self.svg_scale
- for obj in self.annotations.get("equal_objs", []):
- self.draw_dimension_annotations(obj, text_override="EQ")
- for obj in self.annotations.get("dimension_objs", []):
- self.draw_dimension_annotations(obj)
+ def add_stylesheet(self, uri):
+ default_stylesheet = os.path.join(self.data_dir, "styles", f"default.css")
+ with open(tool.Ifc.resolve_uri(uri) or default_stylesheet, "r") as stylesheet:
+ self.svg.defs.add(self.svg.style(stylesheet.read()))
+
+ def add_markers(self, uri):
+ tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "markers.svg"))
+ root = tree.getroot()
+ for child in root:
+ self.svg.defs.add(External(child))
+
+ def add_symbols(self, uri):
+ tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "symbols.svg"))
+ root = tree.getroot()
+ for child in root:
+ self.svg.defs.add(External(child))
+
+ def add_patterns(self, uri):
+ tree = ET.parse(tool.Ifc.resolve_uri(uri) or os.path.join(self.data_dir, "templates", "patterns.svg"))
+ root = tree.getroot()
+ for child in root:
+ self.svg.defs.add(External(child))
+
+ def draw_annotations(self, annotations):
+ for element in annotations:
+ obj = tool.Ifc.get_object(element)
+ if not obj or element.ObjectType == "DRAWING":
+ continue
+ elif element.ObjectType == "GRID":
+ self.draw_grid_annotation(obj)
+ elif element.ObjectType == "TEXT_LEADER":
+ self.draw_leader_annotation(obj)
+ elif element.ObjectType == "STAIR_ARROW":
+ self.draw_stair_annotation(obj)
+ elif element.ObjectType == "DIMENSION":
+ self.draw_dimension_annotations(obj)
+ elif element.ObjectType == "ANGLE":
+ self.draw_angle_annotations(obj)
+ elif element.ObjectType == "RADIUS":
+ self.draw_radius_annotations(obj)
+ elif element.ObjectType == "DIAMETER":
+ self.draw_diameter_annotations(obj)
+ elif element.ObjectType == "ELEVATION":
+ self.draw_elevation_annotation(obj)
+ elif element.ObjectType == "SECTION":
+ self.draw_section_annotation(obj)
+ elif element.ObjectType == "BREAKLINE":
+ self.draw_break_annotations(obj)
+ elif element.ObjectType == "HIDDEN_LINE":
+ self.draw_line_annotation(obj)
+ elif element.ObjectType == "PLAN_LEVEL":
+ self.draw_plan_level_annotation(obj)
+ elif element.ObjectType == "SECTION_LEVEL":
+ self.draw_section_level_annotation(obj)
+ elif element.ObjectType == "TEXT":
+ self.draw_text_annotation(obj, obj.location)
+ else:
+ self.draw_misc_annotation(obj)
+
+ # Experimental integration with the MeasureIt-ARCH Add-on
self.draw_measureit_arch_dimension_annotations()
+ return self
- if self.annotations.get("break_obj"):
- self.draw_break_annotations(self.annotations["break_obj"])
-
- for grid_obj in self.annotations.get("grid_objs", []):
- matrix_world = grid_obj.matrix_world
- classes = self.get_attribute_classes(grid_obj)
- for edge in grid_obj.data.edges:
- v0_global = matrix_world @ grid_obj.data.vertices[edge.vertices[0]].co.xyz
- v1_global = matrix_world @ grid_obj.data.vertices[edge.vertices[1]].co.xyz
- v0 = self.project_point_onto_camera(v0_global)
- v1 = self.project_point_onto_camera(v1_global)
- start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
- end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
- vector = end - start
- line = self.svg.add(
- self.svg.line(
- start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes)
- )
- )
- line["stroke-dasharray"] = "12.5, 3, 3, 3"
- axis_tag = tool.Ifc.get_entity(grid_obj).Name
- self.svg.add(
- self.svg.text(
- axis_tag,
- insert=tuple(start * self.scale),
- **{
- "font-size": annotation.Annotator.get_svg_text_size(5.0),
- "font-family": "OpenGost Type B TT",
- "text-anchor": "middle",
- "alignment-baseline": "middle",
- "dominant-baseline": "middle",
- },
- )
- )
- self.svg.add(
- self.svg.text(
- axis_tag,
- insert=tuple(end * self.scale),
- **{
- "font-size": annotation.Annotator.get_svg_text_size(5.0),
- "font-family": "OpenGost Type B TT",
- "text-anchor": "middle",
- "alignment-baseline": "middle",
- "dominant-baseline": "middle",
- },
- )
- )
-
- self.draw_ifc_annotation()
-
- for obj in self.annotations.get("misc_objs", []):
- self.draw_misc_annotation(obj)
-
- for obj_data in self.annotations.get("hidden_objs", []):
- self.draw_line_annotation(obj_data)
-
- for obj_data in self.annotations.get("solid_objs", []):
- self.draw_line_annotation(obj_data)
-
- self.draw_leader_annotations()
-
- for obj in self.annotations.get("plan_level_objs", []):
- self.draw_plan_level_annotation(obj)
-
- if self.annotations.get("section_level_obj"):
- matrix_world = self.annotations["section_level_obj"].matrix_world
- classes = self.get_attribute_classes(self.annotations["section_level_obj"])
- for spline in self.annotations["section_level_obj"].data.splines:
- points = self.get_spline_points(spline)
- projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
- d = " ".join(
- [
- "L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
- for p in projected_points
- ]
- )
- d = "M{}".format(d[1:])
- path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
- text_position = Vector(
- (
- (x_offset + projected_points[0].x) * self.scale,
- ((y_offset - projected_points[0].y) * self.scale) - 3.5,
- )
- )
- # TODO: allow metric to be configurable
- rl = (matrix_world @ points[0].co.xyz).z
- if bpy.context.scene.unit_settings.system == "IMPERIAL":
- rl = helper.format_distance(rl)
- else:
- rl = "{:.3f}m".format(rl)
- self.svg.add(
- self.svg.text(
- "RL +{}".format(rl),
- insert=tuple(text_position),
- **{
- "font-size": annotation.Annotator.get_svg_text_size(2.5),
- "font-family": "OpenGost Type B TT",
- "text-anchor": "start",
- "alignment-baseline": "baseline",
- "dominant-baseline": "baseline",
- },
- )
- )
-
- if self.annotations.get("stair_obj"):
- matrix_world = self.annotations["stair_obj"].matrix_world
- classes = self.get_attribute_classes(self.annotations["stair_obj"])
- for spline in self.annotations["stair_obj"].data.splines:
- points = self.get_spline_points(spline)
- projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
- d = " ".join(
- [
- "L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
- for p in projected_points
- ]
- )
- d = "M{}".format(d[1:])
- start = Vector(((x_offset + projected_points[0].x), (y_offset - projected_points[0].y)))
- next_point = Vector(((x_offset + projected_points[1].x), (y_offset - projected_points[1].y)))
- text_position = (start * self.scale) - ((next_point - start).normalized() * 5)
- path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
- self.svg.add(
- self.svg.text(
- "UP",
- insert=tuple(text_position),
- **{
- "font-size": annotation.Annotator.get_svg_text_size(2.5),
- "font-family": "OpenGost Type B TT",
- "text-anchor": "middle",
- "alignment-baseline": "middle",
- "dominant-baseline": "middle",
- },
- )
- )
-
- for obj in self.annotations.get("elevation_objs", []):
- self.draw_elevation_annotation(obj)
-
- for obj in self.annotations.get("section_objs", []):
- self.draw_section_annotation(obj)
-
- for text_obj in self.annotations.get("text_objs", []):
- self.draw_text_annotation(text_obj, text_obj.location)
-
- def draw_ifc_annotation(self):
+ def draw_section_level_annotation(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
- for annotation in self.annotations.get("annotation_objs", []):
- for edge in annotation["edges"]:
- v0_global = annotation["vertices"][edge[0]]
- v1_global = annotation["vertices"][edge[1]]
- v0 = self.project_point_onto_camera(v0_global)
- v1 = self.project_point_onto_camera(v1_global)
- start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
- end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
- vector = end - start
- line = self.svg.add(
- self.svg.line(
- start=tuple(start * self.scale),
- end=tuple(end * self.scale),
- class_=" ".join(annotation["classes"]),
- )
+ matrix_world = obj.matrix_world
+ classes = self.get_attribute_classes(obj)
+ element = tool.Ifc.get_entity(obj)
+ storey = tool.Drawing.get_annotation_element(element)
+ tag = storey.Name if storey else element.Description
+ for spline in obj.data.splines:
+ points = self.get_spline_points(spline)
+ projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
+ d = " ".join(
+ [
+ "L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
+ for p in projected_points
+ ]
+ )
+ d = "M{}".format(d[1:])
+ path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
+ text_position = Vector(
+ (
+ (x_offset + projected_points[0].x) * self.svg_scale,
+ ((y_offset - projected_points[0].y) * self.svg_scale) - 3.5,
)
+ )
+ # TODO: allow metric to be configurable
+ rl = (matrix_world @ points[0].co.xyz).z
+ if bpy.context.scene.unit_settings.system == "IMPERIAL":
+ rl = helper.format_distance(rl)
+ else:
+ rl = "{:.3f}m".format(rl)
+ text_style = {
+ "font-size": annotation.Annotator.get_svg_text_size(2.5),
+ "font-family": "OpenGost Type B TT",
+ "text-anchor": "start",
+ "alignment-baseline": "baseline",
+ "dominant-baseline": "baseline",
+ }
+ self.svg.add(self.svg.text(f"RL +{rl}", insert=tuple(text_position), **text_style))
+ if tag:
+ self.svg.add(self.svg.text(tag, insert=(text_position[0], text_position[1] - 5), **text_style))
+
+ def draw_stair_annotation(self, obj):
+ x_offset = self.raw_width / 2
+ y_offset = self.raw_height / 2
+ matrix_world = obj.matrix_world
+ classes = self.get_attribute_classes(obj)
+ for spline in obj.data.splines:
+ points = self.get_spline_points(spline)
+ projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
+ d = " ".join(
+ [
+ "L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
+ for p in projected_points
+ ]
+ )
+ d = "M{}".format(d[1:])
+ start = Vector(((x_offset + projected_points[0].x), (y_offset - projected_points[0].y)))
+ next_point = Vector(((x_offset + projected_points[1].x), (y_offset - projected_points[1].y)))
+ text_position = (start * self.svg_scale) - ((next_point - start).normalized() * 5)
+ path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
+ self.svg.add(
+ self.svg.text(
+ "UP",
+ insert=tuple(text_position),
+ **{
+ "font-size": annotation.Annotator.get_svg_text_size(2.5),
+ "font-family": "OpenGost Type B TT",
+ "text-anchor": "middle",
+ "alignment-baseline": "middle",
+ "dominant-baseline": "middle",
+ },
+ )
+ )
+
+ def draw_grid_annotation(self, obj):
+ x_offset = self.raw_width / 2
+ y_offset = self.raw_height / 2
+ matrix_world = obj.matrix_world
+ classes = self.get_attribute_classes(obj)
+ for edge in obj.data.edges:
+ v0_global = matrix_world @ obj.data.vertices[edge.vertices[0]].co.xyz
+ v1_global = matrix_world @ obj.data.vertices[edge.vertices[1]].co.xyz
+ v0 = self.project_point_onto_camera(v0_global)
+ v1 = self.project_point_onto_camera(v1_global)
+ start = Vector(((x_offset + v0.x), (y_offset - v0.y)))
+ end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
+ vector = end - start
+ line = self.svg.add(
+ self.svg.line(start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes))
+ )
+ line["stroke-dasharray"] = "12.5, 3, 3, 3"
+ axis_tag = tool.Ifc.get_entity(obj).Name
+ self.svg.add(
+ self.svg.text(
+ axis_tag,
+ insert=tuple(start * self.svg_scale),
+ **{
+ "font-size": annotation.Annotator.get_svg_text_size(5.0),
+ "font-family": "OpenGost Type B TT",
+ "text-anchor": "middle",
+ "alignment-baseline": "middle",
+ "dominant-baseline": "middle",
+ },
+ )
+ )
+ self.svg.add(
+ self.svg.text(
+ axis_tag,
+ insert=tuple(end * self.svg_scale),
+ **{
+ "font-size": annotation.Annotator.get_svg_text_size(5.0),
+ "font-family": "OpenGost Type B TT",
+ "text-anchor": "middle",
+ "alignment-baseline": "middle",
+ "dominant-baseline": "middle",
+ },
+ )
+ )
def draw_misc_annotation(self, obj):
# We have to decide whether this should come from Blender or from IFC.
@@ -332,7 +302,7 @@ class SvgWriter:
projected_points.append(projected_points[0])
d = " ".join(
[
- "L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
+ "L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
@@ -346,65 +316,36 @@ class SvgWriter:
str(ifcopenshell.util.element.get_predefined_type(element))
)
classes = [global_id, element.is_a(), predefined_type]
- for attribute in self.annotations.get("attributes", []):
- result = self.get_obj_value(obj, attribute)
- if result:
- classes.append(
- "{}-{}".format(re.sub("[^0-9a-zA-Z]+", "", attribute), re.sub("[^0-9a-zA-Z]+", "", result))
- )
+ for key in self.metadata:
+ value = ifcopenshell.util.selector.Selector.get_element_value(element, key)
+ if value:
+ classes.append(self.canonicalise_class_name(key) + "-" + self.canonicalise_class_name(str(value)))
return classes
def canonicalise_class_name(self, name):
return re.sub("[^0-9a-zA-Z]+", "", name)
- def get_obj_value(self, obj, key):
- # This is a duplicate implementation of the IFC selector key in Blender
- # In the future if all this becomes purely IFC based this can be deleted
- if "." in key and key.split(".")[0] == "type":
- try:
- obj = obj.BIMObjectProperties.relating_type
- except:
- return
- key = ".".join(key.split(".")[1:])
- result = obj.BIMObjectProperties.attributes.get(key)
- if result:
- return result.string_value
- elif key == "Name":
- return obj.name.split("/")[1]
- elif "." in key:
- pset_name, prop = key.split(".")
- pset = obj.BIMObjectProperties.psets.get(pset_name)
- if not pset:
- pset = obj.BIMObjectProperties.qtos.get(pset_name)
- if not pset:
- return
- result = pset.properties.get(prop)
- if result:
- return result.string_value
-
- def draw_line_annotation(self, obj_data):
+ def draw_line_annotation(self, obj):
# TODO: properly scope these offsets
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
- obj, data = obj_data
-
classes = self.get_attribute_classes(obj)
matrix_world = obj.matrix_world
- if isinstance(data, bpy.types.Curve):
- for spline in data.splines:
+ if isinstance(obj.data, bpy.types.Curve):
+ for spline in obj.data.splines:
points = self.get_spline_points(spline)
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
- "L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
+ "L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
d = "M{}".format(d[1:])
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
- elif isinstance(data, bpy.types.Mesh):
+ elif isinstance(obj.data, bpy.types.Mesh):
self.draw_edge_annotation(obj, classes)
def draw_edge_annotation(self, obj, classes):
@@ -420,25 +361,24 @@ class SvgWriter:
end = Vector(((x_offset + v1.x), (y_offset - v1.y)))
vector = end - start
line = self.svg.add(
- self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
+ self.svg.line(start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes))
)
- def draw_leader_annotations(self):
- for obj in self.annotations.get("leader_objs", []):
- self.draw_line_annotation((obj, obj.data))
- spline = obj.data.splines[0]
- spline_points = spline.bezier_points if spline.bezier_points else spline.points
- if spline_points:
- position = obj.matrix_world @ spline_points[0].co.xyz
- else:
- position = Vector((0, 0, 0))
- self.draw_text_annotation(obj, position)
+ def draw_leader_annotation(self, obj):
+ self.draw_line_annotation(obj)
+ spline = obj.data.splines[0]
+ spline_points = spline.bezier_points if spline.bezier_points else spline.points
+ if spline_points:
+ position = obj.matrix_world @ spline_points[0].co.xyz
+ else:
+ position = Vector((0, 0, 0))
+ self.draw_text_annotation(obj, position)
def draw_section_annotation(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
- self.draw_line_annotation((obj, obj.data))
+ self.draw_line_annotation(obj)
v1 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, 0)))
v2 = self.project_point_onto_camera(obj.matrix_world @ Vector((0, 0, -1)))
@@ -450,17 +390,17 @@ class SvgWriter:
symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y)))
transform = "rotate({}, {}, {})".format(
angle,
- (symbol_position * self.scale)[0],
- (symbol_position * self.scale)[1],
+ (symbol_position * self.svg_scale)[0],
+ (symbol_position * self.svg_scale)[1],
)
self.svg.add(
- self.svg.use("#section-arrow", insert=tuple(symbol_position * self.scale), transform=transform)
+ self.svg.use("#section-arrow", insert=tuple(symbol_position * self.svg_scale), transform=transform)
)
- self.svg.add(self.svg.use("#section-tag", insert=tuple(symbol_position * self.scale)))
+ self.svg.add(self.svg.use("#section-tag", insert=tuple(symbol_position * self.svg_scale)))
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
- text_position = list(symbol_position * self.scale)
+ text_position = list(symbol_position * self.svg_scale)
text_style = {
"font-size": annotation.Annotator.get_svg_text_size(2.5),
"font-family": "OpenGost Type B TT",
@@ -483,15 +423,15 @@ class SvgWriter:
transform = "rotate({}, {}, {})".format(
angle,
- (symbol_position * self.scale)[0],
- (symbol_position * self.scale)[1],
+ (symbol_position * self.svg_scale)[0],
+ (symbol_position * self.svg_scale)[1],
)
- self.svg.add(self.svg.use("#elevation-arrow", insert=tuple(symbol_position * self.scale), transform=transform))
- self.svg.add(self.svg.use("#elevation-tag", insert=tuple(symbol_position * self.scale)))
+ self.svg.add(self.svg.use("#elevation-arrow", insert=tuple(symbol_position * self.svg_scale), transform=transform))
+ self.svg.add(self.svg.use("#elevation-tag", insert=tuple(symbol_position * self.svg_scale)))
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
- text_position = list(symbol_position * self.scale)
+ text_position = list(symbol_position * self.svg_scale)
text_style = {
"font-size": annotation.Annotator.get_svg_text_size(2.5),
"font-family": "OpenGost Type B TT",
@@ -508,7 +448,7 @@ class SvgWriter:
drawing = tool.Drawing.get_annotation_element(element)
reference = tool.Drawing.get_drawing_reference(drawing)
if reference:
- sheet = tool.Drawing.get_reference_sheet(reference)
+ sheet = tool.Drawing.get_reference_document(reference)
if sheet:
if tool.Ifc.get_schema() == "IFC2X3":
reference_id = reference.ItemReference or "-"
@@ -539,14 +479,13 @@ class SvgWriter:
transform = "rotate({}, {}, {})".format(
angle,
- (text_position * self.scale)[0],
- (text_position * self.scale)[1],
+ (text_position * self.svg_scale)[0],
+ (text_position * self.svg_scale)[1],
)
- if text_obj.BIMTextProperties.symbol != "None":
- self.svg.add(
- self.svg.use(f"#{text_obj.BIMTextProperties.symbol}", insert=tuple(text_position * self.scale))
- )
+ symbol = tool.Drawing.get_annotation_symbol(element)
+ if symbol:
+ self.svg.add(self.svg.use(f"#{symbol}", insert=tuple(text_position * self.svg_scale)))
if text_literal.BoxAlignment == "top-left":
alignment_baseline = "hanging"
@@ -578,7 +517,7 @@ class SvgWriter:
literal = text_literal.Literal
- product = tool.Drawing.get_text_product(element)
+ product = tool.Drawing.get_assigned_product(element)
selector = ifcopenshell.util.selector.Selector
variables = {}
for variable in re.findall("{{.*?}}", literal):
@@ -588,7 +527,7 @@ class SvgWriter:
self.svg.add(
self.svg.text(
text_line,
- insert=tuple((text_position * self.scale) + Vector((0, 3.5 * line_number))),
+ insert=tuple((text_position * self.svg_scale) + Vector((0, 3.5 * line_number))),
class_=" ".join(self.get_attribute_classes(text_obj)),
**{
"font-size": annotation.Annotator.get_svg_text_size(text_obj.BIMTextProperties.font_size),
@@ -601,33 +540,28 @@ class SvgWriter:
)
)
- def draw_break_annotations(self, break_obj):
+ def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
- matrix_world = break_obj.matrix_world
- for polygon in break_obj.data.polygons:
- points = [break_obj.data.vertices[v] for v in polygon.vertices]
+ classes = self.get_attribute_classes(obj)
+ matrix_world = obj.matrix_world
+ for edge in obj.data.edges:
+ points = [obj.data.vertices[v] for v in edge.vertices]
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
+ projected_points = [
+ projected_points[0],
+ (projected_points[0] + projected_points[1]) / 2,
+ projected_points[1],
+ ]
d = " ".join(
[
- "L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
+ "L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
d = "M{}".format(d[1:])
- path = self.svg.add(self.svg.path(d=d, class_=" ".join(["break"])))
-
- break_points = [
- projected_points[0],
- ((projected_points[1] - projected_points[0]) / 2) + projected_points[0],
- projected_points[1],
- ]
- d = " ".join(
- ["L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale) for p in break_points]
- )
- d = "M{}".format(d[1:])
- path = self.svg.add(self.svg.path(d=d, class_=" ".join(["breakline"])))
+ path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
def draw_plan_level_annotation(self, obj):
x_offset = self.raw_width / 2
@@ -639,7 +573,7 @@ class SvgWriter:
projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
d = " ".join(
[
- "L {} {}".format((x_offset + p.x) * self.scale, (y_offset - p.y) * self.scale)
+ "L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
for p in projected_points
]
)
@@ -647,8 +581,8 @@ class SvgWriter:
path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
text_position = Vector(
(
- (x_offset + projected_points[0].x) * self.scale,
- ((y_offset - projected_points[0].y) * self.scale) - 2.5,
+ (x_offset + projected_points[0].x) * self.svg_scale,
+ ((y_offset - projected_points[0].y) * self.svg_scale) - 2.5,
)
)
# TODO: allow metric to be configurable
@@ -675,10 +609,180 @@ class SvgWriter:
)
)
- def draw_dimension_annotations(self, dimension_obj, text_override=None):
- classes = self.get_attribute_classes(dimension_obj)
- matrix_world = dimension_obj.matrix_world
- for spline in dimension_obj.data.splines:
+ def draw_angle_annotations(self, obj):
+ # This implementation uses an SVG arc, which means that it can only draw
+ # arcs that are orthogonal to the view (e.g. not arcs in 3D).
+ # Gosh this is bad code :(
+ x_offset = self.raw_width / 2
+ y_offset = self.raw_height / 2
+ classes = self.get_attribute_classes(obj)
+ matrix_world = obj.matrix_world
+
+ points = [v.co for v in obj.data.vertices][:3]
+ center = tool.Cad.get_center_of_arc(points, obj)
+
+ bm = bmesh.new()
+ bm.from_mesh(obj.data)
+ bm.verts.ensure_lookup_table()
+ arc_end_verts = [v for v in bm.verts if len(v.link_edges) == 1]
+ arc_end_pts = [matrix_world @ v.co for v in arc_end_verts]
+
+ # Probably need this when rewriting to use an SVG polyline instead of an arc
+ # arc_path = [arc_end_verts[0]]
+ # while True:
+ # last_point = arc_end_verts[0]
+ # found_another_point = False
+ # for edge in last_point.link_edges:
+ # v = edge.other_vert(last_point)
+ # if v not in arc_path:
+ # found_another_point = True
+ # arc_path.append(v)
+ # if not found_another_point:
+ # break
+
+ distance_between_end_verts = (arc_end_verts[0].co - arc_end_verts[1].co).length
+ arc_mid_vert = arc_end_verts[0].link_edges[0].other_vert(arc_end_verts[0])
+ is_reflex = 0 if (arc_mid_vert.co - arc_end_verts[1].co).length < distance_between_end_verts else 1
+
+ bm.free()
+
+ # Calculate the angle
+ # This is the true normal in 3D, whereas the camera projection we use is the drawing direction.
+ # This assumes (because we use SVG arcs) that the radius is always orthogonal to our view.
+ # When rewriting to use polylines, we should use this normal instead.
+ # normal = mathutils.geometry.normal([arc_end_pts[0], arc_end_pts[1], center])
+ normal = Vector(self.camera_projection)
+ dir1 = (arc_end_pts[0] - center).normalized()
+ dir2 = (arc_end_pts[1] - center).normalized()
+
+ # Let's get the matrix that represents the coordinate system of the arc.
+ # This matrix allows us to get 2D vectors for calculating the signed arc angle.
+ z = normal
+ x = (arc_end_pts[0] - center).normalized()
+ y = z.cross(x)
+ arc_matrix = mathutils.Matrix([x, y, z]).transposed().to_4x4()
+
+ dir1 = ((arc_matrix.inverted() @ arc_end_pts[0]) - (arc_matrix.inverted() @ center)).normalized()
+ dir2 = ((arc_matrix.inverted() @ arc_end_pts[1]) - (arc_matrix.inverted() @ center)).normalized()
+ angle = -dir1.xy.angle_signed(dir2.xy)
+
+ #if is_reflex:
+ # angle = angle % (math.pi * 2)
+
+ # Center of gravity of all vertices, used to help position the text
+ cog = Vector((0, 0, 0))
+ for vert in obj.data.vertices:
+ cog += vert.co
+ cog = matrix_world @ (cog / len(obj.data.vertices))
+
+ radius = ((matrix_world @ obj.data.vertices[0].co) - center).length
+
+ arc_midpoint = center + ((cog - center).normalized() * radius)
+
+ text_position = self.project_point_onto_camera(arc_midpoint)
+ text_position = Vector(((x_offset + text_position.x) * self.svg_scale, (y_offset - text_position.y) * self.svg_scale))
+
+ center_projected = self.project_point_onto_camera(center)
+ center_position = Vector(
+ ((x_offset + center_projected.x) * self.svg_scale, (y_offset - center_projected.y) * self.svg_scale)
+ )
+ text_offset = (text_position - center_position).xy.normalized() * 5
+ text_position += text_offset
+
+ text_style = {
+ "font-size": annotation.Annotator.get_svg_text_size(2.5),
+ "font-family": "OpenGost Type B TT",
+ "text-anchor": "middle",
+ "alignment-baseline": "middle",
+ "dominant-baseline": "middle",
+ }
+
+ angle_text = abs(round(math.degrees(angle), 3))
+ if is_reflex:
+ angle_text = 360 - angle_text
+ self.svg.add(self.svg.text(f"{angle_text}deg", insert=tuple(text_position), **text_style))
+
+ # Draw SVG arc, see for details: http://xahlee.info/js/svg_circle_arc.html
+ arc_proj_end_pts = [self.project_point_onto_camera(v) for v in arc_end_pts]
+ p1 = Vector(((x_offset + arc_proj_end_pts[0].x) * self.svg_scale, (y_offset - arc_proj_end_pts[0].y) * self.svg_scale))
+ p2 = Vector(((x_offset + arc_proj_end_pts[1].x) * self.svg_scale, (y_offset - arc_proj_end_pts[1].y) * self.svg_scale))
+ r = radius * self.svg_scale
+ # reflex = 1 if angle > math.pi else 0
+ reflex = is_reflex
+ if reflex:
+ sense = 0 if angle > 0 else 1
+ else:
+ sense = 1 if angle > 0 else 0
+ d = f"M {p1.x} {p1.y} A {r} {r} 0 {reflex} {sense} {p2.x} {p2.y}"
+ path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
+
+ def draw_radius_annotations(self, obj):
+ x_offset = self.raw_width / 2
+ y_offset = self.raw_height / 2
+ classes = self.get_attribute_classes(obj)
+ element = tool.Ifc.get_entity(obj)
+ matrix_world = obj.matrix_world
+ for spline in obj.data.splines:
+ points = self.get_spline_points(spline)
+ projected_points = [self.project_point_onto_camera(matrix_world @ p.co.xyz) for p in points]
+ d = " ".join(
+ [
+ "L {} {}".format((x_offset + p.x) * self.svg_scale, (y_offset - p.y) * self.svg_scale)
+ for p in projected_points
+ ]
+ )
+ d = "M{}".format(d[1:])
+ path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes)))
+
+ p0 = Vector(
+ ((x_offset + projected_points[0].x) * self.svg_scale, (y_offset - projected_points[0].y) * self.svg_scale)
+ )
+ p1 = Vector(
+ ((x_offset + projected_points[1].x) * self.svg_scale, (y_offset - projected_points[1].y) * self.svg_scale)
+ )
+ text_offset = (p0 - p1).xy.normalized() * 5
+ text_position = projected_points[0]
+ text_position = Vector(
+ ((x_offset + text_position.x) * self.svg_scale, (y_offset - text_position.y) * self.svg_scale)
+ )
+ text_position += text_offset
+
+ text_style = {
+ "font-size": annotation.Annotator.get_svg_text_size(2.5),
+ "font-family": "OpenGost Type B TT",
+ "text-anchor": "middle",
+ "alignment-baseline": "middle",
+ "dominant-baseline": "middle",
+ }
+
+ radius = ((matrix_world @ points[-1].co) - (matrix_world @ points[-2].co)).length
+ radius = helper.format_distance(radius)
+ tag = element.Description or f"R{radius}"
+
+ self.svg.add(self.svg.text(tag, insert=tuple(text_position), **text_style))
+
+ def draw_diameter_annotations(self, obj):
+ classes = self.get_attribute_classes(obj)
+ matrix_world = obj.matrix_world
+ element = tool.Ifc.get_entity(obj)
+ text_override = element.Description
+ for spline in obj.data.splines:
+ points = self.get_spline_points(spline)
+ for i, p in enumerate(points):
+ if i + 1 >= len(points):
+ continue
+ v0_global = matrix_world @ points[i].co.xyz
+ v1_global = matrix_world @ points[i + 1].co.xyz
+ self.draw_dimension_annotation(
+ v0_global, v1_global, classes, text_override, text_format=lambda x: "D" + x
+ )
+
+ def draw_dimension_annotations(self, obj):
+ classes = self.get_attribute_classes(obj)
+ matrix_world = obj.matrix_world
+ element = tool.Ifc.get_entity(obj)
+ text_override = element.Description
+ for spline in obj.data.splines:
points = self.get_spline_points(spline)
for i, p in enumerate(points):
if i + 1 >= len(points):
@@ -699,7 +803,7 @@ class SvgWriter:
Vector(coord[0]), Vector(coord[1]), ["IfcAnnotation", "PredefinedType-DIMENSION"]
)
- def draw_dimension_annotation(self, v0_global, v1_global, classes, text_override=None):
+ def draw_dimension_annotation(self, v0_global, v1_global, classes, text_override=None, text_format=lambda x: x):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
v0 = self.project_point_onto_camera(v0_global)
@@ -711,20 +815,21 @@ class SvgWriter:
perpendicular = Vector((vector.y, -vector.x)).normalized()
dimension = (v1_global - v0_global).length
dimension = helper.format_distance(dimension)
- sheet_dimension = ((end * self.scale) - (start * self.scale)).length
+ sheet_dimension = ((end * self.svg_scale) - (start * self.svg_scale)).length
if sheet_dimension < 5: # annotation can't fit
# offset text to right of marker
- text_position = (end * self.scale) + perpendicular + (3 * vector.normalized())
+ text_position = (end * self.svg_scale) + perpendicular + (3 * vector.normalized())
else:
- text_position = (mid * self.scale) + perpendicular
+ text_position = (mid * self.svg_scale) + perpendicular
rotation = math.degrees(vector.angle_signed(Vector((1, 0))))
line = self.svg.add(
- self.svg.line(start=tuple(start * self.scale), end=tuple(end * self.scale), class_=" ".join(classes))
+ self.svg.line(start=tuple(start * self.svg_scale), end=tuple(end * self.svg_scale), class_=" ".join(classes))
)
if text_override is not None:
text = text_override
else:
text = str(dimension)
+ text = text_format(text)
self.svg.add(
self.svg.text(
text,
@@ -750,35 +855,8 @@ class SvgWriter:
def get_spline_points(self, spline):
return spline.bezier_points if spline.bezier_points else spline.points
- def draw_cut_polygons(self):
- return # deprecate?
- for polygon in self.ifc_cutter.cut_polygons:
- self.draw_polygon(polygon, "cut")
-
- def draw_polyline(self, element, position):
- load_occ()
- classes = self.get_classes(element["raw"], position)
- exp = BRepTools.BRepTools_WireExplorer(element["geometry"])
- points = []
- while exp.More():
- point = BRep.BRep_Tool.Pnt(exp.CurrentVertex())
- points.append((point.X() * self.scale, -point.Y() * self.scale))
- exp.Next()
- self.svg.add(self.svg.polyline(points=points, class_=" ".join(classes)))
-
- def draw_line(self, element, position):
- load_occ()
- classes = self.get_classes(element["raw"], position)
- exp = TopExp.TopExp_Explorer(element["geometry"], TopAbs.TopAbs_VERTEX)
- points = []
- while exp.More():
- point = BRep.BRep_Tool.Pnt(topods.Vertex(exp.Current()))
- points.append((point.X() * self.scale, -point.Y() * self.scale))
- exp.Next()
- self.svg.add(self.svg.line(start=points[0], end=points[1], class_=" ".join(classes)))
-
def draw_polygon(self, polygon, position):
- points = [(p[0] * self.scale, p[1] * self.scale) for p in polygon["points"]]
+ points = [(p[0] * self.svg_scale, p[1] * self.svg_scale) for p in polygon["points"]]
if "classes" in polygon["metadata"]:
classes = " ".join(polygon["metadata"]["classes"])
else:
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py
index e21b9d6006..0b8f719a1b 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py
@@ -20,7 +20,7 @@ import bpy
import blenderbim.bim.helper
import blenderbim.tool as tool
from bpy.types import Panel
-from blenderbim.bim.module.drawing.data import TextData, SheetsData, DrawingsData
+from blenderbim.bim.module.drawing.data import ProductAssignmentsData, TextData, SheetsData, SchedulesData, DrawingsData
class BIM_PT_camera(Panel):
@@ -62,17 +62,9 @@ class BIM_PT_camera(Panel):
row = layout.row()
row.prop(props, "is_nts")
- row = layout.row()
- row.operator("bim.generate_references")
row = layout.row()
row.operator("bim.resize_text")
- row = layout.row()
- row.prop(props, "cut_objects")
- if props.cut_objects == "CUSTOM":
- row = layout.row()
- row.prop(props, "cut_objects_custom")
-
row = layout.row()
row.prop(props, "raster_x")
row = layout.row()
@@ -152,6 +144,10 @@ class BIM_PT_drawings(Panel):
bl_region_type = "UI"
bl_category = "BIM Documentation"
+ @classmethod
+ def poll(cls, context):
+ return tool.Ifc.get()
+
def draw(self, context):
if not DrawingsData.is_loaded:
DrawingsData.load()
@@ -203,23 +199,40 @@ class BIM_PT_schedules(Panel):
bl_region_type = "UI"
bl_category = "BIM Documentation"
+ @classmethod
+ def poll(cls, context):
+ return tool.Ifc.get()
+
def draw(self, context):
- layout = self.layout
- layout.use_property_split = True
- props = context.scene.DocProperties
+ if not SchedulesData.is_loaded:
+ SchedulesData.load()
- row = layout.row(align=True)
- row.operator("bim.add_schedule")
+ self.props = context.scene.DocProperties
- if props.schedules:
- row.operator("bim.build_schedule", icon="LINENUMBERS_ON", text="")
- row.operator("bim.remove_schedule", icon="X", text="").index = props.active_schedule_index
+ if not self.props.is_editing_schedules:
+ row = self.layout.row(align=True)
+ row.label(text=f"{SchedulesData.data['total_schedules']} Schedules Found", icon="LONGDISPLAY")
+ row.operator("bim.load_schedules", text="", icon="IMPORT")
+ return
- layout.template_list("BIM_UL_generic", "", props, "schedules", props, "active_schedule_index")
+ row = self.layout.row(align=True)
+ row.operator("bim.add_schedule", icon="ADD")
+ row.operator("bim.disable_editing_schedules", text="", icon="CANCEL")
- row = layout.row()
- row.prop(props.active_schedule, "file")
- row.operator("bim.select_schedule_file", icon="FILE_FOLDER", text="")
+ if self.props.schedules:
+ if self.props.active_schedule_index < len(self.props.schedules):
+ active_schedule = self.props.schedules[self.props.active_schedule_index]
+ row = self.layout.row(align=True)
+ row.alignment = "RIGHT"
+ row.operator("bim.open_schedule", icon="URL", text="").schedule = active_schedule.ifc_definition_id
+ row.operator(
+ "bim.build_schedule", icon="LINENUMBERS_ON", text=""
+ ).schedule = active_schedule.ifc_definition_id
+ row.operator("bim.remove_schedule", icon="X", text="").schedule = active_schedule.ifc_definition_id
+
+ self.layout.template_list(
+ "BIM_UL_generic", "", self.props, "schedules", self.props, "active_schedule_index"
+ )
class BIM_PT_sheets(Panel):
@@ -229,6 +242,10 @@ class BIM_PT_sheets(Panel):
bl_region_type = "UI"
bl_category = "BIM Documentation"
+ @classmethod
+ def poll(cls, context):
+ return tool.Ifc.get()
+
def draw(self, context):
if not SheetsData.is_loaded:
SheetsData.load()
@@ -263,6 +280,39 @@ class BIM_PT_sheets(Panel):
self.layout.template_list("BIM_UL_sheets", "", self.props, "sheets", self.props, "active_sheet_index")
+class BIM_PT_product_assignments(Panel):
+ bl_label = "IFC Product Assignments"
+ bl_idname = "BIM_PT_product_assignments"
+ bl_space_type = "PROPERTIES"
+ bl_region_type = "WINDOW"
+ bl_context = "object"
+
+ @classmethod
+ def poll(cls, context):
+ if not tool.Ifc.get() or not context.active_object:
+ return
+ element = tool.Ifc.get_entity(context.active_object)
+ if not element:
+ return
+ return element.is_a("IfcAnnotation")
+
+ def draw(self, context):
+ if not ProductAssignmentsData.is_loaded:
+ ProductAssignmentsData.load()
+
+ props = context.active_object.BIMAssignedProductProperties
+
+ if props.is_editing_product:
+ row = self.layout.row(align=True)
+ row.prop(props, "relating_product", text="")
+ row.operator("bim.edit_assigned_product", icon="CHECKMARK", text="")
+ row.operator("bim.disable_editing_assigned_product", icon="CANCEL", text="")
+ else:
+ row = self.layout.row(align=True)
+ row.label(text=ProductAssignmentsData.data["relating_product"] or "No Relating Product", icon="OBJECT_DATA")
+ row.operator("bim.enable_editing_assigned_product", icon="GREASEPENCIL", text="")
+
+
class BIM_PT_text(Panel):
bl_label = "IFC Text"
bl_idname = "BIM_PT_text"
@@ -299,16 +349,6 @@ class BIM_PT_text(Panel):
row.label(text=attribute["name"])
row.label(text=attribute["value"])
- if props.is_editing_product:
- row = self.layout.row(align=True)
- row.prop(props, "relating_product", text="")
- row.operator("bim.edit_text_product", icon="CHECKMARK", text="")
- row.operator("bim.disable_editing_text_product", icon="CANCEL", text="")
- else:
- row = self.layout.row(align=True)
- row.label(text=TextData.data["relating_product"] or "No Relating Product", icon="OBJECT_DATA")
- row.operator("bim.enable_editing_text_product", icon="GREASEPENCIL", text="")
-
row = self.layout.row()
row.prop(props, "font_size")
row = self.layout.row()
@@ -328,11 +368,19 @@ class BIM_PT_annotation_utilities(Panel):
self.props = context.scene.DocProperties
row = layout.row(align=True)
- op = row.operator("bim.add_annotation", text="Dim", icon="ARROW_LEFTRIGHT")
+ op = row.operator("bim.add_annotation", text="Dimension", icon="FIXED_SIZE")
op.object_type = "DIMENSION"
op.data_type = "curve"
- op = row.operator("bim.add_annotation", text="Dim (Eq)", icon="ARROW_LEFTRIGHT")
- op.object_type = "EQUAL_DIMENSION"
+ op = row.operator("bim.add_annotation", text="Angle", icon="DRIVER_ROTATIONAL_DIFFERENCE")
+ op.object_type = "ANGLE"
+ op.data_type = "mesh"
+
+ row = layout.row(align=True)
+ op = row.operator("bim.add_annotation", text="Radius", icon="FORWARD")
+ op.object_type = "RADIUS"
+ op.data_type = "curve"
+ op = row.operator("bim.add_annotation", text="Diameter", icon="ARROW_LEFTRIGHT")
+ op.object_type = "DIAMETER"
op.data_type = "curve"
row = layout.row(align=True)
@@ -363,10 +411,14 @@ class BIM_PT_annotation_utilities(Panel):
op = row.operator("bim.add_annotation", text="Breakline", icon="FCURVE")
op.object_type = "BREAKLINE"
op.data_type = "mesh"
- op = row.operator("bim.add_annotation", text="Misc", icon="MESH_MONKEY")
- op.object_type = "MISC"
+ op = row.operator("bim.add_annotation", text="Line", icon="MESH_MONKEY")
+ op.object_type = "LINEWORK"
op.data_type = "mesh"
+ row = layout.row(align=True)
+ op = row.operator("bim.add_annotation", text="Fill Area", icon="NODE_TEXTURE")
+ op.object_type = "FILL_AREA"
+
row = layout.row(align=True)
row.prop(self.props, "should_draw_decorations", text="Viewport Annotations")
@@ -407,6 +459,8 @@ class BIM_UL_sheets(bpy.types.UIList):
row.label(text="", icon="BLANK1")
if item.reference_type == "DRAWING":
row.label(text="", icon="IMAGE_DATA")
+ elif item.reference_type == "SCHEDULE":
+ row.label(text="", icon="LONGDISPLAY")
name = "{} - {}".format(item.identification or "X", item.name or "Unnamed")
row.label(text=name)
diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py
index 8f8c301913..541cb0a92d 100644
--- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py
@@ -206,25 +206,14 @@ class UpdateRepresentation(bpy.types.Operator):
new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data)
- [
- blenderbim.core.style.add_style(tool.Ifc, tool.Style, obj=s.material)
- for s in obj.material_slots
- if s.material and not s.material.BIMMaterialProperties.ifc_style_id
- ]
-
- if isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons):
+ if tool.Geometry.is_body_representation(new_representation):
+ [tool.Geometry.run_style_add_style(obj=mat) for mat in tool.Geometry.get_object_materials_without_styles(obj)]
ifcopenshell.api.run(
"style.assign_representation_styles",
self.file,
- **{
- "shape_representation": new_representation,
- "styles": [
- self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id)
- for s in obj.material_slots
- if s.material
- ],
- "should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
- },
+ shape_representation=new_representation,
+ styles=tool.Geometry.get_styles(obj),
+ should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
)
tool.Geometry.record_object_materials(obj)
@@ -332,8 +321,8 @@ class CopyRepresentation(bpy.types.Operator, Operator):
class OverrideDeleteTrait:
def delete_ifc_object(self, obj):
- if obj.BIMObjectProperties.ifc_definition_id:
- element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
+ element = tool.Ifc.get_entity(obj)
+ if element:
IfcStore.delete_element(element)
if getattr(element, "FillsVoids", None):
self.remove_filling(element)
@@ -342,9 +331,12 @@ class OverrideDeleteTrait:
self.remove_filling(rel.RelatedBuildingElement)
if element.VoidsElements:
self.delete_opening_element(element)
- elif getattr(element, "HasOpenings", None):
- for rel in element.HasOpenings:
- self.delete_opening_element(rel.RelatedOpeningElement)
+ else:
+ if getattr(element, "HasOpenings", None):
+ for rel in element.HasOpenings:
+ self.delete_opening_element(rel.RelatedOpeningElement)
+ for port in ifcopenshell.util.system.get_ports(element):
+ self.remove_port(port)
def delete_opening_element(self, element):
obj = IfcStore.get_element(element.VoidsElements[0].RelatingBuildingElement.id())
@@ -354,6 +346,9 @@ class OverrideDeleteTrait:
obj = IfcStore.get_element(element.id())
bpy.ops.bim.remove_filling(obj=obj.name)
+ def remove_port(self, port):
+ blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
+
class OverrideDelete(bpy.types.Operator, OverrideDeleteTrait):
bl_idname = "object.delete"
diff --git a/src/blenderbim/blenderbim/bim/module/georeference/data.py b/src/blenderbim/blenderbim/bim/module/georeference/data.py
new file mode 100644
index 0000000000..66df12b93b
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/georeference/data.py
@@ -0,0 +1,123 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of BlenderBIM Add-on.
+#
+# BlenderBIM Add-on is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# BlenderBIM Add-on is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with BlenderBIM Add-on. If not, see .
+
+
+import bpy
+import ifcopenshell.util.geolocation
+import blenderbim.tool as tool
+
+
+def refresh():
+ GeoreferenceData.is_loaded = False
+
+
+class GeoreferenceData:
+ data = {}
+ is_loaded = False
+
+ @classmethod
+ def load(cls):
+ cls.data["blender_derived_angle"] = cls.blender_derived_angle()
+ cls.data["map_conversion"] = cls.map_conversion()
+ cls.data["map_derived_angle"] = cls.map_derived_angle()
+ cls.data["projected_crs"] = cls.projected_crs()
+ cls.data["true_north"] = cls.true_north()
+ cls.data["true_derived_angle"] = cls.true_derived_angle()
+ cls.is_loaded = True
+
+ @classmethod
+ def blender_derived_angle(cls):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ if props.has_blender_offset:
+ return str(
+ round(
+ ifcopenshell.util.geolocation.xaxis2angle(
+ float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
+ ),
+ 3,
+ )
+ )
+
+ @classmethod
+ def map_conversion(cls):
+ if tool.Ifc.get_schema() == "IFC2X3":
+ return {}
+
+ for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
+ if context.HasCoordinateOperation:
+ map_conversion = context.HasCoordinateOperation[0].get_info()
+ del map_conversion["id"]
+ del map_conversion["type"]
+ del map_conversion["SourceCRS"]
+ del map_conversion["TargetCRS"]
+ return map_conversion
+ return {}
+
+ @classmethod
+ def map_derived_angle(cls):
+ if (
+ cls.data["map_conversion"]
+ and cls.data["map_conversion"]["XAxisAbscissa"] is not None
+ and cls.data["map_conversion"]["XAxisOrdinate"] is not None
+ ):
+ return str(
+ round(
+ ifcopenshell.util.geolocation.xaxis2angle(
+ cls.data["map_conversion"]["XAxisAbscissa"], cls.data["map_conversion"]["XAxisOrdinate"]
+ ),
+ 3,
+ )
+ )
+ return ""
+
+ @classmethod
+ def projected_crs(cls):
+ if tool.Ifc.get_schema() == "IFC2X3":
+ return {}
+
+ for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
+ if context.HasCoordinateOperation:
+ projected_crs = context.HasCoordinateOperation[0].TargetCRS.get_info()
+ del projected_crs["id"]
+ del projected_crs["type"]
+ if projected_crs["MapUnit"]:
+ unit = projected_crs["MapUnit"]
+ projected_crs["MapUnit"] = (getattr(unit, "Prefix", "") or "") + unit.Name
+ return projected_crs
+ return {}
+
+ @classmethod
+ def true_north(cls):
+ ifc = tool.Ifc.get()
+ true_north = {}
+
+ if ifc.schema == "IFC2X3":
+ return
+
+ for context in ifc.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
+ if not context.TrueNorth:
+ continue
+ true_north = context.TrueNorth.DirectionRatios
+ break
+
+ return true_north
+
+ @classmethod
+ def true_derived_angle(cls):
+ if cls.data["true_north"]:
+ return str(round(ifcopenshell.util.geolocation.yaxis2angle(*cls.data["true_north"][0:2]), 3))
diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py
index 5b3e09037d..71bde91ba8 100644
--- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py
@@ -17,345 +17,157 @@
# along with BlenderBIM Add-on. If not, see .
import bpy
-import json
-import ifcopenshell
-import ifcopenshell.util.unit
-import ifcopenshell.util.attribute
-import ifcopenshell.api
-import blenderbim.bim.helper
-from blenderbim.bim.ifc import IfcStore
-from ifcopenshell.api.georeference.data import Data
-from ifcopenshell.api.unit.data import Data as UnitData
-from math import radians, degrees, atan, tan, cos, sin
+
+import blenderbim.tool as tool
+import blenderbim.core.georeference as core
-class EnableEditingGeoreferencing(bpy.types.Operator):
- bl_idname = "bim.enable_editing_georeferencing"
- bl_label = "Enable Editing Georeferencing"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- self.file = IfcStore.get_file()
- props = context.scene.BIMGeoreferenceProperties
- self.props = props
-
- props.projected_crs.clear()
-
- blenderbim.bim.helper.import_attributes(
- "IfcProjectedCRS", props.projected_crs, Data.projected_crs, self.import_projected_crs_attributes
- )
-
- props.map_conversion.clear()
- blenderbim.bim.helper.import_attributes(
- "IfcMapConversion", props.map_conversion, Data.map_conversion, self.import_map_conversion_attributes
- )
-
- props.has_true_north = bool(Data.true_north)
- if Data.true_north:
- props.true_north_abscissa = str(Data.true_north[0])
- props.true_north_ordinate = str(Data.true_north[1])
-
- props.is_editing = True
- return {"FINISHED"}
-
- def import_projected_crs_attributes(self, name, prop, data):
- if name == "MapUnit":
- new = self.props.projected_crs.add()
- new.name = name
- new.data_type = "enum"
- new.is_null = data[name] is None
- new.is_optional = True
- new.enum_items = json.dumps(
- {u["id"]: u["Name"] for u in UnitData.units.values() if u["UnitType"] == "LENGTHUNIT"}
- )
- if data["MapUnit"]:
- new.enum_value = str(data["MapUnit"]["id"])
- return True
-
- def import_map_conversion_attributes(self, name, prop, data):
- if name not in ["SourceCRS", "TargetCRS"]:
- # Enforce a string data type to prevent data loss in single-precision Blender props
- prop.data_type = "string"
- prop.string_value = "" if prop.is_null else str(data[name])
- return True
-
-
-class DisableEditingGeoreferencing(bpy.types.Operator):
- bl_idname = "bim.disable_editing_georeferencing"
- bl_label = "Disable Editing Georeferencing"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- props = context.scene.BIMGeoreferenceProperties
- props.is_editing = False
- return {"FINISHED"}
-
-
-class EditGeoreferencing(bpy.types.Operator):
- bl_idname = "bim.edit_georeferencing"
- bl_label = "Edit Georeferencing"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- return IfcStore.execute_ifc_operator(self, context)
-
- def _execute(self, context):
- self.file = IfcStore.get_file()
- props = context.scene.BIMGeoreferenceProperties
-
- projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, self.export_crs_attributes)
- map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, self.export_map_attributes)
-
- true_north = None
- if props.has_true_north:
- try:
- true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)]
- except ValueError:
- self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number")
-
- ifcopenshell.api.run(
- "georeference.edit_georeferencing",
- self.file,
- **{
- "map_conversion": map_conversion,
- "projected_crs": projected_crs,
- "true_north": true_north,
- }
- )
- Data.load(self.file)
- bpy.ops.bim.disable_editing_georeferencing()
- return {"FINISHED"}
-
- def export_map_attributes(self, attributes, prop):
- if not prop.is_null and prop.data_type == "string":
- # We store our floats as string to prevent single precision data loss
- attributes[prop.name] = float(prop.string_value)
- return True
-
- def export_crs_attributes(self, attributes, prop):
- if not prop.is_null and prop.name == "MapUnit":
- attributes[prop.name] = self.file.by_id(int(prop.enum_value))
- return True
-
-
-class SetBlenderGridNorth(bpy.types.Operator):
- bl_idname = "bim.set_blender_grid_north"
- bl_label = "Set Blender Grid North"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- context.scene.sun_pos_properties.north_offset = -radians(
- ifcopenshell.util.geolocation.xaxis2angle(
- float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value),
- float(context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value),
- )
- )
- return {"FINISHED"}
-
-
-class SetIfcGridNorth(bpy.types.Operator):
- bl_idname = "bim.set_ifc_grid_north"
- bl_label = "Set IFC Grid North"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- x_angle = -context.scene.sun_pos_properties.north_offset
- context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle))
- context.scene.BIMGeoreferenceProperties.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle))
- return {"FINISHED"}
-
-
-class SetBlenderTrueNorth(bpy.types.Operator):
- bl_idname = "bim.set_blender_true_north"
- bl_label = "Set Blender True North"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- context.scene.sun_pos_properties.north_offset = -radians(
- ifcopenshell.util.geolocation.yaxis2angle(
- float(context.scene.BIMGeoreferenceProperties.true_north_abscissa),
- float(context.scene.BIMGeoreferenceProperties.true_north_ordinate),
- )
- )
- return {"FINISHED"}
-
-
-class SetIfcTrueNorth(bpy.types.Operator):
- bl_idname = "bim.set_ifc_true_north"
- bl_label = "Set IFC True North"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- y_angle = -context.scene.sun_pos_properties.north_offset + radians(90)
- context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(cos(y_angle))
- context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(sin(y_angle))
- return {"FINISHED"}
-
-
-class RemoveGeoreferencing(bpy.types.Operator):
- bl_idname = "bim.remove_georeferencing"
- bl_label = "Remove Georeferencing"
- bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- return IfcStore.execute_ifc_operator(self, context)
-
- def _execute(self, context):
- ifcopenshell.api.run("georeference.remove_georeferencing", IfcStore.get_file())
- Data.load(IfcStore.get_file())
- return {"FINISHED"}
-
-
-class AddGeoreferencing(bpy.types.Operator):
+class AddGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_georeferencing"
bl_label = "Add Georeferencing"
bl_options = {"REGISTER", "UNDO"}
-
- def execute(self, context):
- return IfcStore.execute_ifc_operator(self, context)
+ bl_description = "Add a new georeference"
def _execute(self, context):
- ifcopenshell.api.run("georeference.add_georeferencing", IfcStore.get_file())
- Data.load(IfcStore.get_file())
- return {"FINISHED"}
+ core.add_georeferencing(tool.Ifc)
-class ConvertLocalToGlobal(bpy.types.Operator):
- bl_idname = "bim.convert_local_to_global"
- bl_label = "Convert Local To Global"
+class EnableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.enable_editing_georeferencing"
+ bl_label = "Enable Editing Georeferencing"
bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Enable editing georeferencing"
- @classmethod
- def poll(cls, context):
- file = IfcStore.get_file()
- props = context.scene.BIMGeoreferenceProperties
- return file and props.coordinate_input.count(",") == 2
-
- def execute(self, context):
- if not Data.is_loaded:
- Data.load(IfcStore.get_file())
- props = context.scene.BIMGeoreferenceProperties
- x, y, z = [float(co) for co in props.coordinate_input.split(",")]
-
- if props.has_blender_offset:
- results = ifcopenshell.util.geolocation.xyz2enh(
- x,
- y,
- z,
- float(props.blender_eastings),
- float(props.blender_northings),
- float(props.blender_orthogonal_height),
- float(props.blender_x_axis_abscissa),
- float(props.blender_x_axis_ordinate),
- 1.0,
- )
- x, y, z = results
-
- # TODO: what if the project CRS units and the project units are different?
-
- if Data.map_conversion:
- results = ifcopenshell.util.geolocation.xyz2enh(
- x,
- y,
- z,
- Data.map_conversion["Eastings"],
- Data.map_conversion["Northings"],
- Data.map_conversion["OrthogonalHeight"],
- Data.map_conversion.get("XAxisAbscissa", 1.0),
- Data.map_conversion.get("XAxisOrdinate", 0.0),
- Data.map_conversion.get("Scale", 1.0),
- )
- else:
- results = (x, y, z)
-
- props.coordinate_output = ",".join([str(r) for r in results])
- context.scene.cursor.location = results
- return {"FINISHED"}
+ def _execute(self, context):
+ core.enable_editing_georeferencing(tool.Georeference)
-class ConvertGlobalToLocal(bpy.types.Operator):
- bl_idname = "bim.convert_global_to_local"
- bl_label = "Convert Global To Local"
+class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.remove_georeferencing"
+ bl_label = "Remove Georeferencing"
bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Remove the georeferencing"
- @classmethod
- def poll(cls, context):
- file = IfcStore.get_file()
- props = context.scene.BIMGeoreferenceProperties
- return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2
-
- def execute(self, context):
- if not Data.is_loaded:
- Data.load(IfcStore.get_file())
- props = context.scene.BIMGeoreferenceProperties
- x, y, z = [float(co) for co in props.coordinate_input.split(",")]
-
- if Data.map_conversion:
- results = ifcopenshell.util.geolocation.enh2xyz(
- x,
- y,
- z,
- Data.map_conversion["Eastings"],
- Data.map_conversion["Northings"],
- Data.map_conversion["OrthogonalHeight"],
- Data.map_conversion.get("XAxisAbscissa", 1.0),
- Data.map_conversion.get("XAxisOrdinate", 0.0),
- Data.map_conversion.get("Scale", 1.0),
- )
- else:
- results = (x, y, z)
-
- if props.has_blender_offset:
- results = ifcopenshell.util.geolocation.enh2xyz(
- results[0],
- results[1],
- results[2],
- float(props.blender_eastings),
- float(props.blender_northings),
- float(props.blender_orthogonal_height),
- float(props.blender_x_axis_abscissa),
- float(props.blender_x_axis_ordinate),
- 1.0,
- )
-
- props.coordinate_output = ",".join([str(r) for r in results])
-
- scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
- context.scene.cursor.location = [o * scale for o in results]
- return {"FINISHED"}
+ def _execute(self, context):
+ core.remove_georeferencing(tool.Ifc)
-class GetCursorLocation(bpy.types.Operator):
+class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.edit_georeferencing"
+ bl_label = "Edit Georeferencing"
+ bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Edit the georeferencing"
+
+ def _execute(self, context):
+ core.edit_georeferencing(tool.Ifc, tool.Georeference)
+
+
+class DisableEditingGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.disable_editing_georeferencing"
+ bl_label = "Disable Editing Georeferencing"
+ bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Close editing panel"
+
+ def _execute(self, context):
+ core.disable_editing_georeferencing(tool.Georeference)
+
+
+class SetIfcGridNorth(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.set_ifc_grid_north"
+ bl_label = "Set IFC Grid North"
+ bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Set IFC grid north"
+
+ def _execute(self, context):
+ core.set_ifc_grid_north(tool.Georeference)
+
+
+class SetBlenderGridNorth(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.set_blender_grid_north"
+ bl_label = "Set Blender Grid North"
+ bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Set Blender grid north"
+
+ def _execute(self, context):
+ core.set_blender_grid_north(tool.Georeference)
+
+
+class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.get_cursor_location"
bl_label = "Get Cursor Location"
bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Insert the current cursor coordinates"
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
- return file and file.by_type("IfcUnitAssignment")
+ return tool.Ifc.get()
- def execute(self, context):
- props = context.scene.BIMGeoreferenceProperties
- scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
- project_coordinates = [o / scale for o in context.scene.cursor.location]
- props.coordinate_input = ",".join([str(o) for o in project_coordinates])
- return {"FINISHED"}
+ def _execute(self, context):
+ core.get_cursor_location(tool.Georeference)
-class SetCursorLocation(bpy.types.Operator):
+class SetCursorLocation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.set_cursor_location"
bl_label = "Set Cursor Location"
bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Move curson location to the specified coordinates"
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
props = context.scene.BIMGeoreferenceProperties
- return file and file.by_type("IfcUnitAssignment") and props.coordinate_output.count(",") == 2
+ return tool.Ifc.get() and props.coordinate_output.count(",") == 2
- def execute(self, context):
+ def _execute(self, context):
+ core.set_cursor_location(tool.Georeference)
+
+
+class SetIfcTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.set_ifc_true_north"
+ bl_label = "Set IFC True North"
+ bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Set IFC True north"
+
+ def _execute(self, context):
+ core.set_ifc_true_north(tool.Georeference)
+
+
+class SetBlenderTrueNorth(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.set_blender_true_north"
+ bl_label = "Set Blender True North"
+ bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Set Blender true north"
+
+ def _execute(self, context):
+ core.set_blender_true_north(tool.Georeference)
+
+
+class ConvertLocalToGlobal(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.convert_local_to_global"
+ bl_label = "Convert Local To Global"
+ bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Convert local coordinate to global coordinate"
+
+ @classmethod
+ def poll(cls, context):
+ file = tool.Ifc.get()
props = context.scene.BIMGeoreferenceProperties
- scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file())
- context.scene.cursor.location = [float(co) * scale for co in props.coordinate_output.split(",")]
- return {"FINISHED"}
+ return file and props.coordinate_input.count(",") == 2
+
+ def _execute(self, context):
+ core.convert_local_to_global(tool.Georeference)
+
+
+class ConvertGlobalToLocal(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.convert_global_to_local"
+ bl_label = "Convert Global To Local"
+ bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Convert global coordinate to local coordinate"
+
+ @classmethod
+ def poll(cls, context):
+ file = tool.Ifc.get()
+ props = context.scene.BIMGeoreferenceProperties
+ return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2
+
+ def _execute(self, context):
+ core.convert_global_to_local(tool.Georeference)
diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py
index 062b229bab..15c28e3467 100644
--- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py
@@ -16,11 +16,10 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see .
-import ifcopenshell.util.geolocation
+import blenderbim.tool as tool
from bpy.types import Panel
-from ifcopenshell.api.georeference.data import Data
-from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes, draw_attribute
+from blenderbim.bim.module.georeference.data import GeoreferenceData
class BIM_PT_gis(Panel):
@@ -32,16 +31,13 @@ class BIM_PT_gis(Panel):
bl_context = "scene"
bl_parent_id = "BIM_PT_geometry"
- @classmethod
- def poll(cls, context):
- return IfcStore.get_file()
-
def draw(self, context):
self.layout.use_property_split = True
self.layout.use_property_decorate = False
props = context.scene.BIMGeoreferenceProperties
- if not Data.is_loaded:
- Data.load(IfcStore.get_file())
+
+ if not GeoreferenceData.is_loaded:
+ GeoreferenceData.load()
if props.is_editing:
return self.draw_editable_ui(context)
@@ -82,10 +78,10 @@ class BIM_PT_gis(Panel):
def draw_ui(self, context):
props = context.scene.BIMGeoreferenceProperties
- if not Data.projected_crs:
+ if not GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Not Georeferenced")
- if IfcStore.get_file().schema != "IFC2X3":
+ if tool.Ifc.get_schema != "IFC2X3":
row.operator("bim.add_georeferencing", icon="ADD", text="")
if props.has_blender_offset:
@@ -109,43 +105,27 @@ class BIM_PT_gis(Panel):
row.label(text=props.blender_x_axis_ordinate)
row = self.layout.row(align=True)
row.label(text="Derived Grid North")
- row.label(
- text=str(
- round(
- ifcopenshell.util.geolocation.xaxis2angle(
- float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
- ),
- 3,
- )
- )
- )
- elif IfcStore.get_file().schema == "IFC2X3":
- row = self.layout.row()
- row.label(text="Not Georeferenced")
+ row.label(text=GeoreferenceData.data["blender_derived_angle"])
- if Data.projected_crs:
+ if GeoreferenceData.data["projected_crs"]:
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
row.operator("bim.enable_editing_georeferencing", icon="GREASEPENCIL", text="")
row.operator("bim.remove_georeferencing", icon="X", text="")
- for key, value in Data.projected_crs.items():
- if key == "id" or key == "type" or not value:
+ for key, value in GeoreferenceData.data["projected_crs"].items():
+ if not value:
continue
- if key == "MapUnit":
- unit_value = value.get("Prefix", "") or ""
- unit_value += value["Name"]
- value = unit_value
row = self.layout.row(align=True)
row.label(text=key)
row.label(text=str(value))
- if Data.map_conversion:
+ if GeoreferenceData.data["map_conversion"]:
row = self.layout.row(align=True)
row.label(text="Map Conversion", icon="GRID")
- for key, value in Data.map_conversion.items():
- if key == "id" or key == "type" or key == "SourceCRS" or key == "TargetCRS" or value is None:
+ for key, value in GeoreferenceData.data["map_conversion"].items():
+ if value is None:
continue
row = self.layout.row(align=True)
row.label(text=key)
@@ -153,26 +133,17 @@ class BIM_PT_gis(Panel):
if key == "XAxisOrdinate":
row = self.layout.row(align=True)
row.label(text="Derived Angle")
- row.label(
- text=str(
- round(
- ifcopenshell.util.geolocation.xaxis2angle(
- Data.map_conversion["XAxisAbscissa"], Data.map_conversion["XAxisOrdinate"]
- ),
- 3,
- )
- )
- )
+ row.label(text=GeoreferenceData.data["map_derived_angle"])
- if Data.true_north:
+ if GeoreferenceData.data["true_north"]:
row = self.layout.row()
row.label(text="True North", icon="LIGHT_SUN")
row = self.layout.row(align=True)
row.label(text="Vector")
- row.label(text=str(Data.true_north[0:2])[1:-1])
+ row.label(text=str(GeoreferenceData.data["true_north"][0:2])[1:-1])
row = self.layout.row(align=True)
row.label(text="Derived Angle")
- row.label(text=str(round(ifcopenshell.util.geolocation.yaxis2angle(*Data.true_north[0:2]), 3)))
+ row.label(text=GeoreferenceData.data["true_derived_angle"])
class BIM_PT_gis_utilities(Panel):
diff --git a/src/blenderbim/blenderbim/bim/module/material/__init__.py b/src/blenderbim/blenderbim/bim/module/material/__init__.py
index 8ba682ae5c..818cd76977 100644
--- a/src/blenderbim/blenderbim/bim/module/material/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/material/__init__.py
@@ -28,6 +28,7 @@ classes = (
operator.AddProfile,
operator.AssignMaterial,
operator.AssignParameterizedProfile,
+ operator.ContractMaterialCategory,
operator.CopyMaterial,
operator.DisableEditingAssignedMaterial,
operator.DisableEditingMaterialSetItem,
@@ -36,6 +37,7 @@ classes = (
operator.EditMaterialSetItem,
operator.EnableEditingAssignedMaterial,
operator.EnableEditingMaterialSetItem,
+ operator.ExpandMaterialCategory,
operator.LoadMaterials,
operator.RemoveConstituent,
operator.RemoveLayer,
diff --git a/src/blenderbim/blenderbim/bim/module/material/data.py b/src/blenderbim/blenderbim/bim/module/material/data.py
index a327e31ab8..4d90a29408 100644
--- a/src/blenderbim/blenderbim/bim/module/material/data.py
+++ b/src/blenderbim/blenderbim/bim/module/material/data.py
@@ -78,9 +78,24 @@ class ObjectMaterialData:
def load(cls):
cls.data = {
"materials": cls.materials(),
+ "type_material": cls.type_material(),
}
cls.is_loaded = True
@classmethod
def materials(cls):
return sorted([(str(m.id()), m.Name or "Unnamed", "") for m in tool.Ifc.get().by_type("IfcMaterial")], key=lambda x: x[1])
+
+ @classmethod
+ def type_material(cls):
+ element = tool.Ifc.get_entity(bpy.context.active_object)
+ element_type = ifcopenshell.util.element.get_type(element)
+ if element_type and element_type != element:
+ material = ifcopenshell.util.element.get_material(element_type)
+ if not material:
+ return
+ if material.is_a() in ("IfcMaterialLayerSetUsage", "IfcMaterialLayerSet"):
+ name_attr = "LayerSetName"
+ else:
+ name_attr = "Name"
+ return getattr(material, name_attr, "Unnamed") or "Unnamed"
diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py
index 37e4781581..b635f13fa6 100644
--- a/src/blenderbim/blenderbim/bim/module/material/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/material/operator.py
@@ -778,3 +778,31 @@ class CopyMaterial(bpy.types.Operator):
if material.id() in object_material_ids:
return
obj.data.materials.append(IfcStore.get_element(material.id()))
+
+
+class ExpandMaterialCategory(bpy.types.Operator):
+ bl_idname = "bim.expand_material_category"
+ bl_label = "Expand Material Category"
+ bl_options = {"REGISTER", "UNDO"}
+ category: bpy.props.StringProperty()
+
+ def execute(self, context):
+ props = context.scene.BIMMaterialProperties
+ for category in [c for c in props.materials if c.is_category and c.name == self.category]:
+ category.is_expanded = True
+ core.load_materials(tool.Material, props.material_type)
+ return {"FINISHED"}
+
+
+class ContractMaterialCategory(bpy.types.Operator):
+ bl_idname = "bim.contract_material_category"
+ bl_label = "Contract Material Category"
+ bl_options = {"REGISTER", "UNDO"}
+ category: bpy.props.StringProperty()
+
+ def execute(self, context):
+ props = context.scene.BIMMaterialProperties
+ for category in [c for c in props.materials if c.is_category and c.name == self.category]:
+ category.is_expanded = False
+ core.load_materials(tool.Material, props.material_type)
+ return {"FINISHED"}
diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py
index 349d132452..8f400ab07f 100644
--- a/src/blenderbim/blenderbim/bim/module/material/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/material/prop.py
@@ -109,6 +109,8 @@ def get_material_types(self, context):
class Material(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
+ is_category: BoolProperty(name="Is Category", default=False)
+ is_expanded: BoolProperty(name="Is Expanded", default=True)
total_elements: IntProperty(name="Total Elements")
diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py
index 818d59cedb..d0048f5633 100644
--- a/src/blenderbim/blenderbim/bim/module/material/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/material/ui.py
@@ -61,16 +61,18 @@ class BIM_PT_materials(Panel):
row.operator("bim.add_material", text="", icon="ADD")
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
- op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
- op.material = material.ifc_definition_id
- row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
+ if material.ifc_definition_id:
+ op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
+ op.material = material.ifc_definition_id
+ row.operator("bim.remove_material", text="", icon="X").material = material.ifc_definition_id
else:
row.operator("bim.add_material_set", text="", icon="ADD").set_type = self.props.material_type
if self.props.materials and self.props.active_material_index < len(self.props.materials):
material = self.props.materials[self.props.active_material_index]
- op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
- op.material = material.ifc_definition_id
- row.operator("bim.remove_material_set", text="", icon="X").material = material.ifc_definition_id
+ if material.ifc_definition_id:
+ op = row.operator("bim.select_by_material", text="", icon="RESTRICT_SELECT_OFF")
+ op.material = material.ifc_definition_id
+ row.operator("bim.remove_material_set", text="", icon="X").material = material.ifc_definition_id
self.layout.template_list("BIM_UL_materials", "", self.props, "materials", self.props, "active_material_index")
@@ -139,6 +141,10 @@ class BIM_PT_object_material(Panel):
row.operator("bim.add_material", icon="ADD", text="").obj = ""
return
+ if ObjectMaterialData.data["type_material"]:
+ row = self.layout.row(align=True)
+ row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT")
+
if self.product_data:
if self.product_data["type"] == "IfcMaterialConstituentSet":
self.material_set_id = self.product_data["id"]
@@ -395,7 +401,21 @@ class BIM_UL_materials(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
- row.label(text=item.name)
- row2 = row.row()
- row2.alignment = "RIGHT"
- row2.label(text=str(item.total_elements))
+
+ if item.is_category:
+ if item.is_expanded:
+ row.operator(
+ "bim.contract_material_category", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN"
+ ).category = item.name
+ else:
+ row.operator(
+ "bim.expand_material_category", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT"
+ ).category = item.name
+ row.label(text=item.name or "Uncategorised")
+ else:
+ row.label(text="", icon="BLANK1")
+ row.label(text=item.name, icon="MATERIAL")
+
+ row2 = row.row()
+ row2.alignment = "RIGHT"
+ row2.label(text=str(item.total_elements))
diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py
index 1ee9a3f7be..393fe73273 100644
--- a/src/blenderbim/blenderbim/bim/module/model/data.py
+++ b/src/blenderbim/blenderbim/bim/module/model/data.py
@@ -58,7 +58,7 @@ class AuthoringData:
if not ifc_class and ifc_classes:
ifc_class = ifc_classes[0][0]
if ifc_class:
- elements = [(str(e.id()), e.Name, "") for e in tool.Ifc.get().by_type(ifc_class)]
+ elements = [(str(e.id()), e.Name, e.Description or "") for e in tool.Ifc.get().by_type(ifc_class)]
results.extend(sorted(elements, key=lambda s: s[1]))
return results
return []
diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py
index e971318d78..3fe07c3fe8 100644
--- a/src/blenderbim/blenderbim/bim/module/model/mep.py
+++ b/src/blenderbim/blenderbim/bim/module/model/mep.py
@@ -54,7 +54,6 @@ class MepGenerator:
self.width = dimensions.get("NominalDiameterOrWidth")
self.height = dimensions.get("NominalHeight")
self.length = 1
- self.rotation = 0
return self.derive_from_cursor()
elif self.relating_type.is_a("IfcPipeSegmentType"):
@@ -66,14 +65,14 @@ class MepGenerator:
def create_rectangle_segment(self):
verts = [
- Vector((0, self.width, 0)),
- Vector((0, 0, 0)),
- Vector((0, self.width, self.height)),
- Vector((0, 0, self.height)),
- Vector((self.length, self.width, 0)),
- Vector((self.length, 0, 0)),
- Vector((self.length, self.width, self.height)),
- Vector((self.length, 0, self.height)),
+ Vector((- self.width / 2, self.height / 2, 0)),
+ Vector((- self.width / 2, - self.height / 2, 0)),
+ Vector((- self.width / 2, self.height / 2, self.length)),
+ Vector((- self.width / 2, - self.height / 2, self.length)),
+ Vector((self.width / 2, self.height / 2, 0)),
+ Vector((self.width / 2, - self.height / 2, 0)),
+ Vector((self.width / 2, self.height / 2, self.length)),
+ Vector((self.width / 2, - self.height / 2, self.length)),
]
faces = [
[1, 3, 2, 0],
@@ -91,7 +90,8 @@ class MepGenerator:
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
obj.location = self.location
- obj.rotation_euler[2] = self.rotation
+ obj.rotation_euler[0] = math.pi / 2
+ obj.rotation_euler[2] = math.pi / 2
self.collection.objects.link(obj)
bpy.ops.bim.assign_class(
@@ -100,9 +100,19 @@ class MepGenerator:
ifc_representation_class="IfcExtrudedAreaSolid/IfcRectangleProfileDef",
)
- blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=tool.Ifc.get_entity(obj), type=self.relating_type)
- element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
+ element = tool.Ifc.get_entity(obj)
+
+ blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=self.relating_type)
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.Mep"})
+
+ start_port_matrix = Matrix()
+ end_port_matrix = Matrix.Translation((0, 0, self.length))
+
+ for mat in [start_port_matrix, end_port_matrix]:
+ port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
+ tool.Ifc.run("system.assign_port", element=element, port=port)
+ tool.Ifc.run("geometry.edit_object_placement", product=port, matrix=obj.matrix_world @ mat, is_si=True)
+
obj.select_set(True)
return obj
diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py
index ab47a6866c..b56712216d 100644
--- a/src/blenderbim/blenderbim/bim/module/model/product.py
+++ b/src/blenderbim/blenderbim/bim/module/model/product.py
@@ -20,7 +20,9 @@ import bpy
import mathutils
import ifcopenshell
import ifcopenshell.api
+import ifcopenshell.util.system
import ifcopenshell.util.element
+import ifcopenshell.util.placement
import ifcopenshell.util.representation
import blenderbim.tool as tool
import blenderbim.core.type
@@ -81,7 +83,7 @@ class AddTypeInstance(bpy.types.Operator):
elif material and material.is_a("IfcMaterialLayerSet"):
if self.generate_layered_element(ifc_class, relating_type):
return {"FINISHED"}
- if relating_type.is_a("IfcFlowSegmentType"):
+ if relating_type.is_a("IfcFlowSegmentType") and not relating_type.RepresentationMaps:
if mep.MepGenerator(relating_type).generate():
return {"FINISHED"}
@@ -117,11 +119,9 @@ class AddTypeInstance(bpy.types.Operator):
collection.objects.link(obj)
collection_obj = bpy.data.objects.get(collection.name)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class)
+ element = tool.Ifc.get_entity(obj)
blenderbim.core.type.assign_type(
- tool.Ifc,
- tool.Type,
- element=tool.Ifc.get_entity(obj),
- type=tool.Ifc.get().by_id(int(props.relating_type)),
+ tool.Ifc, tool.Type, element=element, type=relating_type
)
if building_obj:
@@ -136,6 +136,17 @@ class AddTypeInstance(bpy.types.Operator):
if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id:
obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box])
+ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ for port in ifcopenshell.util.system.get_ports(relating_type):
+ mat = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement)
+ mat[0][3] *= unit_scale
+ mat[1][3] *= unit_scale
+ mat[2][3] *= unit_scale
+ mat = obj.matrix_world @ mathutils.Matrix(mat)
+ new_port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort")
+ tool.Ifc.run("system.assign_port", element=element, port=new_port)
+ tool.Ifc.run("geometry.edit_object_placement", product=new_port, matrix=mat, is_si=True)
+
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
context.view_layer.objects.active = obj
diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py
index d41f8d5e11..368c907350 100644
--- a/src/blenderbim/blenderbim/bim/module/model/wall.py
+++ b/src/blenderbim/blenderbim/bim/module/model/wall.py
@@ -688,23 +688,10 @@ class DumbWallGenerator:
for stroke in layer.active_frame.strokes:
if len(stroke.points) == 1:
continue
- coords = (stroke.points[0].co, stroke.points[-1].co)
- direction = coords[1] - coords[0]
- length = direction.length
- if length < 0.1:
- continue
- data = {"coords": coords}
-
- # Round to nearest 50mm (yes, metric for now)
- self.length = 0.05 * round(length / 0.05)
- self.rotation = math.atan2(direction[1], direction[0])
- # Round to nearest 5 degrees
- nearest_degree = (math.pi / 180) * 5
- self.rotation = nearest_degree * round(self.rotation / nearest_degree)
- self.location = coords[0]
- data["obj"] = self.create_wall()
- strokes.append(data)
- objs.append(data["obj"])
+ data = self.create_wall_from_2_points((stroke.points[0].co, stroke.points[-1].co))
+ if data:
+ strokes.append(data)
+ objs.append(data["obj"])
if len(objs) < 2:
return objs
@@ -726,6 +713,23 @@ class DumbWallGenerator:
bpy.context.scene.grease_pencil.layers.remove(layer)
return objs
+ def create_wall_from_2_points(self, coords):
+ direction = coords[1] - coords[0]
+ length = direction.length
+ if length < 0.1:
+ return
+ data = {"coords": coords}
+
+ # Round to nearest 50mm (yes, metric for now)
+ self.length = 0.05 * round(length / 0.05)
+ self.rotation = math.atan2(direction[1], direction[0])
+ # Round to nearest 5 degrees
+ nearest_degree = (math.pi / 180) * 5
+ self.rotation = nearest_degree * round(self.rotation / nearest_degree)
+ self.location = coords[0]
+ data["obj"] = self.create_wall()
+ return data
+
def has_end_near_stroke(self, stroke, stroke2):
point, distance = mathutils.geometry.intersect_point_line(stroke["coords"][0], *stroke2["coords"])
if distance > 0 and distance < 1 and self.is_near(point, stroke["coords"][0]):
diff --git a/src/blenderbim/blenderbim/bim/module/owner/__init__.py b/src/blenderbim/blenderbim/bim/module/owner/__init__.py
index 9b676a586d..3c9ba9de47 100644
--- a/src/blenderbim/blenderbim/bim/module/owner/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/owner/__init__.py
@@ -28,6 +28,7 @@ classes = (
operator.AddPersonAndOrganisation,
operator.AddPersonAttribute,
operator.AddRole,
+ operator.AssignActor,
operator.ClearUser,
operator.DisableEditingActor,
operator.DisableEditingAddress,
@@ -53,11 +54,13 @@ classes = (
operator.RemovePersonAttribute,
operator.RemoveRole,
operator.SetUser,
+ operator.UnassignActor,
prop.BIMOwnerProperties,
ui.BIM_PT_people,
ui.BIM_PT_organisations,
ui.BIM_PT_owner,
ui.BIM_PT_actor,
+ ui.BIM_PT_object_actor,
)
diff --git a/src/blenderbim/blenderbim/bim/module/owner/data.py b/src/blenderbim/blenderbim/bim/module/owner/data.py
index 3eae8fc574..be1cae0192 100644
--- a/src/blenderbim/blenderbim/bim/module/owner/data.py
+++ b/src/blenderbim/blenderbim/bim/module/owner/data.py
@@ -25,6 +25,7 @@ def refresh():
OrganisationsData.is_loaded = False
OwnerData.is_loaded = False
ActorData.is_loaded = False
+ ObjectActorData.is_loaded = False
class RolesAddressesData:
@@ -93,15 +94,17 @@ class PeopleData(RolesAddressesData):
def get_people(cls):
people = []
for person in tool.Ifc.get().by_type("IfcPerson"):
+ roles = cls.get_roles(person)
people.append(
{
"id": person.id(),
"props": bpy.context.scene.BIMOwnerProperties.person_attributes,
"name": cls.get_person_name(person),
+ "roles_label": ", ".join([r["label"] for r in roles]),
"is_editing": cls.get_person_is_editing(person),
"is_engaged": bool(person.EngagedIn),
"list_attributes": cls.get_person_list_attributes(person),
- "roles": cls.get_roles(person),
+ "roles": roles,
"addresses": cls.get_addresses(person),
}
)
@@ -151,14 +154,16 @@ class OrganisationsData(RolesAddressesData):
def get_organisations(cls):
organisations = []
for organisation in tool.Ifc.get().by_type("IfcOrganization"):
+ roles = cls.get_roles(organisation)
organisations.append(
{
"id": organisation.id(),
"props": bpy.context.scene.BIMOwnerProperties.organisation_attributes,
"name": organisation.Name,
+ "roles_label": ", ".join([r["label"] for r in roles]),
"is_editing": bpy.context.scene.BIMOwnerProperties.active_organisation_id == organisation.id(),
"is_engaged": bool(organisation.Engages),
- "roles": cls.get_roles(organisation),
+ "roles": roles,
"addresses": cls.get_addresses(organisation),
}
)
@@ -212,13 +217,13 @@ class ActorData:
@classmethod
def load(cls):
cls.data = {
- "actor": cls.actor(),
+ "the_actor": cls.the_actor(),
"actors": cls.actors(),
}
cls.is_loaded = True
@classmethod
- def actor(cls):
+ def the_actor(cls):
ifc_class = bpy.context.scene.BIMOwnerProperties.actor_type
return [(str(p.id()), p[0] or "Unnamed", "") for p in tool.Ifc.get().by_type(ifc_class)]
@@ -228,5 +233,57 @@ class ActorData:
props = bpy.context.scene.BIMOwnerProperties
for actor in tool.Ifc.get().by_type(props.actor_class, include_subtypes=False):
is_editing = props.active_actor_id == actor.id()
- actors.append({"id": actor.id(), "name": actor.Name or "Unnamed", "is_editing": is_editing})
+ if actor.TheActor.is_a("IfcPerson"):
+ the_actor = actor.TheActor.Identification or "N/A"
+ elif actor.TheActor.is_a("IfcOrganization"):
+ the_actor = actor.TheActor.Identification or "N/A"
+ elif actor.TheActor.is_a("IfcPersonAndOrganization"):
+ the_actor = actor.TheActor.ThePerson.Identification or "N/A"
+ the_actor += "-" + actor.TheActor.TheOrganization.Identification or "N/A"
+ actors.append(
+ {"id": actor.id(), "name": actor.Name or "Unnamed", "the_actor": the_actor, "is_editing": is_editing}
+ )
return actors
+
+
+class ObjectActorData:
+ data = {}
+ is_loaded = False
+
+ @classmethod
+ def load(cls):
+ cls.is_loaded = True
+ cls.data = {
+ "actor": cls.actor(),
+ "actors": cls.actors()
+ }
+
+ @classmethod
+ def actor(cls):
+ return [(str(p.id()), p.Name or "Unnamed", "") for p in tool.Ifc.get().by_type("IfcActor")]
+
+ @classmethod
+ def actors(cls):
+ results = []
+ element = tool.Ifc.get_entity(bpy.context.active_object)
+ if not element:
+ return results
+ for rel in getattr(element, "HasAssignments", []):
+ if rel.is_a("IfcRelAssignsToActor"):
+ actor = rel.RelatingActor
+ if actor.TheActor.is_a("IfcPerson"):
+ roles = cls.get_roles(actor.TheActor)
+ elif actor.TheActor.is_a("IfcOrganization"):
+ roles = cls.get_roles(actor.TheActor)
+ elif actor.TheActor.is_a("IfcPersonAndOrganization"):
+ roles = cls.get_roles(actor.TheActor.ThePerson)
+ roles.extend(cls.get_roles(actor.TheActor.TheOrganization))
+ role = ", ".join(roles)
+ results.append({
+ "id": actor.id(), "name": actor.Name or "Unnamed", "role": role, "ifc_class": actor.is_a()
+ })
+ return results
+
+ @classmethod
+ def get_roles(cls, parent):
+ return [r.UserDefinedRole or r.Role for r in parent.Roles or []]
diff --git a/src/blenderbim/blenderbim/bim/module/owner/operator.py b/src/blenderbim/blenderbim/bim/module/owner/operator.py
index d678369ac2..38709184b5 100644
--- a/src/blenderbim/blenderbim/bim/module/owner/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/owner/operator.py
@@ -315,8 +315,8 @@ class AddActor(bpy.types.Operator, Operator):
def _execute(self, context):
props = bpy.context.scene.BIMOwnerProperties
- if props.actor:
- core.add_actor(tool.Ifc, ifc_class=props.actor_class, actor=tool.Ifc.get().by_id(int(props.actor)))
+ if props.the_actor:
+ core.add_actor(tool.Ifc, ifc_class=props.actor_class, actor=tool.Ifc.get().by_id(int(props.the_actor)))
class EnableEditingActor(bpy.types.Operator, Operator):
@@ -355,3 +355,27 @@ class RemoveActor(bpy.types.Operator, Operator):
def _execute(self, context):
core.remove_actor(tool.Ifc, actor=tool.Ifc.get().by_id(self.actor))
+
+
+class AssignActor(bpy.types.Operator, Operator):
+ bl_idname = "bim.assign_actor"
+ bl_label = "Assign Actor"
+ bl_options = {"REGISTER", "UNDO"}
+ actor: bpy.props.IntProperty()
+
+ def _execute(self, context):
+ core.assign_actor(
+ tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=tool.Ifc.get_entity(context.active_object)
+ )
+
+
+class UnassignActor(bpy.types.Operator, Operator):
+ bl_idname = "bim.unassign_actor"
+ bl_label = "Unassign Actor"
+ bl_options = {"REGISTER", "UNDO"}
+ actor: bpy.props.IntProperty()
+
+ def _execute(self, context):
+ core.unassign_actor(
+ tool.Ifc, actor=tool.Ifc.get().by_id(self.actor), element=tool.Ifc.get_entity(context.active_object)
+ )
diff --git a/src/blenderbim/blenderbim/bim/module/owner/prop.py b/src/blenderbim/blenderbim/bim/module/owner/prop.py
index 49759be4bc..c1a2e65915 100644
--- a/src/blenderbim/blenderbim/bim/module/owner/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/owner/prop.py
@@ -18,7 +18,7 @@
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
-from blenderbim.bim.module.owner.data import OwnerData, ActorData
+from blenderbim.bim.module.owner.data import OwnerData, ActorData, ObjectActorData
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -44,14 +44,20 @@ def get_user_organisation(self, context):
return OwnerData.data["user_organisation"]
-def get_actor(self, context):
+def get_the_actor(self, context):
if not ActorData.is_loaded:
ActorData.load()
- return ActorData.data["actor"]
+ return ActorData.data["the_actor"]
+
+
+def get_actor(self, context):
+ if not ObjectActorData.is_loaded:
+ ObjectActorData.load()
+ return ObjectActorData.data["actor"]
def update_actor_type(self, context):
- ActorData.data["actor"] = ActorData.actor()
+ ActorData.data["the_actor"] = ActorData.the_actor()
def update_actor_class(self, context):
@@ -94,4 +100,5 @@ class BIMOwnerProperties(PropertyGroup):
name="Actor Type",
update=update_actor_type,
)
+ the_actor: EnumProperty(items=get_the_actor, name="Actor")
actor: EnumProperty(items=get_actor, name="Actor")
diff --git a/src/blenderbim/blenderbim/bim/module/owner/ui.py b/src/blenderbim/blenderbim/bim/module/owner/ui.py
index e3902775c8..2cf26457c0 100644
--- a/src/blenderbim/blenderbim/bim/module/owner/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/owner/ui.py
@@ -19,7 +19,7 @@
import bpy
import blenderbim.bim.helper
import blenderbim.tool as tool
-from blenderbim.bim.module.owner.data import PeopleData, OrganisationsData, OwnerData, ActorData
+from blenderbim.bim.module.owner.data import PeopleData, OrganisationsData, OwnerData, ActorData, ObjectActorData
def draw_roles(box, parent):
@@ -127,7 +127,9 @@ class BIM_PT_people(bpy.types.Panel):
draw_addresses(box, person)
else:
row = self.layout.row(align=True)
- row.label(text=person["name"])
+ row.label(text=person["name"], icon="OUTLINER_OB_ARMATURE")
+ if person["roles_label"]:
+ row.label(text=person["roles_label"])
row.operator("bim.enable_editing_person", icon="GREASEPENCIL", text="").person = person["id"]
if not person["is_engaged"]:
row.operator("bim.remove_person", icon="X", text="").person = person["id"]
@@ -171,7 +173,9 @@ class BIM_PT_organisations(bpy.types.Panel):
draw_addresses(box, organisation)
else:
row = self.layout.row(align=True)
- row.label(text=organisation["name"])
+ row.label(text=organisation["name"], icon="COMMUNITY")
+ if organisation["roles_label"]:
+ row.label(text=organisation["roles_label"])
op = row.operator("bim.enable_editing_organisation", icon="GREASEPENCIL", text="")
op.organisation = organisation["id"]
if not organisation["is_engaged"]:
@@ -254,9 +258,9 @@ class BIM_PT_actor(bpy.types.Panel):
row = self.layout.row(align=True)
row.prop(self.props, "actor_class", text="")
row.prop(self.props, "actor_type", text="")
- if ActorData.data["actor"]:
+ if ActorData.data["the_actor"]:
row = self.layout.row(align=True)
- row.prop(self.props, "actor", text="")
+ row.prop(self.props, "the_actor", text="")
row.operator("bim.add_actor", icon="ADD", text="")
else:
self.layout.label(text="No users found.")
@@ -273,6 +277,42 @@ class BIM_PT_actor(bpy.types.Panel):
blenderbim.bim.helper.draw_attributes(self.props.actor_attributes, box)
else:
row = self.layout.row(align=True)
- row.label(text=actor["name"])
+ row.label(text=actor["name"], icon="USER")
+ row.label(text=actor["the_actor"])
row.operator("bim.enable_editing_actor", icon="GREASEPENCIL", text="").actor = actor["id"]
row.operator("bim.remove_actor", icon="X", text="").actor = actor["id"]
+
+
+class BIM_PT_object_actor(bpy.types.Panel):
+ bl_label = "IFC Actor"
+ bl_idname = "BIM_PT_object_actor"
+ bl_options = {"DEFAULT_CLOSED"}
+ bl_space_type = "PROPERTIES"
+ bl_region_type = "WINDOW"
+ bl_context = "object"
+ bl_parent_id = "BIM_PT_misc_object"
+
+ @classmethod
+ def poll(cls, context):
+ return tool.Ifc.get()
+
+ def draw(self, context):
+ if not ObjectActorData.is_loaded:
+ ObjectActorData.load()
+
+ self.props = context.scene.BIMOwnerProperties
+
+ if not ObjectActorData.data["actor"]:
+ row = self.layout.row(align=True)
+ row.label(text="No Actors Found", icon="USER")
+ return
+
+ row = self.layout.row(align=True)
+ row.prop(self.props, "actor", text="")
+ row.operator("bim.assign_actor", icon="ADD", text="").actor = int(self.props.actor)
+
+ for actor in ObjectActorData.data["actors"]:
+ row = self.layout.row(align=True)
+ row.label(text=actor["name"], icon="USER")
+ row.label(text=actor["role"])
+ row.operator("bim.unassign_actor", icon="X", text="").actor = actor["id"]
diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py
index 7898d1ddf0..a2929523cd 100644
--- a/src/blenderbim/blenderbim/bim/module/project/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py
@@ -20,28 +20,29 @@ import bpy
from . import ui, prop, operator
classes = (
- operator.CreateProject,
- operator.LoadProject,
- operator.UnloadProject,
- operator.LoadProjectElements,
- operator.LinkIfc,
- operator.UnlinkIfc,
- operator.UnloadLink,
- operator.LoadLink,
- operator.ToggleLinkVisibility,
- operator.SelectLibraryFile,
- operator.ChangeLibraryElement,
- operator.RefreshLibrary,
- operator.RewindLibrary,
- operator.AssignLibraryDeclaration,
- operator.UnassignLibraryDeclaration,
- operator.SaveLibraryFile,
operator.AppendLibraryElement,
- operator.EnableEditingHeader,
+ operator.AssignLibraryDeclaration,
+ operator.ChangeLibraryElement,
+ operator.CreateProject,
operator.DisableEditingHeader,
operator.EditHeader,
+ operator.EnableEditingHeader,
operator.ExportIFC,
operator.ImportIFC,
+ operator.LinkIfc,
+ operator.LoadLink,
+ operator.LoadProject,
+ operator.LoadProjectElements,
+ operator.RefreshLibrary,
+ operator.RewindLibrary,
+ operator.SaveLibraryFile,
+ operator.SelectLibraryFile,
+ operator.ToggleFilterCategories,
+ operator.ToggleLinkVisibility,
+ operator.UnassignLibraryDeclaration,
+ operator.UnlinkIfc,
+ operator.UnloadLink,
+ operator.UnloadProject,
prop.LibraryElement,
prop.FilterCategory,
prop.Link,
@@ -57,7 +58,7 @@ classes = (
def menu_func_export(self, context):
op = self.layout.operator(operator.ExportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcjson)")
- op.should_save_as = False
+ op.should_save_as = True
def menu_func_import(self, context):
diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py
index 17ddfdc7bc..f311ff50d5 100644
--- a/src/blenderbim/blenderbim/bim/module/project/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/project/operator.py
@@ -495,6 +495,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
return {"FINISHED"}
context.scene.BIMProperties.ifc_file = self.filepath
context.scene.BIMProjectProperties.is_loading = True
+ context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
if not self.is_advanced:
bpy.ops.bim.load_project_elements()
return {"FINISHED"}
@@ -545,6 +546,8 @@ class LoadProjectElements(bpy.types.Operator):
settings.elements = self.get_decomposition_elements()
elif self.props.filter_mode == "IFC_CLASS":
settings.elements = self.get_ifc_class_elements()
+ elif self.props.filter_mode == "IFC_TYPE":
+ settings.elements = self.get_ifc_type_elements()
elif self.props.filter_mode == "WHITELIST":
settings.elements = self.get_whitelist_elements()
elif self.props.filter_mode == "BLACKLIST":
@@ -595,6 +598,14 @@ class LoadProjectElements(bpy.types.Operator):
elements.update(self.file.by_type(filter_category.name, include_subtypes=False))
return elements
+ def get_ifc_type_elements(self):
+ elements = set()
+ for filter_category in self.props.filter_categories:
+ if not filter_category.is_selected:
+ continue
+ elements.update(ifcopenshell.util.element.get_types(self.file.by_id(filter_category.ifc_definition_id)))
+ return elements
+
def get_whitelist_elements(self):
selector = ifcopenshell.util.selector.Selector()
return set(selector.parse(self.file, self.props.filter_query))
@@ -604,6 +615,18 @@ class LoadProjectElements(bpy.types.Operator):
return set(self.file.by_type("IfcElement")) - set(selector.parse(self.file, self.props.filter_query))
+class ToggleFilterCategories(bpy.types.Operator):
+ bl_idname = "bim.toggle_filter_categories"
+ bl_label = "Toggle Filter Categories"
+ bl_options = {"REGISTER", "UNDO"}
+ should_select: bpy.props.BoolProperty(name="Should Select", default=True)
+
+ def execute(self, context):
+ for filter_category in context.scene.BIMProjectProperties.filter_categories:
+ filter_category.is_selected = self.should_select
+ return {"FINISHED"}
+
+
class LinkIfc(bpy.types.Operator):
bl_idname = "bim.link_ifc"
bl_label = "Link IFC"
@@ -685,7 +708,7 @@ class LoadLink(bpy.types.Operator):
continue
bpy.data.scenes[0].collection.children.link(child)
link = context.scene.BIMProjectProperties.links.get(filepath)
- link.collection_name = child.name
+ link.collection = child
link.is_loaded = True
return {"FINISHED"}
@@ -695,11 +718,20 @@ class ToggleLinkVisibility(bpy.types.Operator):
bl_label = "Toggle Link Visibility"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle visibility between SOLID and WIREFRAME"
- collection_name: bpy.props.StringProperty()
+ link: bpy.props.StringProperty()
+ mode: bpy.props.StringProperty()
def execute(self, context):
- collection_name = self.collection_name
- objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, bpy.data.collections[collection_name].all_objects)
+ props = context.scene.BIMProjectProperties
+ link = props.links.get(self.link)
+ if self.mode == "WIREFRAME":
+ self.toggle_wireframe(link)
+ elif self.mode == "VISIBLE":
+ self.toggle_visibility(link)
+ return {"FINISHED"}
+
+ def toggle_wireframe(self, link):
+ objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, link.collection.all_objects)
for i,obj in enumerate(objs):
if i == 0:
if obj.display_type == "WIRE":
@@ -707,7 +739,22 @@ class ToggleLinkVisibility(bpy.types.Operator):
else:
display_type = "WIRE"
obj.display_type = display_type
- return {"FINISHED"}
+ link.is_wireframe = display_type == "WIRE"
+
+ def toggle_visibility(self, link):
+ queue = [bpy.context.view_layer.layer_collection]
+ layer_collection = None
+
+ while queue:
+ layer = queue.pop()
+ if layer.collection == link.collection:
+ layer_collection = layer
+ break
+ queue.extend(list(layer.children))
+
+ if layer_collection:
+ layer_collection.exclude = not layer_collection.exclude
+ link.is_hidden = layer_collection.exclude
class ExportIFC(bpy.types.Operator):
diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py
index fe49738432..3fe5e5077f 100644
--- a/src/blenderbim/blenderbim/bim/module/project/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/project/prop.py
@@ -23,6 +23,7 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty
from bpy.types import PropertyGroup
from bpy.props import (
+ PointerProperty,
BoolProperty,
CollectionProperty,
EnumProperty,
@@ -63,10 +64,16 @@ def update_filter_mode(self, context):
new.ifc_definition_id = element.id()
new.total_elements = sum([len(r.RelatedElements) for r in element.ContainsElements])
elif self.filter_mode == "IFC_CLASS":
- for ifc_class in sorted(list(set([e.is_a() for e in file.by_type("IfcElement")]))):
+ for ifc_class in sorted(list({e.is_a() for e in file.by_type("IfcElement")})):
new = self.filter_categories.add()
new.name = ifc_class
new.total_elements = len(file.by_type(ifc_class, include_subtypes=False))
+ elif self.filter_mode == "IFC_TYPE":
+ for ifc_type in sorted(file.by_type("IfcElementType"), key=lambda e: e.Name or "Unnamed"):
+ new = self.filter_categories.add()
+ new.name = ifc_type.is_a() + "/" + (ifc_type.Name or "Unnamed")
+ new.ifc_definition_id = ifc_type.id()
+ new.total_elements = len(ifcopenshell.util.element.get_types(ifc_type))
class LibraryElement(PropertyGroup):
@@ -85,8 +92,10 @@ class FilterCategory(PropertyGroup):
class Link(PropertyGroup):
name: StringProperty(name="Name")
- collection_name: StringProperty(name="Collection Name")
+ collection: PointerProperty(name="Collection", type=bpy.types.Collection)
is_loaded: BoolProperty(name="Is Loaded", default=False)
+ is_wireframe: BoolProperty(name="Is Wireframe", default=False)
+ is_hidden: BoolProperty(name="Is Hidden", default=False)
class BIMProjectProperties(PropertyGroup):
@@ -117,25 +126,40 @@ class BIMProjectProperties(PropertyGroup):
("NONE", "None", "No filtering is performed"),
("DECOMPOSITION", "Decomposition", "Filter objects by decomposition"),
("IFC_CLASS", "IFC Class", "Filter objects by class"),
+ ("IFC_TYPE", "IFC Type", "Filter objects by type"),
("WHITELIST", "Whitelist", "Filter objects using a custom whitelist query"),
("BLACKLIST", "Blacklist", "Filter objects using a custom blacklist query"),
],
name="Filter Mode",
update=update_filter_mode,
)
+ total_elements: IntProperty(name="Total Elements", default=0)
filter_categories: CollectionProperty(name="Filter Categories", type=FilterCategory)
active_filter_category_index: IntProperty(name="Active Filter Category Index")
filter_query: StringProperty(name="Filter Query")
should_filter_spatial_elements: BoolProperty(name="Filter Spatial Elements", default=False)
- should_use_cpu_multiprocessing: BoolProperty(name="Import with CPU Multiprocessing", default=True)
- should_merge_by_class: BoolProperty(name="Import and Merge by Class", default=False)
- should_merge_by_material: BoolProperty(name="Import and Merge by Material", default=False)
- should_merge_materials_by_colour: BoolProperty(name="Import and Merge Materials by Colour", default=False)
- should_clean_mesh: BoolProperty(name="Import and Clean Mesh", default=True)
- deflection_tolerance: FloatProperty(name="Import Deflection Tolerance", default=0.001)
- angular_tolerance: FloatProperty(name="Import Angular Tolerance", default=0.5)
- should_offset_model: BoolProperty(name="Import and Offset Model", default=False)
- model_offset_coordinates: StringProperty(name="Model Offset Coordinates", default="0,0,0")
+ should_use_cpu_multiprocessing: BoolProperty(name="CPU Multiprocessing", default=True)
+ merge_mode: bpy.props.EnumProperty(
+ items=[
+ ("NONE", "None", "No objects are merged"),
+ ("IFC_CLASS", "IFC Class", "One object per IFC class"),
+ ("IFC_TYPE", "IFC Type", "One object per IFC construction type"),
+ ("MATERIAL", "Material", "One object per material"),
+ ],
+ name="Merge Mode",
+ default="NONE",
+ )
+ should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False)
+ should_use_native_meshes: BoolProperty(name="Native Meshes", default=False)
+ should_clean_mesh: BoolProperty(name="Clean Meshes", default=True)
+ should_cache: BoolProperty(name="Cache", default=False)
+ is_coordinating: BoolProperty(name="For Coordination Only", default=False)
+ deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.001)
+ angular_tolerance: FloatProperty(name="Angular Tolerance", default=0.5)
+ distance_limit: FloatProperty(name="Distance Limit", default=1000)
+ false_origin: StringProperty(name="False Origin", default="0,0,0")
+ element_offset: IntProperty(name="Element Offset", default=0)
+ element_limit: IntProperty(name="Element Offset", default=30000)
links: CollectionProperty(name="Links", type=Link)
active_link_index: IntProperty(name="Active Link Index")
export_schema: EnumProperty(items=get_export_schema, name="IFC Schema")
diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py
index e90c818113..c8be2f2933 100644
--- a/src/blenderbim/blenderbim/bim/module/project/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/project/ui.py
@@ -52,7 +52,11 @@ class BIM_PT_project(Panel):
row.prop(pprops, "collection_mode")
row = self.layout.row()
row.prop(pprops, "filter_mode")
- if pprops.filter_mode in ["DECOMPOSITION", "IFC_CLASS"]:
+ if pprops.filter_mode in ["DECOMPOSITION", "IFC_CLASS", "IFC_TYPE"]:
+ row = self.layout.row(align=True)
+ row.label(text=f"Total: {pprops.total_elements}")
+ row.operator("bim.toggle_filter_categories", text="", icon="CHECKBOX_HLT").should_select = True
+ row.operator("bim.toggle_filter_categories", text="", icon="CHECKBOX_DEHLT").should_select = False
self.layout.template_list(
"BIM_UL_filter_categories",
"",
@@ -70,21 +74,32 @@ class BIM_PT_project(Panel):
row = self.layout.row()
row.prop(pprops, "should_use_cpu_multiprocessing")
row = self.layout.row()
- row.prop(pprops, "should_merge_by_class")
+ row.prop(pprops, "should_clean_mesh")
row = self.layout.row()
- row.prop(pprops, "should_merge_by_material")
+ row.prop(pprops, "should_cache")
+ row = self.layout.row()
+ row.prop(pprops, "should_use_native_meshes")
row = self.layout.row()
row.prop(pprops, "should_merge_materials_by_colour")
row = self.layout.row()
- row.prop(pprops, "should_clean_mesh")
+ row.prop(pprops, "is_coordinating")
+ if pprops.is_coordinating:
+ row = self.layout.row()
+ row.prop(pprops, "merge_mode")
row = self.layout.row()
row.prop(pprops, "deflection_tolerance")
row = self.layout.row()
row.prop(pprops, "angular_tolerance")
row = self.layout.row()
- row.prop(pprops, "should_offset_model")
+ row.prop(pprops, "distance_limit")
row = self.layout.row()
- row.prop(pprops, "model_offset_coordinates")
+ row.prop(pprops, "false_origin")
+
+ row = self.layout.row()
+ row.label(text="Element Range")
+ row = self.layout.row(align=True)
+ row.prop(pprops, "element_offset", text="")
+ row.prop(pprops, "element_limit", text="")
row = self.layout.row(align=True)
row.operator("bim.load_project_elements")
@@ -150,6 +165,7 @@ class BIM_PT_project(Panel):
op.should_save_as = False
op = row.operator("export_ifc.bim", icon="FILE_TICK", text="Save As")
op.should_save_as = True
+ row.operator("bim.unload_project", text="", icon="X")
def draw_create_project_ui(self, context):
props = context.scene.BIMProperties
@@ -286,8 +302,22 @@ class BIM_UL_links(UIList):
row = layout.row(align=True)
if item.is_loaded:
row.label(text=item.name)
- op = row.operator("bim.toggle_link_visibility", text="", icon="HIDE_OFF")
- op.collection_name = item.collection_name
+ op = row.operator(
+ "bim.toggle_link_visibility",
+ text="",
+ icon="CUBE" if item.is_wireframe else "MESH_CUBE",
+ emboss=False,
+ )
+ op.link = item.name
+ op.mode = "WIREFRAME"
+ op = row.operator(
+ "bim.toggle_link_visibility",
+ text="",
+ icon="HIDE_ON" if item.is_hidden else "HIDE_OFF",
+ emboss=False,
+ )
+ op.link = item.name
+ op.mode = "VISIBLE"
op = row.operator("bim.unload_link", text="", icon="UNLINKED")
op.filepath = item.name
else:
diff --git a/src/blenderbim/blenderbim/bim/module/pset/data.py b/src/blenderbim/blenderbim/bim/module/pset/data.py
index 06ce5d638a..78e6350e7a 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/data.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/data.py
@@ -41,7 +41,9 @@ class Data:
@classmethod
def psetqtos(cls, element, psets_only=False, qtos_only=False):
results = []
- psetqtos = ifcopenshell.util.element.get_psets(element, psets_only=psets_only, qtos_only=qtos_only)
+ psetqtos = ifcopenshell.util.element.get_psets(
+ element, psets_only=psets_only, qtos_only=qtos_only, should_inherit=False
+ )
for name, data in psetqtos.items():
results.append(
{
diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py
index a5520d3815..15cb559978 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py
@@ -148,6 +148,7 @@ class EnablePsetEditing(bpy.types.Operator):
new.is_null = data.get(prop_template.Name, None) is None
new.is_optional = True
new.data_type = data_type
+ new.is_uri = prop_template.PrimaryMeasureType == "IfcURIReference"
if data_type == "string":
new.string_value = "" if new.is_null else data[prop_template.Name]
diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py
index 9cfca725c7..15d9937d24 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py
@@ -147,7 +147,7 @@ class BIM_PT_object_psets(Panel):
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
if ObjectPsetsData.data["inherited_psets"]:
- self.layout.label(text="Inherited Psets:")
+ self.layout.label(text="Inherited Psets:", icon="FILE_PARENT")
for pset in ObjectPsetsData.data["inherited_psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
diff --git a/src/blenderbim/blenderbim/bim/module/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py
index 3cfd87fb97..369f0b50e8 100644
--- a/src/blenderbim/blenderbim/bim/module/root/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/root/ui.py
@@ -54,7 +54,7 @@ class BIM_PT_class(Panel):
self.draw_class_dropdowns(
context,
root_prop.getIfcPredefinedTypes(context.scene.BIMRootProperties, context),
- should_draw_product=False,
+ is_reassigning_class=True,
)
else:
row = self.layout.row(align=True)
@@ -74,9 +74,9 @@ class BIM_PT_class(Panel):
op.predefined_type = context.scene.BIMRootProperties.ifc_predefined_type if ifc_predefined_types else ""
op.userdefined_type = context.scene.BIMRootProperties.ifc_userdefined_type
- def draw_class_dropdowns(self, context, ifc_predefined_types, should_draw_product=True):
+ def draw_class_dropdowns(self, context, ifc_predefined_types, is_reassigning_class=False):
props = context.scene.BIMRootProperties
- if should_draw_product:
+ if not is_reassigning_class:
row = self.layout.row()
row.prop(props, "ifc_product")
row = self.layout.row()
@@ -87,5 +87,6 @@ class BIM_PT_class(Panel):
if ifc_predefined_types == "USERDEFINED":
row = self.layout.row()
row.prop(props, "ifc_userdefined_type")
- row = self.layout.row()
- row.prop(context.scene.BIMRootProperties, "contexts")
+ if not is_reassigning_class:
+ row = self.layout.row()
+ row.prop(context.scene.BIMRootProperties, "contexts")
diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py
index cd4365289e..5004f5c7fa 100644
--- a/src/blenderbim/blenderbim/bim/module/search/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/search/operator.py
@@ -84,7 +84,7 @@ class SelectIfcClass(bpy.types.Operator):
def execute(self, context):
self.file = IfcStore.get_file()
for obj in context.visible_objects:
- if not obj.BIMObjectProperties.ifc_definition_id:
+ if not obj.BIMObjectProperties.ifc_definition_id or obj.is_library_indirect:
continue
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
if does_keyword_exist(self.ifc_class, element.is_a(), context):
diff --git a/src/blenderbim/blenderbim/bim/module/style/__init__.py b/src/blenderbim/blenderbim/bim/module/style/__init__.py
index c7ecdab9e3..d8d6b7f344 100644
--- a/src/blenderbim/blenderbim/bim/module/style/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/style/__init__.py
@@ -27,6 +27,7 @@ classes = (
operator.EnableEditingStyle,
operator.LoadStyles,
operator.RemoveStyle,
+ operator.SelectByStyle,
operator.UnlinkStyle,
operator.UpdateStyleColours,
operator.UpdateStyleTextures,
diff --git a/src/blenderbim/blenderbim/bim/module/style/operator.py b/src/blenderbim/blenderbim/bim/module/style/operator.py
index 0e352ed138..09e1215b1f 100644
--- a/src/blenderbim/blenderbim/bim/module/style/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/style/operator.py
@@ -24,14 +24,7 @@ import ifcopenshell.util.representation
from blenderbim.bim.ifc import IfcStore
-class Operator:
- def execute(self, context):
- IfcStore.execute_ifc_operator(self, context)
- blenderbim.bim.handler.refresh_ui_data()
- return {"FINISHED"}
-
-
-class UpdateStyleColours(bpy.types.Operator, Operator):
+class UpdateStyleColours(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_style_colours"
bl_label = "Update Style Colours"
bl_options = {"REGISTER", "UNDO"}
@@ -40,7 +33,7 @@ class UpdateStyleColours(bpy.types.Operator, Operator):
core.update_style_colours(tool.Ifc, tool.Style, obj=context.active_object.active_material)
-class UpdateStyleTextures(bpy.types.Operator, Operator):
+class UpdateStyleTextures(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.update_style_textures"
bl_label = "Update Style Textures"
bl_options = {"REGISTER", "UNDO"}
@@ -55,7 +48,7 @@ class UpdateStyleTextures(bpy.types.Operator, Operator):
)
-class RemoveStyle(bpy.types.Operator, Operator):
+class RemoveStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_style"
bl_label = "Remove Style"
bl_options = {"REGISTER", "UNDO"}
@@ -65,7 +58,7 @@ class RemoveStyle(bpy.types.Operator, Operator):
core.remove_style(tool.Ifc, tool.Material, tool.Style, style=tool.Ifc.get().by_id(self.style))
-class AddStyle(bpy.types.Operator, Operator):
+class AddStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_style"
bl_label = "Add Style"
bl_options = {"REGISTER", "UNDO"}
@@ -74,7 +67,7 @@ class AddStyle(bpy.types.Operator, Operator):
core.add_style(tool.Ifc, tool.Style, obj=context.active_object.active_material)
-class UnlinkStyle(bpy.types.Operator, Operator):
+class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unlink_style"
bl_label = "Unlink Style"
bl_options = {"REGISTER", "UNDO"}
@@ -83,7 +76,7 @@ class UnlinkStyle(bpy.types.Operator, Operator):
core.unlink_style(tool.Ifc, tool.Style, obj=context.active_object.active_material)
-class EnableEditingStyle(bpy.types.Operator, Operator):
+class EnableEditingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_style"
bl_label = "Enable Editing Style"
bl_options = {"REGISTER", "UNDO"}
@@ -92,7 +85,7 @@ class EnableEditingStyle(bpy.types.Operator, Operator):
core.enable_editing_style(tool.Style, obj=context.active_object.active_material)
-class DisableEditingStyle(bpy.types.Operator, Operator):
+class DisableEditingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_style"
bl_options = {"REGISTER", "UNDO"}
bl_label = "Disable Editing Style"
@@ -101,7 +94,7 @@ class DisableEditingStyle(bpy.types.Operator, Operator):
core.disable_editing_style(tool.Style, obj=context.active_object.active_material)
-class EditStyle(bpy.types.Operator, Operator):
+class EditStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_style"
bl_label = "Edit Style"
bl_options = {"REGISTER", "UNDO"}
@@ -110,7 +103,7 @@ class EditStyle(bpy.types.Operator, Operator):
core.edit_style(tool.Ifc, tool.Style, obj=context.active_object.active_material)
-class DisableEditingStyles(bpy.types.Operator, Operator):
+class DisableEditingStyles(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_styles"
bl_options = {"REGISTER", "UNDO"}
bl_label = "Disable Editing Styles"
@@ -119,7 +112,7 @@ class DisableEditingStyles(bpy.types.Operator, Operator):
core.disable_editing_styles(tool.Style)
-class LoadStyles(bpy.types.Operator, Operator):
+class LoadStyles(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.load_styles"
bl_label = "Load Styles"
bl_options = {"REGISTER", "UNDO"}
@@ -127,3 +120,13 @@ class LoadStyles(bpy.types.Operator, Operator):
def _execute(self, context):
core.load_styles(tool.Style, style_type=self.style_type)
+
+
+class SelectByStyle(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.select_by_style"
+ bl_label = "Select By Material"
+ bl_options = {"REGISTER", "UNDO"}
+ style: bpy.props.IntProperty()
+
+ def _execute(self, context):
+ core.select_by_style(tool.Style, style=tool.Ifc.get().by_id(self.style))
diff --git a/src/blenderbim/blenderbim/bim/module/style/ui.py b/src/blenderbim/blenderbim/bim/module/style/ui.py
index d41a3ad46f..86f4261866 100644
--- a/src/blenderbim/blenderbim/bim/module/style/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/style/ui.py
@@ -57,8 +57,8 @@ class BIM_PT_styles(Panel):
#row.operator("bim.add_presentation_style", text="", icon="ADD")
if self.props.styles and self.props.active_style_index < len(self.props.styles):
style = self.props.styles[self.props.active_style_index]
- # op = row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF")
- # op.style = style.ifc_definition_id
+ op = row.operator("bim.select_by_style", text="", icon="RESTRICT_SELECT_OFF")
+ op.style = style.ifc_definition_id
row.operator("bim.remove_style", text="", icon="X").style = style.ifc_definition_id
self.layout.template_list("BIM_UL_styles", "", self.props, "styles", self.props, "active_style_index")
@@ -138,6 +138,6 @@ class BIM_UL_styles(UIList):
if item:
row = layout.row(align=True)
row.label(text=item.name)
- #row2 = row.row()
- #row2.alignment = "RIGHT"
- #row2.label(text=str(item.total_elements))
+ row2 = row.row()
+ row2.alignment = "RIGHT"
+ row2.label(text=str(item.total_elements))
diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py
index f87f83740d..6cc772e135 100644
--- a/src/blenderbim/blenderbim/bim/module/system/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py
@@ -31,6 +31,7 @@ classes = (
operator.EnableEditingSystem,
operator.HidePorts,
operator.LoadSystems,
+ operator.RemovePort,
operator.RemoveSystem,
operator.SelectSystemProducts,
operator.SetFlowDirection,
diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py
index 290654884e..9b9165e97d 100644
--- a/src/blenderbim/blenderbim/bim/module/system/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/system/operator.py
@@ -140,7 +140,7 @@ class ShowPorts(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- core.show_ports(tool.System, element=tool.Ifc.get_entity(context.active_object))
+ core.show_ports(tool.Ifc, tool.System, element=tool.Ifc.get_entity(context.active_object))
class HidePorts(bpy.types.Operator, Operator):
@@ -149,18 +149,27 @@ class HidePorts(bpy.types.Operator, Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- core.hide_ports(tool.System, element=tool.Ifc.get_entity(context.active_object))
+ core.hide_ports(tool.Ifc, tool.System, element=tool.Ifc.get_entity(context.active_object))
class AddPort(bpy.types.Operator, Operator):
bl_idname = "bim.add_port"
- bl_label = "Add Ports"
+ bl_label = "Add Port"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.add_port(tool.Ifc, tool.System, element=tool.Ifc.get_entity(context.active_object))
+class RemovePort(bpy.types.Operator, Operator):
+ bl_idname = "bim.remove_port"
+ bl_label = "Remove Port"
+ bl_options = {"REGISTER", "UNDO"}
+
+ def _execute(self, context):
+ core.remove_port(tool.Ifc, tool.System, port=tool.Ifc.get_entity(context.active_object))
+
+
class ConnectPort(bpy.types.Operator, Operator):
bl_idname = "bim.connect_port"
bl_label = "Connect Ports"
diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py
index 33a0e975f6..e65e90f87f 100644
--- a/src/blenderbim/blenderbim/bim/module/system/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/system/ui.py
@@ -140,7 +140,9 @@ class BIM_PT_ports(Panel):
if not context.active_object:
return False
element = tool.Ifc.get_entity(context.active_object)
- if not element or not element.is_a("IfcDistributionElement"):
+ if not element:
+ return False
+ if not element.is_a("IfcDistributionElement") and not element.is_a("IfcDistributionElementType"):
return False
return True
@@ -183,6 +185,7 @@ class BIM_PT_port(Panel):
row.operator("bim.set_flow_direction", icon="BACK", text="").direction = "SINK"
row.operator("bim.set_flow_direction", icon="ARROW_LEFTRIGHT", text="").direction = "SOURCEANDSINK"
row.operator("bim.set_flow_direction", icon="RESTRICT_INSTANCED_ON", text="").direction = "NOTDEFINED"
+ row.operator("bim.remove_port", icon="X", text="")
class BIM_UL_systems(UIList):
diff --git a/src/blenderbim/blenderbim/bim/module/unit/ui.py b/src/blenderbim/blenderbim/bim/module/unit/ui.py
index 8f053d108b..a6d13447f5 100644
--- a/src/blenderbim/blenderbim/bim/module/unit/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/unit/ui.py
@@ -111,7 +111,7 @@ class BIM_UL_units(UIList):
op.unit = item.ifc_definition_id
if props.active_unit_id == item.ifc_definition_id:
- row.operator("bim.edit_unit", text="", icon="CHECKMARK")
+ row.operator("bim.edit_unit", text="", icon="CHECKMARK").unit = item.ifc_definition_id
row.operator("bim.disable_editing_unit", text="", icon="CANCEL")
elif props.active_unit_id:
row.operator("bim.remove_unit", text="", icon="X").unit = item.ifc_definition_id
diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py
index 000ceac13c..2457cd99bf 100644
--- a/src/blenderbim/blenderbim/bim/ui.py
+++ b/src/blenderbim/blenderbim/bim/ui.py
@@ -106,8 +106,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
name="SVG to DXF Command",
description="E.g. [['inkscape', svg, '-o', eps], ['pstoedit', '-dt', '-f', 'dxf:-polyaslines -mm', eps, dxf, '-psarg', '-dNOSAFER']]",
)
- svg_command: StringProperty(name="SVG Command", description="E.g. [['firefox-bin', path]]")
- pdf_command: StringProperty(name="PDF Command", description="E.g. [['firefox-bin', path]]")
+ svg_command: StringProperty(name="SVG Command", description="E.g. [['firefox', path]]")
+ pdf_command: StringProperty(name="PDF Command", description="E.g. [['firefox', path]]")
+ spreadsheet_command: StringProperty(name="Spreadsheet Command", description="E.g. [['libreoffice', path]]")
openlca_port: IntProperty(name="OpenLCA IPC Port", default=8080)
should_hide_empty_props: BoolProperty(name="Should Hide Empty Properties", default=True)
should_play_chaching_sound: BoolProperty(
@@ -141,6 +142,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row = layout.row()
row.prop(self, "pdf_command")
row = layout.row()
+ row.prop(self, "spreadsheet_command")
+ row = layout.row()
row.prop(self, "openlca_port")
row = layout.row()
row.prop(self, "should_hide_empty_props")
diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py
index 845d35ed89..6bad766d63 100644
--- a/src/blenderbim/blenderbim/core/drawing.py
+++ b/src/blenderbim/blenderbim/core/drawing.py
@@ -36,18 +36,18 @@ def edit_text(ifc, drawing, obj=None):
drawing.disable_editing_text(obj)
-def enable_editing_text_product(drawing, obj=None):
- drawing.enable_editing_text_product(obj)
- drawing.import_text_product(obj)
+def enable_editing_assigned_product(drawing, obj=None):
+ drawing.enable_editing_assigned_product(obj)
+ drawing.import_assigned_product(obj)
-def disable_editing_text_product(drawing, obj=None):
- drawing.disable_editing_text_product(obj)
+def disable_editing_assigned_product(drawing, obj=None):
+ drawing.disable_editing_assigned_product(obj)
-def edit_text_product(ifc, drawing, obj=None, product=None):
+def edit_assigned_product(ifc, drawing, obj=None, product=None):
element = ifc.get_entity(obj)
- existing_product = drawing.get_text_product(element)
+ existing_product = drawing.get_assigned_product(element)
if existing_product == product:
return
if existing_product:
@@ -55,7 +55,7 @@ def edit_text_product(ifc, drawing, obj=None, product=None):
if product:
ifc.run("drawing.assign_product", relating_product=product, related_object=element)
drawing.update_text_value(obj)
- drawing.disable_editing_text_product(obj)
+ drawing.disable_editing_assigned_product(obj)
def load_sheets(drawing):
@@ -81,7 +81,7 @@ def add_sheet(ifc, drawing, titleblock=None):
def open_sheet(drawing, sheet=None):
- drawing.open_svg(drawing.get_sheet_filename(sheet))
+ drawing.open_svg(drawing.get_document_uri(sheet))
def remove_sheet(ifc, drawing, sheet=None):
@@ -89,6 +89,42 @@ def remove_sheet(ifc, drawing, sheet=None):
drawing.import_sheets()
+def load_schedules(drawing):
+ drawing.import_schedules()
+ drawing.enable_editing_schedules()
+
+
+def disable_editing_schedules(drawing):
+ drawing.disable_editing_schedules()
+
+
+def add_schedule(ifc, drawing, uri=None):
+ schedule = ifc.run("document.add_information")
+ reference = ifc.run("document.add_reference", information=schedule)
+ if ifc.get_schema() == "IFC2X3":
+ attributes = {"DocumentId": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"}
+ ifc.run("document.edit_information", information=schedule, attributes=attributes)
+ ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
+ else:
+ attributes = {"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": uri}
+ ifc.run("document.edit_information", information=schedule, attributes=attributes)
+ drawing.import_schedules()
+
+
+def remove_schedule(ifc, drawing, schedule=None):
+ ifc.run("document.remove_information", information=schedule)
+ drawing.import_schedules()
+
+
+def open_schedule(drawing, schedule=None):
+ drawing.open_spreadsheet(drawing.get_schedule_location(schedule))
+
+
+def update_schedule_name(ifc, drawing, schedule=None, name=None):
+ if drawing.get_name(schedule) != name:
+ ifc.run("document.edit_information", information=schedule, attributes={"Name": name})
+
+
def load_drawings(drawing):
drawing.import_drawings()
drawing.enable_editing_drawings()
@@ -115,7 +151,19 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
ifc.run("group.assign_group", group=group, product=element)
collector.assign(camera)
pset = ifc.run("pset.add_pset", product=element, name="EPset_Drawing")
- ifc.run("pset.edit_pset", pset=pset, properties={"TargetView": target_view, "Scale": "1/100", "HumanScale": "1:100"})
+ ifc.run(
+ "pset.edit_pset",
+ pset=pset,
+ properties={
+ "TargetView": target_view,
+ "Scale": "1/100",
+ "HumanScale": "1:100",
+ "HasUnderlay": False,
+ "HasLinework": True,
+ "HasAnnotation": True,
+ "GlobalReferencing": True,
+ },
+ )
drawing.import_drawings()
@@ -144,7 +192,7 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
if not context:
return
drawing_tool.show_decorations()
- obj = drawing_tool.create_annotation_object(object_type)
+ obj = drawing_tool.create_annotation_object(drawing, object_type)
element = ifc.get_entity(obj)
if not element:
element = drawing_tool.run_root_assign_class(
@@ -160,7 +208,15 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
drawing_tool.enable_editing(obj)
+def build_schedule(drawing, schedule=None):
+ drawing.create_svg_schedule(schedule)
+ drawing.open_svg(drawing.get_document_uri(schedule))
+
+
def sync_references(ifc, collector, drawing_tool, drawing=None):
+ if not drawing_tool.has_linework(drawing):
+ return
+
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))
if not context:
return
diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py
index c2329e44dd..65742a684e 100644
--- a/src/blenderbim/blenderbim/core/geometry.py
+++ b/src/blenderbim/blenderbim/core/geometry.py
@@ -21,11 +21,12 @@ import blenderbim.core.style
def edit_object_placement(ifc, geometry, surveyor, obj=None):
element = ifc.get_entity(obj)
- if element:
- geometry.clear_cache(element)
- geometry.clear_scale(obj)
- ifc.run("geometry.edit_object_placement", product=element, matrix=surveyor.get_absolute_matrix(obj))
- geometry.record_object_position(obj)
+ if not element:
+ return
+ geometry.clear_cache(element)
+ geometry.clear_scale(obj)
+ ifc.run("geometry.edit_object_placement", product=element, matrix=surveyor.get_absolute_matrix(obj))
+ geometry.record_object_position(obj)
def add_representation(
@@ -55,7 +56,7 @@ def add_representation(
profile_set_usage=profile_set_usage,
)
- if geometry.does_object_have_mesh_with_faces(obj):
+ if geometry.is_body_representation(representation):
[geometry.run_style_add_style(obj=mat) for mat in geometry.get_object_materials_without_styles(obj)]
ifc.run(
"style.assign_representation_styles",
diff --git a/src/blenderbim/blenderbim/core/georeference.py b/src/blenderbim/blenderbim/core/georeference.py
new file mode 100644
index 0000000000..8cde5d9b20
--- /dev/null
+++ b/src/blenderbim/blenderbim/core/georeference.py
@@ -0,0 +1,82 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of BlenderBIM Add-on.
+#
+# BlenderBIM Add-on is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# BlenderBIM Add-on is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with BlenderBIM Add-on. If not, see .
+
+
+def add_georeferencing(ifc):
+ ifc.run("georeference.add_georeferencing")
+
+
+def enable_editing_georeferencing(georeference):
+ georeference.import_projected_crs()
+ georeference.import_map_conversion()
+ georeference.import_true_north()
+ georeference.enable_editing()
+
+
+def remove_georeferencing(ifc):
+ ifc.run("georeference.remove_georeferencing")
+
+
+def disable_editing_georeferencing(georeference):
+ georeference.disable_editing()
+
+
+def edit_georeferencing(ifc, georeference):
+ ifc.run(
+ "georeference.edit_georeferencing",
+ projected_crs=georeference.get_projected_crs_attributes(),
+ map_conversion=georeference.get_map_conversion_attributes(),
+ true_north=georeference.get_true_north_attributes(),
+ )
+ georeference.disable_editing()
+
+
+def set_ifc_grid_north(georeference):
+ georeference.set_ifc_grid_north()
+
+
+def set_blender_grid_north(georeference):
+ georeference.set_blender_grid_north()
+
+
+def set_ifc_true_north(georeference):
+ georeference.set_ifc_true_north()
+
+
+def set_blender_true_north(georeference):
+ georeference.set_blender_true_north()
+
+
+def get_cursor_location(georeference):
+ georeference.set_coordinates("input", georeference.get_cursor_location())
+
+
+def set_cursor_location(georeference):
+ georeference.set_cursor_location(georeference.get_coordinates("output"))
+
+
+def convert_local_to_global(georeference):
+ coordinates = georeference.xyz2enh(georeference.get_coordinates("input"), georeference.get_map_conversion())
+ georeference.set_coordinates("output", coordinates)
+ georeference.set_cursor_location(coordinates)
+
+
+def convert_global_to_local(georeference):
+ coordinates = georeference.enh2xyz(georeference.get_coordinates("input"), georeference.get_map_conversion())
+ georeference.set_coordinates("output", coordinates)
+ georeference.set_cursor_location(coordinates)
diff --git a/src/blenderbim/blenderbim/core/owner.py b/src/blenderbim/blenderbim/core/owner.py
index 65b28cac39..dfeec08970 100644
--- a/src/blenderbim/blenderbim/core/owner.py
+++ b/src/blenderbim/blenderbim/core/owner.py
@@ -165,3 +165,11 @@ def disable_editing_actor(owner):
def edit_actor(ifc, owner):
ifc.run("owner.edit_actor", actor=owner.get_actor(), attributes=owner.export_actor_attributes())
disable_editing_actor(owner)
+
+
+def assign_actor(ifc, actor=None, element=None):
+ ifc.run("owner.assign_actor", relating_actor=actor, related_object=element)
+
+
+def unassign_actor(ifc, actor=None, element=None):
+ ifc.run("owner.unassign_actor", relating_actor=actor, related_object=element)
diff --git a/src/blenderbim/blenderbim/core/root.py b/src/blenderbim/blenderbim/core/root.py
index 534ba9ab15..bb3ede124c 100644
--- a/src/blenderbim/blenderbim/core/root.py
+++ b/src/blenderbim/blenderbim/core/root.py
@@ -21,6 +21,7 @@ def copy_class(ifc, collector, geometry, root, obj=None):
element = ifc.get_entity(obj)
if not element:
return
+ representation = root.get_object_representation(obj)
element = ifc.run("root.copy_class", product=element)
ifc.link(element, obj)
relating_type = root.get_element_type(element)
@@ -28,7 +29,6 @@ def copy_class(ifc, collector, geometry, root, obj=None):
ifc.run("type.map_type_representations", related_object=element, relating_type=relating_type)
root.link_object_data(ifc.get_object(relating_type), obj)
else:
- representation = root.get_object_representation(obj)
if representation:
root.run_geometry_add_representation(
obj=obj,
diff --git a/src/blenderbim/blenderbim/core/style.py b/src/blenderbim/blenderbim/core/style.py
index 4cc8e32a01..e56f7dcc32 100644
--- a/src/blenderbim/blenderbim/core/style.py
+++ b/src/blenderbim/blenderbim/core/style.py
@@ -110,3 +110,7 @@ def load_styles(style, style_type=None):
def disable_editing_styles(style):
style.disable_editing_styles()
+
+
+def select_by_style(style_tool, style=None):
+ style_tool.select_elements(style_tool.get_elements_by_style(style))
diff --git a/src/blenderbim/blenderbim/core/system.py b/src/blenderbim/blenderbim/core/system.py
index cde210638f..b635521222 100644
--- a/src/blenderbim/blenderbim/core/system.py
+++ b/src/blenderbim/blenderbim/core/system.py
@@ -66,23 +66,42 @@ def select_system_products(system_tool, system=None):
system_tool.select_system_products(system)
-def show_ports(system, element=None):
+def show_ports(ifc, system, element=None):
+ obj = ifc.get_object(element)
+ if obj and ifc.is_moved(obj):
+ system.run_geometry_edit_object_placement(obj=obj)
+
ports = system.get_ports(element)
- system.load_ports(ports)
+ system.load_ports(element, ports)
system.select_elements(ports)
-def hide_ports(system, element=None):
- system.delete_element_objects(system.get_ports(element))
+def hide_ports(ifc, system, element=None):
+ obj = ifc.get_object(element)
+ if obj and ifc.is_moved(obj):
+ system.run_geometry_edit_object_placement(obj=obj)
+
+ ports = system.get_ports(element)
+ for port in ports:
+ obj = ifc.get_object(port)
+ if obj and ifc.is_moved(obj):
+ system.run_geometry_edit_object_placement(obj=obj)
+
+ system.delete_element_objects(ports)
def add_port(ifc, system, element=None):
- system.load_ports(system.get_ports(element))
+ system.load_ports(element, system.get_ports(element))
obj = system.create_empty_at_cursor_with_element_orientation(element)
port = system.run_root_assign_class(obj=obj, ifc_class="IfcDistributionPort")
ifc.run("system.assign_port", element=element, port=port)
+def remove_port(ifc, system, port=None):
+ system.delete_element_objects([port])
+ ifc.run("root.remove_product", product=port)
+
+
def connect_port(ifc, port1=None, port2=None):
ifc.run("system.connect_port", port1=port1, port2=port2)
diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py
index a77f6cb006..3fa9c6808b 100644
--- a/src/blenderbim/blenderbim/core/tool.py
+++ b/src/blenderbim/blenderbim/core/tool.py
@@ -154,21 +154,24 @@ class Document:
@interface
class Drawing:
- def create_annotation_object(cls, object_type): pass
+ def create_annotation_object(cls, drawing, object_type): pass
def create_camera(cls, name, matrix): pass
+ def create_svg_schedule(cls, schedule): pass
def create_svg_sheet(cls, document, titleblock): pass
def delete_collection(cls, collection): pass
def delete_drawing_elements(cls, elements): pass
def delete_object(cls, obj): pass
+ def disable_editing_assigned_product(cls, obj): pass
def disable_editing_drawings(cls): pass
+ def disable_editing_schedules(cls): pass
def disable_editing_sheets(cls): pass
def disable_editing_text(cls, obj): pass
- def disable_editing_text_product(cls, obj): pass
def enable_editing(cls, obj): pass
+ def enable_editing_assigned_product(cls, obj): pass
def enable_editing_drawings(cls): pass
+ def enable_editing_schedules(cls): pass
def enable_editing_sheets(cls): pass
def enable_editing_text(cls, obj): pass
- def enable_editing_text_product(cls, obj): pass
def ensure_unique_drawing_name(cls, name): pass
def ensure_unique_identification(cls, identification): pass
def export_text_literal_attributes(cls, obj): pass
@@ -176,20 +179,23 @@ class Drawing:
def generate_drawing_name(cls, target_view, location_hint): pass
def generate_sheet_identification(cls): pass
def get_annotation_context(cls, target_view): pass
+ def get_assigned_product(cls, element): pass
def get_body_context(cls): pass
+ def get_document_uri(cls, document): pass
def get_drawing_collection(cls, drawing): pass
def get_drawing_group(cls, drawing): pass
def get_drawing_target_view(cls, drawing): pass
def get_group_elements(cls, group): pass
def get_ifc_representation_class(cls, object_type): pass
def get_name(cls, element): pass
- def get_sheet_filename(cls, document): pass
+ def get_schedule_location(cls, schedule): pass
def get_text_literal(cls, obj): pass
- def get_text_product(cls, element): pass
+ def import_assigned_product(cls, obj): pass
def import_drawings(cls): pass
+ def import_schedules(cls): pass
def import_sheets(cls): pass
def import_text_attributes(cls, obj): pass
- def import_text_product(cls, obj): pass
+ def open_spreadsheet(cls, uri): pass
def open_svg(cls, filepath): pass
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
def set_drawing_collection_name(cls, group, collection): pass
@@ -206,7 +212,6 @@ class Geometry:
def clear_scale(cls, obj): pass
def create_dynamic_voids(cls, obj): pass
def delete_data(cls, data): pass
- def does_object_have_mesh_with_faces(cls, obj): pass
def does_representation_id_exist(cls, representation_id): pass
def duplicate_object_data(cls, obj): pass
def get_cartesian_point_coordinate_offset(cls, obj): pass
@@ -243,6 +248,29 @@ class Geometry:
def should_use_presentation_style_assignment(cls): pass
+@interface
+class Georeference:
+ def disable_editing(cls): pass
+ def enable_editing(cls): pass
+ def enh2xyz(cls, map_conversion, coordinates): pass
+ def get_coordinates(cls, io): pass
+ def get_cursor_location(cls): pass
+ def get_map_conversion(cls): pass
+ def get_map_conversion_attributes(cls): pass
+ def get_projected_crs_attributes(cls): pass
+ def get_true_north_attributes(cls): pass
+ def import_map_conversion(cls): pass
+ def import_projected_crs(cls): pass
+ def import_true_north(cls): pass
+ def set_blender_grid_north(cls): pass
+ def set_blender_true_north(cls): pass
+ def set_coordinates(cls, io, coordinates): pass
+ def set_cursor_location(cls, coordinates): pass
+ def set_ifc_grid_north(cls): pass
+ def set_ifc_true_north(cls): pass
+ def xyz2enh(cls, map_conversion, coordinates): pass
+
+
@interface
class Ifc:
def get(cls): pass
@@ -438,6 +466,7 @@ class Style:
def export_surface_attributes(cls, obj): pass
def get_active_style_type(cls): pass
def get_context(cls, obj): pass
+ def get_elements_by_style(cls, style): pass
def get_name(cls, obj): pass
def get_style(cls, obj): pass
def get_surface_rendering_attributes(cls, obj): pass
@@ -449,6 +478,7 @@ class Style:
def import_presentation_styles(cls, style_type): pass
def import_surface_attributes(cls, style, obj): pass
def is_editing_styles(cls): pass
+ def select_elements(cls, elements): pass
@interface
@@ -468,7 +498,8 @@ class System:
def get_ports(cls, element): pass
def import_system_attributes(cls, system): pass
def import_systems(cls): pass
- def load_ports(cls, port): pass
+ def load_ports(cls, element, ports): pass
+ def run_geometry_edit_object_placement(cls, obj=None): pass
def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): pass
def select_elements(cls, elements): pass
def select_system_products(cls, system): pass
@@ -500,10 +531,10 @@ class Unit:
def get_scene_unit_name(cls, unit_type): pass
def get_scene_unit_si_prefix(cls): pass
def get_si_name_from_unit_type(cls, unit_type): pass
- def get_unit_class(cls, unit): pass
def import_unit_attributes(cls, unit): pass
def import_units(cls): pass
def is_scene_unit_metric(cls): pass
+ def is_unit_class(cls, unit, ifc_class): pass
def set_active_unit(cls, unit): pass
diff --git a/src/blenderbim/blenderbim/core/unit.py b/src/blenderbim/blenderbim/core/unit.py
index f5fd274a66..e21ea62816 100644
--- a/src/blenderbim/blenderbim/core/unit.py
+++ b/src/blenderbim/blenderbim/core/unit.py
@@ -96,12 +96,11 @@ def disable_editing_unit(unit):
def edit_unit(ifc, unit_tool, unit=None):
attributes = unit_tool.export_unit_attributes()
- unit_class = unit_tool.get_unit_class(unit)
- if unit_class == "IfcMonetaryUnit":
+ if unit_tool.is_unit_class(unit, "IfcMonetaryUnit"):
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
- elif unit_class == "IfcDerivedUnit":
+ elif unit_tool.is_unit_class(unit, "IfcDerivedUnit"):
ifc.run("unit.edit_derived_unit", unit=unit, attributes=attributes)
- elif unit_class == "IfcNamedUnit":
+ elif unit_tool.is_unit_class(unit, "IfcNamedUnit"):
ifc.run("unit.edit_named_unit", unit=unit, attributes=attributes)
unit_tool.import_units()
unit_tool.clear_active_unit()
diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py
index 7ae4c6fbea..705120b4b6 100644
--- a/src/blenderbim/blenderbim/tool/__init__.py
+++ b/src/blenderbim/blenderbim/tool/__init__.py
@@ -20,6 +20,7 @@ from blenderbim.tool.aggregate import Aggregate
from blenderbim.tool.blender import Blender
from blenderbim.tool.boundary import Boundary
from blenderbim.tool.brick import Brick
+from blenderbim.tool.cad import Cad
from blenderbim.tool.collector import Collector
from blenderbim.tool.context import Context
from blenderbim.tool.debug import Debug
@@ -27,6 +28,7 @@ from blenderbim.tool.demo import Demo
from blenderbim.tool.document import Document
from blenderbim.tool.drawing import Drawing
from blenderbim.tool.geometry import Geometry
+from blenderbim.tool.georeference import Georeference
from blenderbim.tool.ifc import Ifc
from blenderbim.tool.library import Library
from blenderbim.tool.material import Material
diff --git a/src/blenderbim/blenderbim/tool/cad.py b/src/blenderbim/blenderbim/tool/cad.py
new file mode 100644
index 0000000000..e367e93810
--- /dev/null
+++ b/src/blenderbim/blenderbim/tool/cad.py
@@ -0,0 +1,329 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of BlenderBIM Add-on.
+#
+# BlenderBIM Add-on is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# BlenderBIM Add-on is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with BlenderBIM Add-on. If not, see .
+
+# This code was originally taken from https://github.com/zeffii/mesh_tiny_cad
+# which is typically bundled with Blender, licensed under GPL v2-or-later.
+# Modifications are made to make the behaviour more in line with how intuitively
+# CAD drafters use these tools.
+
+# Changes include:
+#
+# - Instead of AutoVTX, user explicitly chooses V, T, or X mode
+# - Instead of adding edges, existing edges are extended
+# - An arc is reconstructed from 3 points instead of a full circle
+# - You can now derive the center from an arc without generating geometry
+
+
+import sys
+import bpy
+import math
+import bmesh
+from mathutils import Vector, Matrix, geometry
+from mathutils.geometry import intersect_line_line as LineIntersect
+from mathutils.geometry import intersect_point_line as PtLineIntersect
+
+
+class CAD_prefs:
+ VTX_PRECISION = 1.0e-5
+ VTX_DOUBLES_THRSHLD = 0.0001
+
+
+class Cad:
+ @classmethod
+ def point_on_edge(cls, p, edge):
+ """
+ > p: vector
+ > edge: tuple of 2 vectors
+ < returns: True / False if a point happens to lie on an edge
+ """
+ pt, _percent = PtLineIntersect(p, *edge)
+ on_line = (pt - p).length < CAD_prefs.VTX_PRECISION
+ return on_line and (0.0 <= _percent <= 1.0)
+
+ @classmethod
+ def line_from_edge_intersect(cls, edge1, edge2):
+ """
+ > takes 2 tuples, each tuple contains 2 vectors
+ - prepares input for sending to intersect_line_line
+ < returns output of intersect_line_line
+ """
+ [p1, p2], [p3, p4] = edge1, edge2
+ return LineIntersect(p1, p2, p3, p4)
+
+ @classmethod
+ def get_intersection(cls, edge1, edge2):
+ """
+ > takes 2 tuples, each tuple contains 2 vectors
+ < returns the point halfway on line. See intersect_line_line
+ """
+ line = cls.line_from_edge_intersect(edge1, edge2)
+ if line:
+ return (line[0] + line[1]) / 2
+
+ @classmethod
+ def test_coplanar(cls, edge1, edge2):
+ """
+ the line that describes the shortest line between the two edges
+ would be short if the lines intersect mathematically. If this
+ line is longer than the VTX_PRECISION then they are either
+ coplanar or parallel.
+ """
+ line = cls.line_from_edge_intersect(edge1, edge2)
+ if line:
+ return (line[0] - line[1]).length < CAD_prefs.VTX_PRECISION
+
+ @classmethod
+ def closest_idx(cls, pt, e):
+ """
+ > pt: vector
+ > e: bmesh edge
+ < returns: returns index of vertex closest to pt.
+
+ if both points in e are equally far from pt, then v1 is returned.
+ """
+ if isinstance(e, bmesh.types.BMEdge):
+ ev = e.verts
+ v1 = ev[0].co
+ v2 = ev[1].co
+ distance_test = (v1 - pt).length <= (v2 - pt).length
+ return ev[0].index if distance_test else ev[1].index
+
+ print("received {0}, check expected input in docstring ".format(e))
+
+ @classmethod
+ def closest_vector(cls, pt, e):
+ """
+ > pt: vector
+ > e: 2 vector tuple
+ < returns:
+ pt, 2 vector tuple: returns closest vector to pt
+
+ if both points in e are equally far from pt, then v1 is returned.
+ """
+ if isinstance(e, tuple) and all([isinstance(co, Vector) for co in e]):
+ v1, v2 = e
+ distance_test = (v1 - pt).length <= (v2 - pt).length
+ return v1 if distance_test else v2
+
+ print("received {0}, check expected input in docstring ".format(e))
+
+ @classmethod
+ def coords_tuple_from_edge_idx(cls, bm, idx):
+ """bm is a bmesh representation"""
+ return tuple(v.co for v in bm.edges[idx].verts)
+
+ @classmethod
+ def vectors_from_indices(cls, bm, raw_vert_indices):
+ """bm is a bmesh representation"""
+ return [bm.verts[i].co for i in raw_vert_indices]
+
+ @classmethod
+ def vertex_indices_from_edges_tuple(cls, bm, edge_tuple):
+ """
+ > bm: is a bmesh representation
+ > edge_tuple: contains two edge indices.
+ < returns the vertex indices of edge_tuple
+ """
+
+ def k(v, w):
+ return bm.edges[edge_tuple[v]].verts[w].index
+
+ return [k(i >> 1, i % 2) for i in range(4)]
+
+ @classmethod
+ def get_vert_indices_from_bmedges(cls, edges):
+ """
+ > bmedges: a list of two bm edges
+ < returns the vertex indices of edge_tuple as a flat list.
+ """
+ temp_edges = []
+ print(edges)
+ for e in edges:
+ for v in e.verts:
+ temp_edges.append(v.index)
+ return temp_edges
+
+ @classmethod
+ def num_edges_point_lies_on(cls, pt, edges):
+ """returns the number of edges that a point lies on."""
+ res = [point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]]
+ return len([i for i in res if i])
+
+ @classmethod
+ def find_intersecting_edges(cls, bm, pt, idx1, idx2):
+ """
+ > pt: Vector
+ > idx1, ix2: edge indices
+ < returns the list of edge indices where pt is on those edges
+ """
+ if not pt:
+ return []
+ idxs = [idx1, idx2]
+ edges = [cls.coords_tuple_from_edge_idx(bm, idx) for idx in idxs]
+ return [idx for edge, idx in zip(edges, idxs) if cls.point_on_edge(pt, edge)]
+
+ @classmethod
+ def duplicates(cls, indices):
+ return len(set(indices)) < 4
+
+ @classmethod
+ def vert_idxs_from_edge_idx(cls, bm, idx):
+ edge = bm.edges[idx]
+ return edge.verts[0].index, edge.verts[1].index
+
+ @classmethod
+ def add_edges(cls, bm, pt, idxs, fdp):
+ """
+ this function is a disaster --
+ index updates and ensure_lookup_table() are called before this function
+ and after, and i've tried doing this less verbose but results tend to be
+ less predictable. I'm obviously a terrible coder, but can only spend so
+ much time figuring out this stuff.
+ """
+
+ v1 = bm.verts.new(pt)
+
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.verts.index_update()
+
+ try:
+ for e in idxs:
+ bm.edges.index_update()
+ v2 = bm.verts[e]
+ bm.edges.new((v1, v2))
+
+ bm.edges.index_update()
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+
+ except Exception as err:
+ print("some failure: details")
+ for l in fdp:
+ print(l)
+
+ sys.stderr.write("ERROR: %s\n" % str(err))
+ print(sys.exc_info()[-1].tb_frame.f_code)
+ print("Error on line {}".format(sys.exc_info()[-1].tb_lineno))
+
+ @classmethod
+ def remove_earmarked_edges(cls, bm, earmarked):
+ edges_select = [e for e in bm.edges if e.index in earmarked]
+ bmesh.ops.delete(bm, geom=edges_select, context="EDGES")
+
+ @classmethod
+ def perform_vtx(cls, bm, pt, edges, pts, vertex_indices):
+ idx1, idx2 = edges[0].index, edges[1].index
+ fdp = pt, edges, pts, vertex_indices
+
+ # this list will hold those edges that pt lies on
+ edges_indices = cls.find_intersecting_edges(bm, pt, idx1, idx2)
+ mode = "VTX"[len(edges_indices)]
+
+ if mode == "V":
+ cl_vert1 = cls.closest_idx(pt, edges[0])
+ cl_vert2 = cls.closest_idx(pt, edges[1])
+ cls.add_edges(bm, pt, [cl_vert1, cl_vert2], fdp)
+
+ elif mode == "T":
+ to_edge_idx = edges_indices[0]
+ from_edge_idx = idx1 if to_edge_idx == idx2 else idx2
+
+ cl_vert = cls.closest_idx(pt, bm.edges[from_edge_idx])
+ to_vert1, to_vert2 = cls.vert_idxs_from_edge_idx(bm, to_edge_idx)
+ cls.add_edges(bm, pt, [cl_vert, to_vert1, to_vert2], fdp)
+
+ elif mode == "X":
+ cls.add_edges(bm, pt, vertex_indices, fdp)
+
+ # final refresh before returning to user.
+ if edges_indices:
+ cls.remove_earmarked_edges(bm, edges_indices)
+
+ bm.edges.index_update()
+ return bm
+
+ @classmethod
+ def perform_t(cls, bm, pt, target, edge, pts, vertex_indices):
+ cl_vert = cls.closest_idx(pt, bm.edges[edge.index])
+ bm.verts[cl_vert].co = pt
+ bm.edges.index_update()
+ return bm
+
+ @classmethod
+ def perform_v(cls, bm, pt, target, edge, pts, vertex_indices):
+ bm = cls.perform_t(bm, pt, target, edge, pts, vertex_indices)
+ bm = cls.perform_t(bm, pt, edge, target, pts, vertex_indices)
+ return bm
+
+ @classmethod
+ def prioritise_active_edge(cls, bm, edges):
+ return [edges[0], edges[1]] if bm.select_history.active == edges[0] else [edges[1], edges[0]]
+
+ @classmethod
+ def do_vtx_if_appropriate(cls, bm, edges, mode):
+ vertex_indices = cls.get_vert_indices_from_bmedges(edges)
+
+ # test 1, are there shared vers? if so return non-viable
+ if not len(set(vertex_indices)) == 4:
+ return {"SHARED_VERTEX"}
+
+ # test 2, is parallel?
+ p1, p2, p3, p4 = [bm.verts[i].co for i in vertex_indices]
+ point = cls.get_intersection([p1, p2], [p3, p4])
+ if not point:
+ return {"PARALLEL_EDGES"}
+
+ # test 3, coplanar edges?
+ coplanar = cls.test_coplanar([p1, p2], [p3, p4])
+ if not coplanar:
+ return {"NON_PLANAR_EDGES"}
+
+ edges = cls.prioritise_active_edge(bm, edges)
+ # point must lie on an edge or the virtual extension of an edge
+ if mode == "T":
+ bm = cls.perform_t(bm, point, edges[0], edges[1], (p1, p2, p3, p4), vertex_indices)
+ elif mode == "V":
+ bm = cls.perform_v(bm, point, edges[0], edges[1], (p1, p2, p3, p4), vertex_indices)
+ return bm
+
+ @classmethod
+ def get_center_of_arc(cls, pts, obj):
+ mw = obj.matrix_world
+ V = Vector
+
+ # construction
+ v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2])
+ edge1_mid = v1.lerp(v2, 0.5)
+ edge2_mid = v3.lerp(v4, 0.5)
+ axis = geometry.normal(v1, v2, v4)
+ mat_rot = Matrix.Rotation(math.radians(90.0), 4, axis)
+
+ # triangle edges
+ v1_ = ((v1 - edge1_mid) @ mat_rot) + edge1_mid
+ v2_ = ((v2 - edge1_mid) @ mat_rot) + edge1_mid
+ v3_ = ((v3 - edge2_mid) @ mat_rot) + edge2_mid
+ v4_ = ((v4 - edge2_mid) @ mat_rot) + edge2_mid
+
+ r = geometry.intersect_line_line(v1_, v2_, v3_, v4_)
+ if r:
+ p1, _ = r
+ cp = mw @ p1
+ return cp
+ else:
+ print("not on a circle")
diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py
index 8926ae9b39..34d9a153bf 100644
--- a/src/blenderbim/blenderbim/tool/drawing.py
+++ b/src/blenderbim/blenderbim/tool/drawing.py
@@ -29,27 +29,31 @@ import blenderbim.core.geometry
import blenderbim.tool as tool
import ifcopenshell.util.representation
import blenderbim.bim.module.drawing.sheeter as sheeter
+import blenderbim.bim.module.drawing.scheduler as scheduler
import blenderbim.bim.module.drawing.annotation as annotation
import blenderbim.bim.module.drawing.helper as helper
class Drawing(blenderbim.core.tool.Drawing):
@classmethod
- def create_annotation_object(cls, object_type):
+ def create_annotation_object(cls, drawing, object_type):
data_type = {
+ "ANGLE": "mesh",
+ "BREAKLINE": "mesh",
+ "DIAMETER": "curve",
"DIMENSION": "curve",
- "EQUAL_DIMENSION": "curve",
+ "FILL_AREA": "mesh",
+ "HIDDEN_LINE": "mesh",
+ "LINEWORK": "mesh",
+ "PLAN_LEVEL": "curve",
+ "RADIUS": "curve",
+ "SECTION_LEVEL": "curve",
+ "STAIR_ARROW": "curve",
"TEXT": "empty",
"TEXT_LEADER": "curve",
- "STAIR_ARROW": "curve",
- "HIDDEN_LINE": "mesh",
- "PLAN_LEVEL": "curve",
- "SECTION_LEVEL": "curve",
- "BREAKLINE": "mesh",
- "MISC": "mesh",
}[object_type]
- obj = annotation.Annotator.get_annotation_obj(object_type, data_type)
- if object_type == "BREAKLINE":
+ obj = annotation.Annotator.get_annotation_obj(drawing, object_type, data_type)
+ if object_type == "FILL_AREA":
obj = annotation.Annotator.add_plane_to_annotation(obj)
elif object_type != "TEXT":
obj = annotation.Annotator.add_line_to_annotation(obj)
@@ -70,11 +74,16 @@ class Drawing(blenderbim.core.tool.Drawing):
bpy.context.scene.collection.objects.link(camera)
return camera
+ @classmethod
+ def create_svg_schedule(cls, schedule):
+ schedule_creator = scheduler.Scheduler()
+ schedule_creator.schedule(cls.get_schedule_location(schedule), cls.get_document_uri(schedule))
+
@classmethod
def create_svg_sheet(cls, document, titleblock):
sheet_builder = sheeter.SheetBuilder()
sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir
- sheet_builder.create(cls.get_sheet_filename(document), titleblock)
+ sheet_builder.create(cls.get_document_uri(document), titleblock)
@classmethod
def delete_collection(cls, collection):
@@ -96,6 +105,10 @@ class Drawing(blenderbim.core.tool.Drawing):
def disable_editing_drawings(cls):
bpy.context.scene.DocProperties.is_editing_drawings = False
+ @classmethod
+ def disable_editing_schedules(cls):
+ bpy.context.scene.DocProperties.is_editing_schedules = False
+
@classmethod
def disable_editing_sheets(cls):
bpy.context.scene.DocProperties.is_editing_sheets = False
@@ -105,8 +118,8 @@ class Drawing(blenderbim.core.tool.Drawing):
obj.BIMTextProperties.is_editing = False
@classmethod
- def disable_editing_text_product(cls, obj):
- obj.BIMTextProperties.is_editing_product = False
+ def disable_editing_assigned_product(cls, obj):
+ obj.BIMAssignedProductProperties.is_editing_product = False
@classmethod
def enable_editing(cls, obj):
@@ -120,6 +133,10 @@ class Drawing(blenderbim.core.tool.Drawing):
def enable_editing_drawings(cls):
bpy.context.scene.DocProperties.is_editing_drawings = True
+ @classmethod
+ def enable_editing_schedules(cls):
+ bpy.context.scene.DocProperties.is_editing_schedules = True
+
@classmethod
def enable_editing_sheets(cls):
bpy.context.scene.DocProperties.is_editing_sheets = True
@@ -129,8 +146,8 @@ class Drawing(blenderbim.core.tool.Drawing):
obj.BIMTextProperties.is_editing = True
@classmethod
- def enable_editing_text_product(cls, obj):
- obj.BIMTextProperties.is_editing_product = True
+ def enable_editing_assigned_product(cls, obj):
+ obj.BIMAssignedProductProperties.is_editing_product = True
@classmethod
def ensure_unique_drawing_name(cls, name):
@@ -163,6 +180,18 @@ class Drawing(blenderbim.core.tool.Drawing):
def get_body_context(cls):
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
+ @classmethod
+ def get_document_uri(cls, document):
+ if hasattr(document, "Identification"):
+ name = document.Identification or "X"
+ else:
+ name = document.DocumentId or "X"
+ name += " - " + (document.Name or "Unnamed")
+ if document.Scope == "DOCUMENTATION":
+ return os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", name + ".svg")
+ elif document.Scope == "SCHEDULE":
+ return os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", name + ".svg")
+
@classmethod
def get_drawing_collection(cls, drawing):
obj = tool.Ifc.get_object(drawing)
@@ -176,7 +205,7 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def get_drawing_target_view(cls, drawing):
- return ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"]["TargetView"]
+ return ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("TargetView", "MODEL_VIEW")
@classmethod
def get_group_elements(cls, group):
@@ -196,13 +225,10 @@ class Drawing(blenderbim.core.tool.Drawing):
return element.Name
@classmethod
- def get_sheet_filename(cls, document):
- if hasattr(document, "Identification"):
- name = document.Identification or "X"
- else:
- name = document.DocumentId or "X"
- name += " - " + (document.Name or "Unnamed")
- return name
+ def get_schedule_location(cls, schedule):
+ if tool.Ifc.get_schema() == "IFC2X3":
+ return schedule.DocumentReferences[0].Location
+ return schedule.Location
@classmethod
def generate_drawing_matrix(cls, target_view, location_hint):
@@ -256,7 +282,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return items[0]
@classmethod
- def get_text_product(cls, element):
+ def get_assigned_product(cls, element):
for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"):
return rel.RelatingProduct
@@ -271,6 +297,19 @@ class Drawing(blenderbim.core.tool.Drawing):
new.name = drawing.Name or "Unnamed"
new.target_view = cls.get_drawing_target_view(drawing)
+ @classmethod
+ def import_schedules(cls):
+ bpy.context.scene.DocProperties.schedules.clear()
+ schedules = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SCHEDULE"]
+ for schedule in schedules:
+ new = bpy.context.scene.DocProperties.schedules.add()
+ new.ifc_definition_id = schedule.id()
+ new.name = schedule.Name or "Unnamed"
+ if tool.Ifc.get_schema() == "IFC2X3":
+ new.identification = schedule.DocumentId
+ else:
+ new.identification = schedule.Identification
+
@classmethod
def import_sheets(cls):
props = bpy.context.scene.DocProperties
@@ -302,9 +341,12 @@ class Drawing(blenderbim.core.tool.Drawing):
new.identification = reference.Identification or "X"
element = cls.get_reference_element(reference)
-
- new.name = element.Name
- new.reference_type = "DRAWING"
+ if element:
+ new.name = element.Name
+ new.reference_type = "DRAWING"
+ else:
+ new.name = cls.get_reference_document(reference).Name or "Unnamed"
+ new.reference_type = "SCHEDULE"
@classmethod
def import_text_attributes(cls, obj):
@@ -314,28 +356,33 @@ class Drawing(blenderbim.core.tool.Drawing):
blenderbim.bim.helper.import_attributes2(text, props.attributes)
@classmethod
- def import_text_product(cls, obj):
+ def import_assigned_product(cls, obj):
element = tool.Ifc.get_entity(obj)
- product = cls.get_text_product(element)
+ product = cls.get_assigned_product(element)
if product:
- obj.BIMTextProperties.relating_product = tool.Ifc.get_object(product)
+ obj.BIMAssignedProductProperties.relating_product = tool.Ifc.get_object(product)
else:
- obj.BIMTextProperties.relating_product = None
+ obj.BIMAssignedProductProperties.relating_product = None
@classmethod
def open_with_user_command(cls, user_command, path):
if user_command:
commands = eval(user_command)
for command in commands:
- subprocess.run(command)
+ subprocess.Popen(command)
else:
webbrowser.open("file://" + path)
@classmethod
- def open_svg(cls, filename):
+ def open_spreadsheet(cls, uri):
cls.open_with_user_command(
- bpy.context.preferences.addons["blenderbim"].preferences.svg_command,
- os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", filename + ".svg"),
+ bpy.context.preferences.addons["blenderbim"].preferences.spreadsheet_command, uri
+ )
+
+ @classmethod
+ def open_svg(cls, uri):
+ cls.open_with_user_command(
+ bpy.context.preferences.addons["blenderbim"].preferences.svg_command, uri
)
@classmethod
@@ -374,7 +421,7 @@ class Drawing(blenderbim.core.tool.Drawing):
if not element:
return
value = element.Literal
- product = cls.get_text_product(tool.Ifc.get_entity(obj))
+ product = cls.get_assigned_product(tool.Ifc.get_entity(obj))
if product:
selector = ifcopenshell.util.selector.Selector()
variables = {}
@@ -403,11 +450,17 @@ class Drawing(blenderbim.core.tool.Drawing):
if element in existing_references or element == drawing:
continue
if element.ObjectType == "DRAWING":
- psets = ifcopenshell.util.element.get_psets(element)
- if psets.get("EPset_Drawing", {}).get("TargetView", None) in ("SECTION_VIEW", "ELEVATION_VIEW"):
+ pset = ifcopenshell.util.element.get_psets(element).get("EPset_Drawing", {})
+ if pset.get("TargetView", None) in ("SECTION_VIEW", "ELEVATION_VIEW") and pset.get(
+ "GlobalReferencing", False
+ ):
elements.append(element)
for element in tool.Ifc.get().by_type("IfcGridAxis"):
elements.append(element)
+ target_view = tool.Drawing.get_drawing_target_view(drawing)
+ if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"):
+ for element in tool.Ifc.get().by_type("IfcBuildingStorey"):
+ elements.append(element)
return elements
@classmethod
@@ -437,6 +490,72 @@ class Drawing(blenderbim.core.tool.Drawing):
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
elif target_view == "SECTION_VIEW":
return cls.generate_section_reference_annotation(drawing, reference_element, context)
+ elif reference_element.is_a("IfcBuildingStorey"):
+ return cls.generate_storey_annotation(drawing, reference_element, context)
+
+ @classmethod
+ def generate_storey_annotation(cls, drawing, reference_element, context):
+ camera = tool.Ifc.get_object(drawing)
+ bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
+ reference_obj = tool.Ifc.get_object(reference_element)
+
+ def to_camera_coords(camera, reference_obj):
+ mat = reference_obj.matrix_world.copy()
+ xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
+ xyz[2] = 0
+ xyz = camera.matrix_world @ xyz
+ mat[0][3] = xyz[0]
+ mat[1][3] = xyz[1]
+ mat[2][3] = xyz[2]
+ annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
+ annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
+ mat[0][3] += annotation_offset[0]
+ mat[1][3] += annotation_offset[1]
+ mat[2][3] += annotation_offset[2]
+ return mat
+
+ def project_point_onto_camera(point, camera):
+ projection = camera.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
+ return camera.matrix_world.inverted() @ mathutils.geometry.intersect_line_plane(
+ point.xyz, point.xyz - projection, camera.location, projection
+ )
+
+ obj_matrix = to_camera_coords(camera, reference_obj)
+
+ if camera.data.BIMCameraProperties.raster_x > camera.data.BIMCameraProperties.raster_y:
+ width = camera.data.ortho_scale
+ height = width / camera.data.BIMCameraProperties.raster_x * camera.data.BIMCameraProperties.raster_y
+ else:
+ height = camera.data.ortho_scale
+ width = height / camera.data.BIMCameraProperties.raster_y * camera.data.BIMCameraProperties.raster_x
+
+ projection = project_point_onto_camera(reference_obj.location, camera)
+ co1 = camera.matrix_world @ mathutils.Vector((width / 2, projection[1], -1))
+ co2 = camera.matrix_world @ mathutils.Vector((-(width / 2), projection[1], -1))
+ co1 = obj_matrix.inverted() @ co1
+ co2 = obj_matrix.inverted() @ co2
+
+ data = bpy.data.curves.new("Annotation", type="CURVE")
+ data.dimensions = "3D"
+ data.resolution_u = 2
+
+ polyline = data.splines.new("POLY")
+ polyline.points.add(1)
+ polyline.points[-2].co = list(co1) + [1]
+ polyline.points[-1].co = list(co2) + [1]
+
+ obj = bpy.data.objects.new(reference_obj.name, data)
+ obj.matrix_world = obj_matrix
+
+ element = cls.run_root_assign_class(
+ obj=obj,
+ ifc_class="IfcAnnotation",
+ predefined_type="SECTION_LEVEL",
+ should_add_representation=True,
+ context=context,
+ ifc_representation_class=None,
+ )
+ return element
@classmethod
def generate_section_reference_annotation(cls, drawing, reference_element, context):
@@ -723,14 +842,48 @@ class Drawing(blenderbim.core.tool.Drawing):
@classmethod
def get_reference_element(cls, reference):
if tool.Ifc.get_schema() == "IFC2X3":
- return [
- r for r in tool.Ifc.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference
- ][0].RelatedObjects[0]
- return reference.DocumentRefForObjects[0].RelatedObjects[0]
+ refs = [r for r in tool.Ifc.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == reference]
+ else:
+ refs = reference.DocumentRefForObjects
+ if refs:
+ return refs[0].RelatedObjects[0]
@classmethod
def get_drawing_human_scale(cls, drawing):
- return ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"]["HumanScale"]
+ return ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("HumanScale", "NTS")
+
+ @classmethod
+ def get_drawing_metadata(cls, drawing):
+ return [
+ v.strip()
+ for v in ifcopenshell.util.element.get_psets(drawing)["EPset_Drawing"].get("Metadata", "").split(",")
+ ]
+
+ @classmethod
+ def get_annotation_z_index(cls, drawing):
+ return ifcopenshell.util.element.get_psets(drawing).get("EPset_Annotation", {}).get("ZIndex", 0)
+
+ @classmethod
+ def get_annotation_symbol(cls, drawing):
+ return ifcopenshell.util.element.get_psets(drawing).get("EPset_Annotation", {}).get("Symbol", None)
+
+ @classmethod
+ def has_linework(cls, drawing):
+ return ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}).get("HasLinework", False)
+
+ @classmethod
+ def get_drawing_elements(cls, drawing):
+ pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
+ include = pset.get("Include", None)
+ if include:
+ elements = set(ifcopenshell.util.selector.Selector.parse(tool.Ifc.get(), include))
+ else:
+ elements = set(tool.Ifc.get().by_type("IfcElement"))
+ exclude = pset.get("Exclude", None)
+ if exclude:
+ elements -= set(ifcopenshell.util.selector.Selector.parse(tool.Ifc.get(), exclude, elements=elements))
+ elements -= set(tool.Ifc.get().by_type("IfcOpeningElement"))
+ return elements
@classmethod
def get_annotation_element(cls, element):
@@ -745,7 +898,7 @@ class Drawing(blenderbim.core.tool.Drawing):
return rel.RelatingDocument
@classmethod
- def get_reference_sheet(cls, reference):
+ def get_reference_document(cls, reference):
if tool.Ifc.get_schema() == "IFC2X3":
return reference.ReferenceToDocument[0]
return reference.ReferencedDocument
diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py
index ed146e454f..15be6ddef3 100644
--- a/src/blenderbim/blenderbim/tool/geometry.py
+++ b/src/blenderbim/blenderbim/tool/geometry.py
@@ -75,10 +75,6 @@ class Geometry(blenderbim.core.tool.Geometry):
def delete_data(cls, data):
bpy.data.meshes.remove(data)
- @classmethod
- def does_object_have_mesh_with_faces(cls, obj):
- return bool(isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons))
-
@classmethod
def does_representation_id_exist(cls, representation_id):
try:
@@ -202,9 +198,16 @@ class Geometry(blenderbim.core.tool.Geometry):
ifc_importer = blenderbim.bim.import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = tool.Ifc.get()
- mesh = ifc_importer.create_mesh(element, shape)
- ifc_importer.material_creator.load_existing_materials()
- ifc_importer.material_creator.create(element, obj, mesh)
+
+ if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
+ mesh = ifc_importer.create_camera(element, shape)
+ if element.is_a("IfcAnnotation") and ifc_importer.is_curve_annotation(element):
+ mesh = ifc_importer.create_curve(element, shape)
+ elif shape:
+ mesh = ifc_importer.create_mesh(element, shape)
+ ifc_importer.material_creator.load_existing_materials()
+ ifc_importer.material_creator.create(element, obj, mesh)
+
return mesh
@classmethod
diff --git a/src/blenderbim/blenderbim/tool/georeference.py b/src/blenderbim/blenderbim/tool/georeference.py
new file mode 100644
index 0000000000..2c92eb2729
--- /dev/null
+++ b/src/blenderbim/blenderbim/tool/georeference.py
@@ -0,0 +1,257 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of BlenderBIM Add-on.
+#
+# BlenderBIM Add-on is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# BlenderBIM Add-on is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with BlenderBIM Add-on. If not, see .
+
+import bpy
+import json
+import blenderbim.core.tool
+import blenderbim.tool as tool
+import ifcopenshell
+import blenderbim.bim.helper
+from math import radians, degrees, atan, tan, cos, sin
+
+
+class Georeference(blenderbim.core.tool.Georeference):
+ @classmethod
+ def import_projected_crs(cls):
+ def callback(name, prop, data):
+ if name == "MapUnit":
+ new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
+ new.name = name
+ new.data_type = "enum"
+ new.is_null = data[name] is None
+ new.is_optional = True
+ new.enum_items = json.dumps(
+ {
+ u.id(): (getattr(u, "Prefix", "") or "") + u.Name
+ for u in tool.Ifc.get().by_type("IfcNamedUnit")
+ if u.UnitType == "LENGTHUNIT"
+ }
+ )
+ if data["MapUnit"]:
+ new.enum_value = str(data["MapUnit"].id())
+ return True
+
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ props.projected_crs.clear()
+
+ if tool.Ifc.get_schema() == "IFC2X3":
+ return
+
+ for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
+ if context.HasCoordinateOperation:
+ projected_crs = context.HasCoordinateOperation[0].TargetCRS
+ blenderbim.bim.helper.import_attributes2(projected_crs, props.projected_crs, callback=callback)
+ return
+
+ @classmethod
+ def import_map_conversion(cls):
+ def callback(name, prop, data):
+ if name not in ["SourceCRS", "TargetCRS"]:
+ # Enforce a string data type to prevent data loss in single-precision Blender props
+ prop.data_type = "string"
+ prop.string_value = "" if prop.is_null else str(data[name])
+ return True
+
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ props.map_conversion.clear()
+
+ if tool.Ifc.get_schema() == "IFC2X3":
+ return
+
+ for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
+ if context.HasCoordinateOperation:
+ map_conversion = context.HasCoordinateOperation[0]
+ blenderbim.bim.helper.import_attributes2(map_conversion, props.map_conversion, callback=callback)
+ return
+
+ @classmethod
+ def import_true_north(cls):
+ if tool.Ifc.get_schema() == "IFC2X3":
+ return
+
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ props.has_true_north = False
+ for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
+ if context.TrueNorth:
+ true_north = context.TrueNorth.DirectionRatios
+ props.true_north_abscissa = str(true_north[0])
+ props.true_north_ordinate = str(true_north[1])
+ props.has_true_north = True
+ return
+
+ @classmethod
+ def get_projected_crs_attributes(cls):
+ def callback(attributes, prop):
+ if not prop.is_null and prop.name == "MapUnit":
+ attributes[prop.name] = tool.Ifc.get().by_id(int(prop.enum_value))
+ return True
+
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ return blenderbim.bim.helper.export_attributes(props.projected_crs, callback=callback)
+
+ @classmethod
+ def get_map_conversion_attributes(cls):
+ def callback(attributes, prop):
+ if not prop.is_null and prop.data_type == "string":
+ # We store our floats as string to prevent single precision data loss
+ attributes[prop.name] = float(prop.string_value)
+ return True
+
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ return blenderbim.bim.helper.export_attributes(props.map_conversion, callback=callback)
+
+ @classmethod
+ def get_true_north_attributes(cls):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ if props.has_true_north:
+ try:
+ return [float(props.true_north_abscissa), float(props.true_north_ordinate)]
+ except ValueError:
+ print("ERROR, True North Abscissa and Ordinate expect a number")
+ # self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number")
+
+ @classmethod
+ def enable_editing(cls):
+ bpy.context.scene.BIMGeoreferenceProperties.is_editing = True
+
+ @classmethod
+ def disable_editing(cls):
+ bpy.context.scene.BIMGeoreferenceProperties.is_editing = False
+
+ @classmethod
+ def set_coordinates(cls, io, coordinates):
+ if io == "input":
+ bpy.context.scene.BIMGeoreferenceProperties.coordinate_input = ",".join([str(o) for o in coordinates])
+ elif io == "output":
+ bpy.context.scene.BIMGeoreferenceProperties.coordinate_output = ",".join([str(o) for o in coordinates])
+
+ @classmethod
+ def get_coordinates(cls, io):
+ if io == "input":
+ return [float(co) for co in bpy.context.scene.BIMGeoreferenceProperties.coordinate_input.split(",")]
+ elif io == "output":
+ return [float(co) for co in bpy.context.scene.BIMGeoreferenceProperties.coordinate_output.split(",")]
+
+ @classmethod
+ def get_cursor_location(cls):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ return [o / scale for o in bpy.context.scene.cursor.location]
+
+ @classmethod
+ def set_cursor_location(cls, coordinates):
+ scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ bpy.context.scene.cursor.location = [co * scale for co in coordinates]
+
+ @classmethod
+ def set_ifc_true_north(cls):
+ y_angle = -bpy.context.scene.sun_pos_properties.north_offset + radians(90)
+ bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = str(cos(y_angle))
+ bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = str(sin(y_angle))
+
+ @classmethod
+ def set_blender_true_north(cls):
+ bpy.context.scene.sun_pos_properties.north_offset = -radians(
+ ifcopenshell.util.geolocation.yaxis2angle(
+ float(bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa),
+ float(bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate),
+ )
+ )
+
+ @classmethod
+ def get_map_conversion(cls):
+ if tool.Ifc.get_schema() == "IFC2X3":
+ return
+ for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
+ if context.HasCoordinateOperation:
+ return context.HasCoordinateOperation[0]
+
+ @classmethod
+ def xyz2enh(cls, coordinates, map_conversion):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ if props.has_blender_offset:
+ coordinates = ifcopenshell.util.geolocation.xyz2enh(
+ coordinates[0],
+ coordinates[1],
+ coordinates[2],
+ float(props.blender_eastings),
+ float(props.blender_northings),
+ float(props.blender_orthogonal_height),
+ float(props.blender_x_axis_abscissa),
+ float(props.blender_x_axis_ordinate),
+ 1.0,
+ )
+ # TODO: what if the project CRS units and the project units are different?
+ if map_conversion:
+ coordinates = ifcopenshell.util.geolocation.xyz2enh(
+ coordinates[0],
+ coordinates[1],
+ coordinates[2],
+ map_conversion.Eastings,
+ map_conversion.Northings,
+ map_conversion.OrthogonalHeight,
+ map_conversion.XAxisAbscissa or 1.0,
+ map_conversion.XAxisOrdinate or 0.0,
+ map_conversion.Scale or 1.0,
+ )
+ return coordinates
+
+ @classmethod
+ def enh2xyz(cls, coordinates, map_conversion):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ if map_conversion:
+ coordinates = ifcopenshell.util.geolocation.enh2xyz(
+ coordinates[0],
+ coordinates[1],
+ coordinates[2],
+ map_conversion.Eastings,
+ map_conversion.Northings,
+ map_conversion.OrthogonalHeight,
+ map_conversion.XAxisAbscissa or 1.0,
+ map_conversion.XAxisOrdinate or 0.0,
+ map_conversion.Scale or 1.0,
+ )
+ if props.has_blender_offset:
+ coordinates = ifcopenshell.util.geolocation.enh2xyz(
+ coordinates[0],
+ coordinates[1],
+ coordinates[2],
+ float(props.blender_eastings),
+ float(props.blender_northings),
+ float(props.blender_orthogonal_height),
+ float(props.blender_x_axis_abscissa),
+ float(props.blender_x_axis_ordinate),
+ 1.0,
+ )
+ return coordinates
+
+ @classmethod
+ def set_ifc_grid_north(cls):
+ x_angle = bpy.context.scene.sun_pos_properties.north_offset
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ props.map_conversion.get("XAxisAbscissa").string_value = str(cos(x_angle))
+ props.map_conversion.get("XAxisOrdinate").string_value = str(sin(x_angle))
+
+ @classmethod
+ def set_blender_grid_north(cls):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ angle = ifcopenshell.util.geolocation.xaxis2angle(
+ float(props.map_conversion.get("XAxisAbscissa").string_value),
+ float(props.map_conversion.get("XAxisOrdinate").string_value),
+ )
+ bpy.context.scene.sun_pos_properties.north_offset = -radians(angle)
diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py
index d487aafbcb..efc7e09e35 100644
--- a/src/blenderbim/blenderbim/tool/ifc.py
+++ b/src/blenderbim/blenderbim/tool/ifc.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see .
+import os
import bpy
import numpy as np
import ifcopenshell.api
@@ -91,6 +92,10 @@ class Ifc(blenderbim.core.tool.Ifc):
def delete(cls, element):
IfcStore.delete_element(element)
+ @classmethod
+ def resolve_uri(cls, uri):
+ return uri if not uri or os.path.isabs(uri) else os.path.join(os.path.dirname(cls.get_path()), uri)
+
@classmethod
def unlink(cls, element=None, obj=None):
IfcStore.unlink_element(element, obj)
diff --git a/src/blenderbim/blenderbim/tool/material.py b/src/blenderbim/blenderbim/tool/material.py
index 9ac4345587..0b3832ca24 100644
--- a/src/blenderbim/blenderbim/tool/material.py
+++ b/src/blenderbim/blenderbim/tool/material.py
@@ -46,7 +46,7 @@ class Material(blenderbim.core.tool.Material):
@classmethod
def get_elements_by_material(cls, material):
- return set(ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material))
+ return ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material)
@classmethod
def get_name(cls, obj):
@@ -55,17 +55,33 @@ class Material(blenderbim.core.tool.Material):
@classmethod
def import_material_definitions(cls, material_type):
props = bpy.context.scene.BIMMaterialProperties
+ expanded_categories = {m.name for m in props.materials if m.is_expanded}
props.materials.clear()
- for material in tool.Ifc.get().by_type(material_type):
+ get_name = lambda x: x.Name or "Unnamed"
+ if material_type == "IfcMaterialLayerSet":
+ get_name = lambda x: x.LayerSetName or "Unnamed"
+ elif material_type == "IfcMaterialList":
+ get_name = lambda x: "Unnamed"
+ materials = sorted(tool.Ifc.get().by_type(material_type), key=get_name)
+ categories = {}
+ if material_type == "IfcMaterial":
+ [categories.setdefault(m.Category, []).append(m) for m in materials]
+ for category, mats in categories.items():
+ cat = props.materials.add()
+ cat.name = category or ""
+ cat.is_category = True
+ cat.is_expanded = cat.name in expanded_categories
+ for material in mats if cat.is_expanded else []:
+ new = props.materials.add()
+ new.ifc_definition_id = material.id()
+ new.name = get_name(material)
+ new.total_elements = len(ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material))
+ return
+ for material in materials:
new = props.materials.add()
new.ifc_definition_id = material.id()
- if material.is_a("IfcMaterialLayerSet"):
- new.name = material.LayerSetName or "Unnamed"
- elif material.is_a("IfcMaterialList"):
- new.name = "Unnamed"
- else:
- new.name = material.Name or "Unnamed"
- new.total_elements = len(set(ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material)))
+ new.name = get_name(material)
+ new.total_elements = len(ifcopenshell.util.element.get_elements_by_material(tool.Ifc.get(), material))
@classmethod
def is_editing_materials(cls):
diff --git a/src/blenderbim/blenderbim/tool/project.py b/src/blenderbim/blenderbim/tool/project.py
index 0670af4d6c..850c7aab24 100644
--- a/src/blenderbim/blenderbim/tool/project.py
+++ b/src/blenderbim/blenderbim/tool/project.py
@@ -102,7 +102,7 @@ class Project(blenderbim.core.tool.Project):
while queue:
layer = queue.pop()
- if layer.name == collection.name:
+ if layer.collection == collection:
layer_collection = layer
break
queue.extend(list(layer.children))
diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py
index 3225907780..cf1fbecb20 100644
--- a/src/blenderbim/blenderbim/tool/root.py
+++ b/src/blenderbim/blenderbim/tool/root.py
@@ -58,6 +58,9 @@ class Root(blenderbim.core.tool.Root):
def get_object_representation(cls, obj):
if obj.data and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ element = tool.Ifc.get_entity(obj)
+ if not obj.data and getattr(element, "ObjectType", None) == "TEXT":
+ return element.Representation.Representations[0]
@classmethod
def get_representation_context(cls, representation):
diff --git a/src/blenderbim/blenderbim/tool/style.py b/src/blenderbim/blenderbim/tool/style.py
index caa87ff36c..a5031d5c60 100644
--- a/src/blenderbim/blenderbim/tool/style.py
+++ b/src/blenderbim/blenderbim/tool/style.py
@@ -56,6 +56,10 @@ class Style(blenderbim.core.tool.Style):
def get_context(cls, obj):
return ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
+ @classmethod
+ def get_elements_by_style(cls, style):
+ return ifcopenshell.util.element.get_elements_by_style(tool.Ifc.get(), style)
+
@classmethod
def get_name(cls, obj):
return obj.name
@@ -183,10 +187,12 @@ class Style(blenderbim.core.tool.Style):
def import_presentation_styles(cls, style_type):
props = bpy.context.scene.BIMStylesProperties
props.styles.clear()
- for style in tool.Ifc.get().by_type(style_type):
+ styles = sorted(tool.Ifc.get().by_type(style_type), key=lambda x: x.Name or "Unnamed")
+ for style in styles:
new = props.styles.add()
new.ifc_definition_id = style.id()
new.name = style.Name or "Unnamed"
+ new.total_elements = len(ifcopenshell.util.element.get_elements_by_style(tool.Ifc.get(), style))
@classmethod
def import_surface_attributes(cls, style, obj):
@@ -196,3 +202,10 @@ class Style(blenderbim.core.tool.Style):
@classmethod
def is_editing_styles(cls):
return bpy.context.scene.BIMStylesProperties.is_editing
+
+ @classmethod
+ def select_elements(cls, elements):
+ for element in elements:
+ obj = tool.Ifc.get_object(element)
+ if obj:
+ obj.select_set(True)
diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py
index b2109330a1..74ec903033 100644
--- a/src/blenderbim/blenderbim/tool/system.py
+++ b/src/blenderbim/blenderbim/tool/system.py
@@ -81,9 +81,10 @@ class System(blenderbim.core.tool.System):
new.ifc_class = system.is_a()
@classmethod
- def load_ports(cls, ports):
+ def load_ports(cls, element, ports):
if not ports:
return
+ obj = tool.Ifc.get_object(element)
ifc_import_settings = import_ifc.IfcImportSettings.factory()
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = tool.Ifc.get()
@@ -93,9 +94,16 @@ class System(blenderbim.core.tool.System):
for port in ports or []:
if tool.Ifc.get_object(port):
continue
- ifc_importer.create_product(port)
+ port_obj = ifc_importer.create_product(port)
+ if obj:
+ port_obj.parent = obj
+ port_obj.matrix_parent_inverse = obj.matrix_world.inverted()
ifc_importer.place_objects_in_collections()
+ @classmethod
+ def run_geometry_edit_object_placement(cls, obj=None):
+ return blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
+
@classmethod
def run_root_assign_class(
cls,
diff --git a/src/blenderbim/blenderbim/tool/unit.py b/src/blenderbim/blenderbim/tool/unit.py
index e377592028..ebd9f1c152 100644
--- a/src/blenderbim/blenderbim/tool/unit.py
+++ b/src/blenderbim/blenderbim/tool/unit.py
@@ -87,10 +87,6 @@ class Unit(blenderbim.core.tool.Unit):
def get_si_name_from_unit_type(cls, unit_type):
return ifcopenshell.util.unit.si_type_names.get(unit_type, None)
- @classmethod
- def get_unit_class(cls, unit):
- return unit.is_a()
-
@classmethod
def import_unit_attributes(cls, unit):
def callback(name, prop, data):
@@ -152,6 +148,10 @@ class Unit(blenderbim.core.tool.Unit):
def is_scene_unit_metric(cls):
return bpy.context.scene.unit_settings.system in ["METRIC", "NONE"]
+ @classmethod
+ def is_unit_class(cls, unit, ifc_class):
+ return unit.is_a(ifc_class)
+
@classmethod
def set_active_unit(cls, unit):
bpy.context.scene.BIMUnitProperties.active_unit_id = unit.id()
diff --git a/src/blenderbim/docs/_static/custom.css b/src/blenderbim/docs/_static/custom.css
new file mode 100644
index 0000000000..4ecfae2065
--- /dev/null
+++ b/src/blenderbim/docs/_static/custom.css
@@ -0,0 +1,17 @@
+.blockbutton {
+ border: 1px solid var(--color-admonition-title--seealso);
+ background-color: var(--color-admonition-title-background--seealso);
+ padding: 5px;
+ text-align: center;
+ border-radius: 5px;
+ max-width: 500px;
+ margin-left: auto;
+ margin-right: auto;
+}
+section img {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
+ border-radius: 5px;
+}
diff --git a/src/blenderbim/docs/blenderbim.rst b/src/blenderbim/docs/blenderbim.rst
deleted file mode 100644
index 3beb554aca..0000000000
--- a/src/blenderbim/docs/blenderbim.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-BlenderBIM
-==========
-
-BlenderBIM is an add-on to Blender that turns Blender into a powerful BIM
-authoring program, supporting IFC import, export, and data manipulation.
-
-This documentation is free software! You are free to contribute and help write
-this document.
-
-.. toctree::
- :maxdepth: 1
- :caption: Contents:
-
- blenderbim/installation
- blenderbim/developer_guide
- blenderbim/undo_system
- blenderbim/georeferencing
- blenderbim/running_tests
- blenderbim/scene
-
-
-Indices and tables
-------------------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/src/blenderbim/docs/blenderbim/georeferencing.rst b/src/blenderbim/docs/blenderbim/georeferencing.rst
deleted file mode 100644
index 35843c2c9e..0000000000
--- a/src/blenderbim/docs/blenderbim/georeferencing.rst
+++ /dev/null
@@ -1,72 +0,0 @@
-Georeferencing
-==============
-
-In the AEC industry, works in the built environment are split between vertical
-construction (such as buildings and sites), and horizontal construction (such as
-transit, transmission, and subterranean networks). Blender and the BlenderBIM
-Add-on is only suitable for vertical construction.
-
-IFC4 onwards supports georeferencing. The BlenderBIM Add-on has full support for
-IFC georeferencing. Here are the potential scenarios you will encounter in the
-wild for vertical construction.
-
-1. Correctly georeferenced IFC4 with a map conversion transformation
-2. Correctly georeferenced IFC4 without a map conversion transformation
-3. Non-georeferenced georeferenced IFC4
-4. Non-georeferenced IFC2X3
-
-The first scenario is desired for most disciplines, such as architects and all
-engineers (except for civil).
-
-For scenarios 2, 3, and 4, the BlenderBIM Add-on will automatically attempt to
-offset coordinates to preserve the precision of the model. The first coordinate
-greater than 1km will be detected and used as an offset coordinate. It is
-possible to specify a custom offset coordinate.
-
-Non-georeferenced workarounds
------------------------------
-
-The unfortunate reality is that IFC2X3 does not support georeferencing, and most
-vendors have poor and inconsistent support for georeferencing, even in IFC4.
-Some users may be under the impression that their file is correctly
-georeferenced, but this is rarely the case. This creates problems. You can see
-whether your file is correctly georeferenced in the ``IFC Georeferencing``
-scene panel. If you see "Not Georeferenced", your file is not correctly
-georeferenced. If your file is georeferenced, it is no guarantee that the
-georeferencing data is actually correct, but how to determine this is out of
-scope of this article.
-
-As an invalid workaround for correct georeferencing, many other BIM vendors
-simply offset their object coordinates from local engineering to map coordinates
-without properly storing a projection system and optionally a map
-transformation. There are two common invalid workarounds that are used.
-
-The first invalid workaround is to shift the origin point of objects in the
-model. We call this the ``OBJECT_PLACEMENT`` workaround. The second invalid
-workaround is to shift the coordinates of geometry within the objects
-themselves, and typically leave the object origin untouched (such as back at (0,
-0, 0)). We call this the ``CARTESIAN_POINT`` workaround. Sometimes, BIM
-applications combine both of these invalid workarounds. To see which workaround
-was used on an object, check the "Blender Offset" property in the ``Transform``
-object panel.
-
-Coordinates and precision limits
---------------------------------
-
-Blender, and subsequently the BlenderBIM Add-on, is not designed for map
-coordinate systems. Blender internally uses single precision floating point
-calculations. A full description of the precision implications are described in
-the `Blender working limits documentation
-`__.
-
-From a software perspective, lengths greater than 5,000 meters start to
-accumulate precision errors that affect the nearest millimeter. Therefore, from
-a software perspective, it is unwise to embark on a project with coordinates
-ranging greater than +/- 5km.
-
-However, if working in local engineering coordinates, a single transformation is
-required to convert from local engineering coordinates to map coordinates. This
-transformation includes a scale factor. The scale factor is only assumed to be
-constant for small sites (defined approximately as less than 1km square).
-This practical limit of georeferenced vertical construction is smaller than the
-software limit, so this is the actual limiting factor.
diff --git a/src/blenderbim/docs/conf.py b/src/blenderbim/docs/conf.py
index 62b5dea99e..e3c6526151 100644
--- a/src/blenderbim/docs/conf.py
+++ b/src/blenderbim/docs/conf.py
@@ -35,12 +35,12 @@
# -- Project information -----------------------------------------------------
-project = "IfcOpenShell"
-copyright = "2020, IfcOpenShell Contributors"
+project = "BlenderBIM Add-on"
+copyright = "2020-2022 IfcOpenShell Contributors"
author = "IfcOpenShell Contributors"
# The full version, including alpha/beta/rc tags
-release = "0.0.1"
+release = "0.0.220504"
# -- General configuration ---------------------------------------------------
@@ -64,9 +64,11 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = "nature"
+html_theme = "furo"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
+
+html_css_files = ["custom.css"]
diff --git a/src/blenderbim/docs/blenderbim/architecture.png b/src/blenderbim/docs/devs/architecture.png
similarity index 100%
rename from src/blenderbim/docs/blenderbim/architecture.png
rename to src/blenderbim/docs/devs/architecture.png
diff --git a/src/blenderbim/docs/blenderbim/demo-module.png b/src/blenderbim/docs/devs/demo-module.png
similarity index 100%
rename from src/blenderbim/docs/blenderbim/demo-module.png
rename to src/blenderbim/docs/devs/demo-module.png
diff --git a/src/blenderbim/docs/blenderbim/diagrams.svg b/src/blenderbim/docs/devs/diagrams.svg
similarity index 100%
rename from src/blenderbim/docs/blenderbim/diagrams.svg
rename to src/blenderbim/docs/devs/diagrams.svg
diff --git a/src/blenderbim/docs/devs/documentation-style.png b/src/blenderbim/docs/devs/documentation-style.png
new file mode 100644
index 0000000000..75f2596c62
Binary files /dev/null and b/src/blenderbim/docs/devs/documentation-style.png differ
diff --git a/src/blenderbim/docs/blenderbim/developer_guide.rst b/src/blenderbim/docs/devs/hello_world.rst
similarity index 99%
rename from src/blenderbim/docs/blenderbim/developer_guide.rst
rename to src/blenderbim/docs/devs/hello_world.rst
index 89e20e53fd..8e88985edc 100644
--- a/src/blenderbim/docs/blenderbim/developer_guide.rst
+++ b/src/blenderbim/docs/devs/hello_world.rst
@@ -1,5 +1,5 @@
-Developer guide
-===============
+Hello, world!
+=============
The BlenderBIM Add-on takes a unique approach to authoring BIM data. Traditional
BIM authoring apps create features that are tailored for a single discipline's
diff --git a/src/blenderbim/docs/blenderbim/ifcopenshell-architecture.png b/src/blenderbim/docs/devs/ifcopenshell-architecture.png
similarity index 100%
rename from src/blenderbim/docs/blenderbim/ifcopenshell-architecture.png
rename to src/blenderbim/docs/devs/ifcopenshell-architecture.png
diff --git a/src/blenderbim/docs/blenderbim/installation.rst b/src/blenderbim/docs/devs/installation.rst
similarity index 71%
rename from src/blenderbim/docs/blenderbim/installation.rst
rename to src/blenderbim/docs/devs/installation.rst
index dd13288af0..1f0979fb4c 100644
--- a/src/blenderbim/docs/blenderbim/installation.rst
+++ b/src/blenderbim/docs/devs/installation.rst
@@ -1,28 +1,12 @@
-.. _blenderbim/installation:
-
Installation
============
There are different methods of installation, depending on your situation.
-1. **Stable installation** is recommended for regular users.
-2. **Unstable installation** is recommended for power users helping with testing.
-3. **Building from source** is recommended for distributing a build from source.
-4. **Live development environment** is recommended for developers who are actively coding.
-5. **Distro installation** is recommended for those who use a Linux package manager.
-
-Stable installation
--------------------
-
-The BlenderBIM Add-on is packaged like a regular Blender add-on, so installation
-is the same as any other Blender add-on. The full instructions for end-user
-installation is available at the `Get BlenderBIM
-`__ website. The latest release is
-typically updated every few weeks.
-
-Like all Blender add-ons, they can be installed using ``Edit > Preferences >
-Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox``. You can
-enable add-ons permanently by using ``Save User Settings`` from the Addons menu.
+1. **Unstable installation** is recommended for power users helping with testing.
+2. **Building from source** is recommended for distributing a build from source.
+3. **Live development environment** is recommended for developers who are actively coding.
+4. **Distro installation** is recommended for those who use a Linux package manager.
Unstable installation
---------------------
@@ -31,7 +15,7 @@ Unstable installation
that they are typically updated every day. Simply download a daily build from
the `Github releases page
`__, then follow the same
-instructions as a packaged installation.
+instructions as the **Stable installation**.
You will need to choose which build to download.
@@ -137,6 +121,13 @@ Blender for the changes to take effect. In ``Edit > Preferences > Add-ons`` you
will see that the version number of the BlenderBIM Add-on has changed to
``0.0.999999``, which represents an un-versioned BlenderBIM Add-on.
+There is a useful Blender Addon that adds a Reboot button in File menù.
+In this way, it's possible to directly restart Blender and test the modified source code.
+The add on is available `here `__
+(check also the related forum discussion `here `__)
+There is also a VS Code called `Blender Development `__ that has a similar functionality.
+
+
Distro installation
-------------------
@@ -219,65 +210,3 @@ Required static assets are:
bim/data/gantt/jsgantt.js (from jsgantt-improved)
bim/data/gantt/jsgantt.css (from jsgantt-improved)
-
-Where is the BlenderBIM Add-on installed?
------------------------------------------
-
-If you downloaded Blender as a ``.zip`` file without running an installer, you
-will find the BlenderBIM Add-on installed in the following directory, where
-``X.XX`` is the Blender version:
-::
-
- /path/to/blender/X.XX/scripts/addons/
-
-Otherwise, if you installed Blender using an installation package, the add-ons
-folder depends on which operating system you use. On Linux:
-::
-
- ~/.config/blender/X.XX/scripts/addons/
-
-On Mac:
-::
-
- /Users/{YOUR_USER}/Library/Application Support/Blender/X.XX/
-
-On Windows:
-::
-
- C:\Users\{YOUR_USER}\AppData\Roaming\Blender Foundation\X.XX\scripts\addons
-
-Upon installation, the BlenderBIM Add-on is stored in the ``blenderbim/``
-directory.
-
-Updating
---------
-
-First uninstall the current BlenderBIM add-on, then install the latest version.
-
-Uninstalling
-------------
-
-Navigate to ``Edit > Preferences > Add-ons``. Due to a limitation in Blender,
-you have to first disable the BlenderBIM Add-on in your Blender preferences by
-pressing the checkbox next to the add-on, then restart Blender. After
-restarting, you can uninstall the BlenderBIM Add-on by pressing the ``Remove``
-button in the Blender preferences window.
-
-Alternatively, you may uninstall manually by deleting the ``blenderbim/``
-directory in your Blender add-ons directory.
-
-FAQ
----
-
-1. I get an error similar to "ImportError: IfcOpenShell not built for 'linux/64bit/python3.7'"
-
-Check which BlenderBIM Add-on build you are using. The zip will have either
-``py37``, ``py39``, or ``py310`` in the name. See the instructions in the
-**Unstable installation** section to check that you have installed the correct
-version.
-
-2. I am on Ubuntu and get an error similar to "ImportError: /lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29 not found"
-
-Our latest package which uses IfcOpenShell v0.7.0 is built using Ubuntu 20 LTS.
-If you have an older Ubuntu version, you can either upgrade to 19.10 or above,
-or you'll need to compile IfcOpenShell yourself.
diff --git a/src/blenderbim/docs/blenderbim/module-architecture.png b/src/blenderbim/docs/devs/module-architecture.png
similarity index 100%
rename from src/blenderbim/docs/blenderbim/module-architecture.png
rename to src/blenderbim/docs/devs/module-architecture.png
diff --git a/src/blenderbim/docs/blenderbim/native-openbim.png b/src/blenderbim/docs/devs/native-openbim.png
similarity index 100%
rename from src/blenderbim/docs/blenderbim/native-openbim.png
rename to src/blenderbim/docs/devs/native-openbim.png
diff --git a/src/blenderbim/docs/blenderbim/running_tests.rst b/src/blenderbim/docs/devs/running_tests.rst
similarity index 87%
rename from src/blenderbim/docs/blenderbim/running_tests.rst
rename to src/blenderbim/docs/devs/running_tests.rst
index 714cb0082a..165ddd1027 100644
--- a/src/blenderbim/docs/blenderbim/running_tests.rst
+++ b/src/blenderbim/docs/devs/running_tests.rst
@@ -11,7 +11,7 @@ smoke tests. These tests use ``pytest`` as the test framework and runner.
All development is expected to use test driven development, and so we expect
test coverage to be 100% where it is technically possible to test.
-When running tests, a Makefile target is provided for convenience so you can
+When running tests, Makefile targets are provided for convenience so you can
type in a simple command without knowing the internals.
Because the BlenderBIM Add-on depends on IfcOpenShell, it is advised to also run
@@ -57,13 +57,14 @@ of all concrete functions.
.. code-block:: bash
$ cd src/blenderbim/
- $ make test-tool
+ $ make test-tool # Test everything
+ $ make test-tool MODULE=foo # Only test a single module
BlenderBIM Add-on tests
-----------------------
The BIM layer acts as a full integration test. It is not possible to fully test
-the UI, as we cannot reliable emit interface signals, nor read the interface as
+the UI, as we cannot reliably emit interface signals, nor read the interface as
a DOM of sorts. The best we can do is to call Blender operators as a smoke test,
and also check simple property and scene changes. It has the same dependencies
as the tool tests.
@@ -71,4 +72,5 @@ as the tool tests.
.. code-block:: bash
$ cd src/blenderbim/
- $ make test-bim
+ $ make test-bim # Test everything
+ $ make test-bim MODULE=foo # Only test a single module
diff --git a/src/blenderbim/docs/blenderbim/undo_system.rst b/src/blenderbim/docs/devs/undo_system.rst
similarity index 100%
rename from src/blenderbim/docs/blenderbim/undo_system.rst
rename to src/blenderbim/docs/devs/undo_system.rst
diff --git a/src/blenderbim/docs/blenderbim/ux_guidelines.rst b/src/blenderbim/docs/devs/ux_guidelines.rst
similarity index 100%
rename from src/blenderbim/docs/blenderbim/ux_guidelines.rst
rename to src/blenderbim/docs/devs/ux_guidelines.rst
diff --git a/src/blenderbim/docs/devs/writing_docs.rst b/src/blenderbim/docs/devs/writing_docs.rst
new file mode 100644
index 0000000000..279111bb3f
--- /dev/null
+++ b/src/blenderbim/docs/devs/writing_docs.rst
@@ -0,0 +1,9 @@
+Writing docs
+============
+
+The following colours and annotation styles should be used for annotating
+images. All stroke widths are 3px with a corner radius of 3px. Horizonal
+underlines are 5px with a corner radius of 2px. The dark green is **39b54a** and
+the light green is **d9e021**.
+
+.. image:: documentation-style.png
diff --git a/src/blenderbim/docs/ifcopenshell-python.rst b/src/blenderbim/docs/ifcopenshell-python.rst
deleted file mode 100644
index 1f661a3488..0000000000
--- a/src/blenderbim/docs/ifcopenshell-python.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-IfcOpenShell-Python
-===================
-
-This documentation is free software! You are free to contribute and help write
-this document.
-
-.. toctree::
- :maxdepth: 1
- :caption: Contents:
-
- ifcopenshell-python/quickstart
- ifcopenshell-python/api-documentation
-
-Indices and tables
-------------------
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/src/blenderbim/docs/ifcopenshell-python/api-documentation.rst b/src/blenderbim/docs/ifcopenshell-python/api-documentation.rst
deleted file mode 100644
index 299250fb90..0000000000
--- a/src/blenderbim/docs/ifcopenshell-python/api-documentation.rst
+++ /dev/null
@@ -1,21 +0,0 @@
-API Documentation
-=================
-
-.. automodule:: ifcopenshell.entity_instance
- :members:
-
-.. automodule:: ifcopenshell.file
- :members:
-
-.. automodule:: ifcopenshell.guid
- :members:
-
-
-.. automodule:: ifcopenshell.template
- :members:
-
-.. automodule:: ifcopenshell.validate
- :members:
-
-.. automodule:: ifcopenshell.ids
- :members:
diff --git a/src/blenderbim/docs/ifcopenshell-python/quickstart.rst b/src/blenderbim/docs/ifcopenshell-python/quickstart.rst
deleted file mode 100644
index 56c3ce6e60..0000000000
--- a/src/blenderbim/docs/ifcopenshell-python/quickstart.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Quickstart
-==========
-
-For starters, you can read `Using IfcOpenShell to parse IFC files with Python
-`_
diff --git a/src/blenderbim/docs/index.rst b/src/blenderbim/docs/index.rst
index ff0669b07e..60b741527f 100644
--- a/src/blenderbim/docs/index.rst
+++ b/src/blenderbim/docs/index.rst
@@ -1,26 +1,32 @@
-IfcOpenShell Documentation
-==========================
+Let's get started
+=================
+
+The BlenderBIM Add-on lets you analyse, create, and modify OpenBIM with Blender.
+
+.. note::
+
+ This documentation is incomplete. Would you like to help write more? `Get in touch! `__
.. toctree::
+ :hidden:
:maxdepth: 1
- :caption: Contents:
+ :caption: For users:
- ifcopenshell
- ifcopenshell-python
- ifcconvert
- blenderbim
- bimtester
- ifcdiff
- ifcclash
- ifccobie
- ifcmax
- ifcsverchok
- bimserver-plugin
+ users/introduction_to_bim
+ users/installation
+ users/exploring_an_ifc_model
+ users/creating_your_first_ifc
+ users/georeferencing
+ users/dealing_with_large_models
+.. toctree::
+ :hidden:
+ :maxdepth: 1
+ :caption: For developers:
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
+ devs/installation
+ devs/hello_world
+ devs/running_tests
+ devs/writing_docs
+ devs/undo_system
+ devs/ux_guidelines
diff --git a/src/blenderbim/docs/users/attributes.png b/src/blenderbim/docs/users/attributes.png
new file mode 100644
index 0000000000..1635813e4b
Binary files /dev/null and b/src/blenderbim/docs/users/attributes.png differ
diff --git a/src/blenderbim/docs/users/blenderbim-layout.png b/src/blenderbim/docs/users/blenderbim-layout.png
new file mode 100644
index 0000000000..3ef73d21aa
Binary files /dev/null and b/src/blenderbim/docs/users/blenderbim-layout.png differ
diff --git a/src/blenderbim/docs/users/blenderoffset.png b/src/blenderbim/docs/users/blenderoffset.png
new file mode 100644
index 0000000000..d307ab8bd6
Binary files /dev/null and b/src/blenderbim/docs/users/blenderoffset.png differ
diff --git a/src/blenderbim/docs/users/coordinateconversion.png b/src/blenderbim/docs/users/coordinateconversion.png
new file mode 100644
index 0000000000..838859f5c4
Binary files /dev/null and b/src/blenderbim/docs/users/coordinateconversion.png differ
diff --git a/src/blenderbim/docs/users/dealing_with_large_models.rst b/src/blenderbim/docs/users/dealing_with_large_models.rst
new file mode 100644
index 0000000000..346969d959
--- /dev/null
+++ b/src/blenderbim/docs/users/dealing_with_large_models.rst
@@ -0,0 +1,188 @@
+Dealing with large models
+=========================
+
+The BlenderBIM Add-on can handle large models, or federated collections of
+models where the combined total IFCs may be many gigabytes or object counts may
+be in the hundreds of thousands.
+
+Models may be large in terms of different metrics, such as:
+
+- Filesizes over 750MB, which may cause memory issues
+- Individual object polygon counts over 100k, which may cause unreasonable
+ loading times
+- Objects having excessive or low quality booleans, which may cause
+ unreasonable loading times
+- Number of elements exceeding 50,000 loaded in the scene, which may cause
+ unreasonable loading times, selection glitches, and viewport lagging
+
+There are always solutions to all of these, but an understanding of the type of
+size limitation you are up against will always help.
+
+Large filesizes
+---------------
+
+The first priority is to ensure you do not have a prohibitively large filesize.
+
+- Use IFC4. It can handle geometry much more efficiently and expect your
+ filesize to drop significantly.
+- When coming from other software, ensure you are exporting solids, not faceted
+ BReps or tessellations where appropriate. Choosing the wrong export setting
+ can easily double or triple your filesize and export times. Choose **Design
+ Transfer View** instead of **Coordination View** or **Reference View**. Look
+ for export settings that use the keywords like "surface", "solid", "brep",
+ "tessellation", or "extrusion".
+- Improve your model breakdown strategy (see below)
+- Identify objects with large polygon counts and improve the modeling (see below)
+
+Model breakdown strategies
+--------------------------
+
+A good general strategy is to never have a large model to begin with. Breaking
+down models is critical for usability during design and coordination. Where full
+models are needed, such as during clash detection, review meetings, or client
+handover, many small models may be federated. Model breakdown strategies
+include:
+
+- **By discipline**, you probably already do this, so let's move on
+- **By location**, such as by building, floor, zone, mid-rise, high-rise, core
+ podium, underground, plant rooms, facade orientation, or similar.
+- **By object type**, such as by primary structural elements vs accessories
+ (plates, bolts, etc), furniture vs general arrangement, facade vs interiors,
+ distribution system elements (equipment, pipes, fittings) vs accessories (pipe
+ clamps, hangers, etc) or similar.
+- **By coordination task**, many people get into the habit of exchanging the
+ entire building when a task only requires a tiny portion of it. Think of the
+ workflow of exchanging traditional drawings. A large project would have
+ thousands of drawings with a few drawings exchanged for a single coordination
+ task. This strategy can be used with models: exchange hundreds of tiny models
+ (some maybe even only 1MB!), scoped to the task at hand. Keep exchanges small
+ and frequent (like code commits, for the geeks reading this).
+
+Filtered model loading
+----------------------
+
+You may filter elements and only load a portion of the model. Click on **Enable
+Advanced Mode** when loading a model and you will be presented with model
+loading options in the **IFC Project** panel. **Filter Modes** include:
+
+- **Decomposition**, filter by location in the building, such as **Level 1** or
+ **Building A**.
+- **IFC Class**, filter by IFC class, such as **Wall**, **Column**, or **Pipe
+ Segments**
+- **IFC Type**, filter by IFC construction type, such as **Copper pipes** and
+ **200mm thick concrete slabs**
+- **Whitelist** or **Blacklist**, filter by a custom query
+
+When **Whitelist** or **Blacklist** is chosen, you may type a custom query to
+filter by attributes, properties, location, and so on.
+
+Large polygon counts
+--------------------
+
+If objects with large polygon counts are blocking you from importing, consider
+enabling **Native Meshes** in the **Advanced Mode** when loading projects.
+
+The **Debug Panel** allows you to **Select High Polygon Meshes** or **Select
+Highest Polygon Meshes** to isolate geometrically complex objects by a polygon
+number threshold or a percentage.
+
+After selecting these elements, you can view them in edit mode to see a polygon
+count and where the offending polygons are. Often, fixing a single object may
+cut out 50MB.
+
+Excessive or low quality booleans
+---------------------------------
+
+In some cases, elements may be generated from external software with an
+excessive (over 50) number of boolean operations or with high polygon, complex
+booleans.
+
+The **IFC Debug** panel has a **Test All Shapes** feature which generates
+geometry for every element one by one and outputs the processing time to the
+console. When you see it stuck on an element, make a note of the element ID. You
+may then use the **IFC Debug** panel's **Inspector** to determine the nature of
+the boolean, or create a **Blacklist Filter Mode** to exclude the element from
+import.
+
+These types of errors are usually problems with external software (i.e. not
+intentionally by the end-user) and typically do not affect critical geometry
+and can be worked around.
+
+Fully resolving boolean issues is a complex case by case topic and not covered
+here.
+
+High number of elements
+-----------------------
+
+Click on **Enable Advanced Mode** when loading a model and you will be presented
+with model loading options in the **IFC Project** panel.
+
+You may specify an **Element Range** to process. The **Element Offset** says the
+first element to start processing at, and the **Element Limit** says how many
+elements should be processed. For example, in a model with 100,000 objects, an
+**Element Start** of 30,000 and an **Element Limit** of 20,000 will process the
+elements starting at item number 30,000 and ending at item number 50,000. This
+allows you to arbitrarily break down large models into submodels. This can be
+combined with other filters.
+
+`Alpha versions of Blender `__ may
+be used which will result in a faster load time (~10%).
+
+.. seealso::
+
+ Blender is working on resolving some performance bottlenecks related to high
+ object counts. See `D9892 `__, `D15129
+ `__ (available in Blender 3.3 alpha),
+ and `D14162 `__
+
+Coordination only models
+------------------------
+
+The BlenderBIM Add-on defaults to authoring IFCs. This allows full editing and
+inspection of all element properties and relationships. However, sometimes only
+geometry and basic attributes such as names are sufficient. Example usecases
+include CG visualisation, overall federated model coordination, or pure
+geometric checks.
+
+Click on **Enable Advanced Mode** when loading a model and you will be presented
+with model loading options in the **IFC Project** panel. Enable **For
+Coordination Only**, which will exclude non geometric elements, openings, and
+types from being imported. This leads to slightly faster imports, and a
+decreased object count.
+
+Enabling **For Coordination Only** also allows you to specify a **Merge Mode**.
+This combines objects to keep object counts low. Blender is very good at
+handling less objects with more complexity, rather than the other way around.
+When a **Merge Mode** is activated, import times will increase (~50%) but object
+counts will be drastically reduced, which is critical for the federation of
+large models. **Merge Modes** include:
+
+- **IFC Class**, where objects of the same IFC class are merged. This is useful
+ if you have models where only the class is meaningful for other disciplines,
+ such as structural models.
+- **IFC Type**, where objects of the same construction type are merged. This is
+ useful where the main identification of interest is the element type, not the
+ element instance.
+- **Material**, where objects of the same material are merged. This is useful if
+ the model is used for purely visual exploration such as CG visualisation.
+
+Once loaded, the model may be saved as a ``.blend`` file for subsequent loads.
+You can think of the ``.blend`` file as a geometry cache, which is very, very
+fast to load. If it no longer necessary to access IFC data, consider pressing
+the **Unload Project** icon so that future loads of the ``.blend`` file will be
+very fast.
+
+With these strategies, a federated 1GB IFC model can easily load in 10 seconds
+from the saved Blender files.
+
+Processing models headlessly
+----------------------------
+
+You can automate model processing using this command (~5% speedup):
+
+.. code-block:: bash
+
+ $ blender -b -P headless_import.py
+
+The ``headless_import.py`` script contains instructions on how to configure
+model loading settings.
diff --git a/src/blenderbim/docs/users/element-class-select.png b/src/blenderbim/docs/users/element-class-select.png
new file mode 100644
index 0000000000..4e2304b93f
Binary files /dev/null and b/src/blenderbim/docs/users/element-class-select.png differ
diff --git a/src/blenderbim/docs/users/element-class.png b/src/blenderbim/docs/users/element-class.png
new file mode 100644
index 0000000000..c38e518e2c
Binary files /dev/null and b/src/blenderbim/docs/users/element-class.png differ
diff --git a/src/blenderbim/docs/users/example-project.png b/src/blenderbim/docs/users/example-project.png
new file mode 100644
index 0000000000..e9816f4c55
Binary files /dev/null and b/src/blenderbim/docs/users/example-project.png differ
diff --git a/src/blenderbim/docs/users/exploring_an_ifc_model.rst b/src/blenderbim/docs/users/exploring_an_ifc_model.rst
new file mode 100644
index 0000000000..7cb0346235
--- /dev/null
+++ b/src/blenderbim/docs/users/exploring_an_ifc_model.rst
@@ -0,0 +1,449 @@
+Exploring an IFC model
+======================
+
+We're going to load an existing IFC model and explore some common properties
+most users will be interested in.
+
+If you don't have an IFC model available, here's a small one for your
+convenience provided by the Institute for Automation and Applied Informatics
+(IAI) / Karlsruhe Institute of Technology. It's in German, so you may need to
+use some creativity when reading the data :)
+
+.. container:: blockbutton
+
+ `Download sample IFC `__
+
+.. seealso::
+
+ You can find more sample models online in the `OSArch Open Data Directory
+ `__
+
+Loading a model
+---------------
+
+Blender's interface is divided into panels. The main **Viewport** panel shows 3D
+geometry. The top right **Outliner** panel shows a tree of geometric objects.
+The bottom right **Properties** panel shows data and relationships.
+
+.. image:: blenderbim-layout.png
+
+The **Properties** panel has tabs to switch between different types of
+properties. Make sure you have the **Scene Properties** tab activated, and find
+the **IFC Project** subpanel. Click on **Load Project** and browse to your
+``.ifc`` file.
+
+.. image:: properties-loadproject.png
+
+After loading, you will see the model appear in the **Viewport** panel.
+
+.. image:: example-project.png
+
+Let's take a look at the **IFC Project** subpanel again. It shows the loaded
+filename, as well as the **IFC Schema**. There are two commonly seen **IFC
+Schema** versions: IFC2X3 and IFC4. Checking the **IFC Schema** is important
+because it has an impact on what BIM data may be stored. IFC4 is the newer
+version and it is recommended to use IFC4 models as it has significantly more
+BIM capabilities compared to IFC2X3.
+
+.. tip::
+
+ Blender's interface is highly customisable. Panels, panel types, colours,
+ sizes, and tabs may be edited to suit your workflow. If you want a `Hot Pink
+ theme `__ look no further.
+
+Navigating a model in 3D
+------------------------
+
+To navigate, can use the **Navigate Gizmo** on the top right corner of the
+**Viewport** panel. Click and drag on the coloured axes to **Orbit**, click and
+drag on the magnifying glass to **Zoom**, and click and drag on the hand icon to
+**Pan**. You can also click on the grid icon to switch between perspective and
+orthographic view.
+
+To switch to a top view, front view, or side view, click the relevant axis on
+the **Navigate Gizmo**.
+
+.. image:: navigate-gizmo.png
+
+You can also use your mouse to navigate. Hover your mouse over the **Viewport**
+panel and click and drag the Middle Mouse Button (``MMB``) to **Orbit**. Scroll
+the mousewheel to **Zoom**, and use ``Shift-MMB`` to **Pan**.
+
+If you have a numpad, you can use the numpad keys to quickly switch to top,
+front, or side view. Use ``7`` for top view, ``1`` for front view, and ``3`` for
+side view.
+
+.. warning::
+
+ Blender's hotkeys are context sensitive. This means that a hotkey has a
+ different meaning depending on the panel your mouse cursor is hovering over.
+ If you press ``7`` to go to top view, make sure your mouse cursor is over the
+ **Viewport** panel. Be very careful where your mouse is, or you might press a
+ hotkey and it will have unintended consequences!
+
+If you click on an object, such as a wall in the **Viewport** panel, you can
+zoom to the selected object by clicking on ``View > Frame Selected``. The hotkey
+is the ``.`` button on the numpad. After zooming into an element, when you
+**Orbit** the 3D view will rotate around the center of that element.
+
+You can also zoom to all objects in the project by clicking on ``View > Frame
+All``.
+
+.. image:: frame-selected.png
+
+Another good way to navigate is by flying or walking around similar to a video
+game. Choose ``View > Navigation > Walk Navigation``, or use the ``Shift-```
+hotkey (the backtick key is usually to the left of the number 1 on the
+keyboard). With **Walk Navigation** enabled, use the ``WASD`` keys and the mouse
+to move around like a video game. You can use the ``Shift`` key to switch
+between moving fast and slow. If you scroll with the mousewheel, it will adjust
+the speed that you move at.
+
+Sometimes, you want to look through objects. You can toggle **X-Ray Mode** by
+pressing the button on the top right of the **Viewport** panel. The hotkey is
+``Alt-Z``.
+
+.. image:: x-ray-mode.png
+
+.. tip::
+
+ Blender has lots of hotkeys to do things quickly. These can take time to
+ learn but it is worth it as you will be much faster. These hotkeys can be
+ customised in Blender's preferences.
+
+
+Overview of all objects
+-----------------------
+
+The **Outliner** panel on the top right shows a hierarchy of all the currently
+loaded physical **Objects** in your IFC project. These **Objects** correlate to
+what you can see in the **Viewport** panel.
+
+Every **Object** in the **Outliner** represents an IFC **Element**. These
+**Objects** have a name with the pattern ``Class/Name``. The class prefix
+represents the type of object, and the name is the name of the object. Examples
+of classes you will see are ``IfcBuilding``, or ``IfcWall``. This naming
+convention makes it easy to quickly spot types of objects.
+
+.. image:: outliner.png
+
+Objects are organised in a hierarchy. By default, this hierarchy represents a
+breakdown of spaces, from large spaces such as a site and a building, down to
+smaller spaces like building storeys and room spaces. The hierarchy will always
+begin with an **IfcProject** object. You can click on the triangle to toggle the
+hierarchy.
+
+.. tip::
+
+ In large projects with deep hierarchies, you can ``Shift-LMB`` click the
+ triangle to recursively toggle the hierarchy. You can also click and drag the
+ ``MMB`` to pan left and right.
+
+When there are lots of objects, you can type a name in the filter box to quickly
+identify objects by name or type.
+
+.. image:: outliner-filter.png
+
+Clicking on an object in the **Outliner** panel also selects the corresponding
+object in the **Viewport** panel. A good strategy to find objects is to then use
+``View > Frame Selected`` to zoom to it in the **Viewport**.
+
+The **Outliner** panel is also great for isolating portions of your project. You
+can include and exclude portions by clicking on the **Tick Icon** next to
+collections of objects in the hierarchy.
+
+Let's isolate a single building storey. Start by disabling the **Tick Icon**
+next to the **IfcProject** collection. This will hide everything in the project.
+Then navigate through the hierarchy and enable the **Tick Icon** next to an
+**IfcBuildingStory**.
+
+.. image:: outliner-isolate.png
+
+Viewing element classes
+-----------------------
+
+Usually the first thing you'll want to check is the **Class** of element that an
+object represents. The IFC **Class** is an international classification system
+provided by IFC. Example IFC **Classes** are Wall, Slab, and Door. Every IFC
+element must have a **Class**.
+
+**Classes** aren't just for categorising elements. They also indicate what types of
+properties and relationships it is allowed to have. For example, a Wall
+**Class** can have a fire rating property, but a Grid **Class** cannot.
+
+.. note::
+
+ There are hundreds of **Classes** to represent all aspects of our built
+ environment, including non geometric classes like Task, Occupant, and CostItem.
+ However, we'll focus only on simple physical **Classes** in this guide. Don't
+ worry about memorising all the available **Classes**, you'll get a feel for them
+ as you explore more.
+
+To view an object's class, click on an object in the **Viewport** or **Outliner**
+panel, then switch to the **Object Properties** tab in the **Properties** panel.
+You can see the **Class** name in the **IFC Class** subpanel.
+
+.. image:: element-class.png
+
+In this case, the **Class** of our roof is an **IfcSlab**. You'll notice this is
+the same **Class** name used as a prefix for the object name in the **Outliner**
+panel. You can also see the name of the actively selected object in the top left
+of the **Viewport** panel.
+
+.. warning::
+
+ Sometimes, an IFC model will use the wrong **Class**. For example, a chair might
+ be assigned as the Wall **Class** instead the Furniture **Class**. There is a
+ special class known as **IfcBuildingElementProxy**, used when the user is
+ unable to find a more semantic **Class**. If you see many
+ **IfcBuildingElementProxy** **Classes**, it is likely a symptom of a low
+ quality IFC model. If this is the case, scold the project manager and ask
+ them to do a better job.
+
+After **IfcSlab** it also says **ROOF**. This is known as the
+**Predefined Type** of the element. You can think of it as a further level of
+classification. In this case, it distinguishes our object as a roof slab,
+compared to other types of slabs. The **Predefined Type** is optional so you may
+not see it all the time.
+
+.. seealso::
+
+ You can use the `Search IFC Class
+ `__ tool to learn the correct
+ classes and predefined types you should see.
+
+Press the **Select Icon** to select all objects that are of the same
+**IfcSlab** **Class**. Then, you can isolate these elements by going to ``Object
+> Show/Hide > Hide Unselected`` (hotkey ``Shift-H``). To show all elements again, you can use
+``Object > Show/Hide > Show Hidden Objects`` (hotkey ``Alt-H``). If you want to
+hide elements instead, you can use ``Object > Show/Hidden > Hide Selected``
+(hotkey ``H``).
+
+.. image:: element-class-select.png
+
+.. note::
+
+ Remember that Blender's hotkeys are context sensitive. Make sure your mouse
+ is hovering over the **Viewport** panel when you press a hotkey or no cake
+ for you.
+
+You can also see statistics about the number of selected objects. If you right
+click on the bottom right status bar and enable **Scene Statistics** you will
+see information like **Objects 4/4**, which means that 4 objects are selected
+out of 4 available objects. This is a great way of counting objects like toilets.
+
+.. image:: scene-statistics.png
+
+Viewing attributes and properties
+---------------------------------
+
+You can view the **Attributes**, **Properties**, and **Quantities** of the
+selected object in the **Object Properties** tab.
+
+Let's focus on **Attributes** first. Scroll down to the **IFC Attributes**
+subpanel. **Attributes** are a limited set of fundamental data (usually less
+than 10) associated with all IFC elements. These are fixed by the IFC standard.
+
+.. image:: attributes.png
+
+Here are some common attributes and what they mean:
+
+- **GlobalId**: a unique ID for the element useful for computer geeks
+- **Name**: a short name, code, number, or label to identify the object for a
+ human. If you had to annotate the object on a drawing or a schedule, the
+ **Name** is what you should see. For example, a pump **Name** might be ``P-10-A``.
+- **Description**: typically the longer form of the name, written to be
+ descriptive and readable for humans. For example a pump **Description** might
+ be ``Water Suction Pump``.
+- **Tag**: this is an ID that may link it back to another BIM application. For
+ example if the IFC model was produced using Revit or ArchiCAD, it might hold
+ the Revit or ArchiCAD element ID.
+- **Predefined Type**: A further level of classification to be read
+ in conjunction with the IFC class.
+
+.. warning::
+
+ Some IFC models have poor quality data. For example, if the **Name**
+ of a Window doesn't match the window code (e.g. ``W01``) you see on a
+ drawing, the project manager has clearly not put enough effort. Shame on
+ them.
+
+**Properties** are other data associated with the object. Every project will
+have different **Properties** depending on what information they want to store.
+Each **Property** has a name and a value, and are grouped into **Property
+Sets**. Each **Property Set** also has a name.
+
+You can find **Properties** in the **IFC Property Sets** subpanel.
+
+.. image:: psets.png
+
+Some very common **Properties** are standardised by international BIM standards.
+For example, the load bearing **Property** of a beam should always be called
+``LoadBearing``. If a **Property Set** is part of the international standard, it
+has a prefix of ``Pset_``, like ``Pset_BeamCommon``.
+
+If you see a **Property Set** without the ``Pset_`` prefix, it is a custom
+property defined by the author of the IFC model.
+
+.. tip::
+
+ It's important to distinguish between **Properties** that are
+ part of the standard compared to custom ones. When **Properties** are
+ standardised, it makes it easy to manage data. So if you need to isolate all
+ ``LoadBearing`` elements, you now know exactly where to look.
+
+**Quantities** are very similar to properties. They also have a name and a
+value, and are grouped into **Quantity Sets**. Similarly, there are also common
+quantities defined as part of the international standard, denoted by the prefix
+``Qto_``. This prefix is short for "Quantity Take-Off".
+
+You can find **Quantities** in the **IFC Quantity Sets** subpanel.
+
+.. image:: qtos.png
+
+Finding the location of objects
+-------------------------------
+
+Every object in the built environment has a location in the world. For example,
+a chair will be located in a space, and a wall is typically located in a
+building storey. You've already seen this hierarchy of spaces in the
+**Outliner** panel, where an IFC project is broken down into site, building,
+storeys, and spaces.
+
+Sometimes, objects may have multiple relevant locations, such
+as a multi-storey column which can be related to multiple building storeys.
+Even in these cases, IFC enforces one location to be its primary
+location, known as its **Spatial Container**.
+
+If you click on any object, you can see its location in the **IFC Spatial
+Container** subpanel in the **Object Properties** tab.
+
+Press the **Select Icon** to select all objects that are in the same location.
+
+.. image:: spatial-container.png
+
+Checking construction types
+---------------------------
+
+Almost everything in the built environment will have a **Construction Type**.
+For example, an architect will specify a door type for every door in a project.
+
+You can see a list of **Construction Types** in the **Outliner** panel in the
+**Types** collection. For example, if the architect has a wall types schedule
+with the wall type names of ``WT01``, ``WT02``, and ``WT03``, you should see
+three **IfcWallType** objects with those same names in the **Outliner**.
+
+You can click on these types to see more details about them in the **Object
+Properties** panel.
+
+.. image:: outliner-types.png
+
+When selecting an object, you can also see its construction type in **Object
+Properties** under the **IFC Construction Type** subpanel. You can press the
+**Select Icon** to select all objects that are of the same **Construction
+Type**. You can use the hide and isolate hotkeys to quickly view them in the
+model.
+
+.. image:: properties-types.png
+
+A **Construction Type** defines properties that are common to all occurrences of
+that type. For example, if a wall type specifies a fire rating property, then
+all walls of that wall type will inherit that fire rating too.
+
+A **Construction Type** may also specify geometry or geometric rules that are
+common to all occurrences of the type. For example, a pump type will define the
+geometry of the pump, so all occurrences of that pump will have the same
+geometry.
+
+You can visually inspect types in isolation to the rest of the model. Types are
+hidden by default, so first enable the visibility of the **Types** collection in
+the **Outliner** by pressing the **Visibility Icon**. Then, select a type, and
+click on ``View > Local View > Toggle Local View`` (hotkey ``/``) in the
+**Viewport**. Toggle the view to see the entire model again.
+
+.. image:: type-local-view.png
+
+.. note::
+
+ Only **Construction Types** where the geometry is exactly the same for all
+ occurrences will specify geometry. When the geometry varies based on the
+ occurrence (such as a wall, which varies based on the wall length), the
+ **Construction Type** will typically have no geometry.
+
+Filtering by materials
+----------------------
+
+Everything in the built environment is made from a physical raw **Material**
+resources. For example, a **Material** might be a 140mm core filled block.
+Another **Material** might be a 190mm hollowcore block. **Materials** are
+grouped into categories like steel, concrete, brick, block, and so on.
+
+We can see a list of **Materials** used in the project in the **IFC Materials**
+subpanel in the **Scene Properties** tab.
+
+Press the **Select Icon** to select all objects that are of the selected
+material.
+
+.. image:: materials.png
+
+Taking simple measurements
+--------------------------
+
+The simplest form of measurement is the one that's already taken for you. The
+**Viewing attributes and properties** section describes how to view
+pre-calculated **Quantities**.
+
+Sometimes, you may wish to take manual measurements yourself. To view the
+overall dimensions of an object, first select an object, then expand out the
+viewport **Sidebar**.
+
+.. image:: viewport-sidebar.png
+
+In the **Item** tab, you can find the X, Y, and Z overall **Dimensions** of the
+currently selected object.
+
+.. image:: viewport-dimensions.png
+
+Another way to manually measure from two points is to use the **Measure** tool.
+First, press the **Snap Icon** to enable snapping. Then choose snap targets in
+the **Snap Menu** in the top middle section of the **Viewport** panel.
+
+.. image:: snap-targets.png
+
+.. tip::
+
+ It is recommended to choose multiple snap targets, like **Vertex**, **Edge**, and
+ **Face**, and **Edge Center**. You can use the ``Shift`` key to select
+ multiple snap targets. For example, the **Face** snap target means that your
+ measurements will automatically snap to the nearest object's surface.
+
+Now that you have configured snapping, press the **Measure Tool Icon** on the
+left of the **Viewport** panel. **Click** and **Drag** in the 3D viewport to
+take a measurement. A circle will appear guiding the first point of your
+measurement. While **Dragging**, press the ``X`` key to lock the measurement
+line along the X axis. Alternatively, press the ``Y`` or ``Z`` key to lock the
+measurement line along the Y or Z axis. Let go of the mouse to finish your
+measurement.
+
+.. image:: measure-tool.png
+
+To delete a measurement, just click on one point of the measurement, and press
+the ``Delete`` key. You can also click and drag the ends of your measurement
+lines to measure to another location.
+
+What else is there?
+-------------------
+
+Congratulations, and welcome to the digital built environment!
+
+We've barely scratched the surface of the data and relationships available in an
+IFC model. We've yet to cover documents and drawings, clearance zones, tasks,
+cost items, structural loads and forces, analytical models, distribution system
+connectivity, energy analysis, rendering textures, and so much more. Our built
+environment and its relationships are vast and complex and it is exciting that
+you can join us on its digital journey!
+
+Please do not hesitate to reach out with any questions.
+
+- `OSArch live chat `__
+- `OSArch community forum `__
diff --git a/src/blenderbim/docs/users/frame-selected.png b/src/blenderbim/docs/users/frame-selected.png
new file mode 100644
index 0000000000..ea297bc9a9
Binary files /dev/null and b/src/blenderbim/docs/users/frame-selected.png differ
diff --git a/src/blenderbim/docs/users/georeferencing.rst b/src/blenderbim/docs/users/georeferencing.rst
new file mode 100644
index 0000000000..4127fbacfb
--- /dev/null
+++ b/src/blenderbim/docs/users/georeferencing.rst
@@ -0,0 +1,256 @@
+Georeferencing
+==============
+
+There are two types of construction: vertical construction (such as buildings
+and sites) which deal with small distances typically under 1km, and horizontal
+construction (such as transport, transmission, and subterranean networks) where
+distances frequently exceed 1km. Blender and the BlenderBIM Add-on focuses on
+vertical construction, and will typically just work out of the box.
+
+Coordinate reference systems
+----------------------------
+
+The minimum requirement for a georeferenced model is to specify the coordinate
+reference system used. This is known as the **Projected CRS**, and is a feature
+available in IFC4 onwards.
+
+.. warning::
+
+ IFC2X3 models cannot be georeferenced. There is a proposed convention to
+ provide fallback support but this is not supported yet in any known vendor.
+ Please consider upgrading to IFC4.
+
+Most architects and engineers will know the name of the **Projected CRS**
+typically chosen by the surveyor. For example in Sydney, Australia, you might
+use GDA2020 / MGA Zone 56. In IFC a standardised code from the EPSG public
+registry is used to refer to the **Projected CRS**. For example, GDA 2020 / MGA
+Zone 56 will be named EPSG:7856.
+
+You can check whether or not your model is georeferenced in the **IFC
+Georeferencing** panel in the **Scene Properties** tab. You should see a section
+for the **Projected CRS** with an EPSG code.
+
+.. image:: projectedcrs.png
+
+If you do not see this, your project is not georeferenced.
+
+.. Note::
+
+ Even if a model has large "real world coordinates", this does not mean the
+ project is georeferenced. Without a **Projected CRS**, these coordinates are
+ meaningless.
+
+Map conversions
+---------------
+
+The coordinates for the nominated **Projected CRS** are known as **Map
+Coordinates**. These **Map Coordinates** are typically large numbers and read as
+Eastings and Northings.
+
+In vertical construction, some disciplines (such as a civil engineer or
+surveyor) will directly use **Map Coordinates** in their designs. Most others,
+such as the architect, structural, and service engineers will instead use
+**Local engineering coordinates**. A **Map Conversion** stores the parameters
+for transforming **Local engineering coordinates** to **Map Coordinates**.
+
+For example, a civil engineer will work directly in **Map Coordinates**. This
+means that the model's coordinates correlate directly to Eastings and
+Northings. Similarly, the model's +Y axis will point to **Grid North**. As
+there is no **Map Conversion** involved, you will see a 0 in the Eastings,
+Northings, and Orthogonal Height in the **IFC Georeferencing** panel.
+
+.. image:: mapcoordinates.png
+
+When **Local engineering coordinates** are used, typically the architect will
+nominate a local origin and model geometry will be drawn orthogonally (i.e.
+along the X and Y axis). This local origin often correlates with a site bounary,
+surveyed point, or grid intersection. This means that the model's coordinates
+are typically smaller numbers and correlate to surface distance measurements,
+not Eastings and Northings, and the model's +Y axis will point to **Project
+North**. The surveyor will then provide the necessary **Map Conversion**
+parameters to convert from **Local engineering coordinates** to Eastings,
+Northings, Orthogonal Height, and **Grid North**.
+
+.. image:: mapconversion.png
+
+.. warning::
+
+ Coordinate systems are a technical topic. A common error is that disciplines
+ may choose to use **Map Coordinates** without realising that map distances
+ do not correlate with surface distances measured on the ground. Unless you
+ are trained to work in **Map Coordinates**, it is safer to work with local
+ engineering coordinates and consult your surveyor for professional guidance.
+
+**Map Conversions** contain six parameters.
+
+**Eastings**, **Northings** and **Orthogonal Height** parameters define the
+translation from the model's XYZ coordinates to map **Eastings**, **Northings**,
+and **Heights**. Your model's local engineering origin at 0, 0, 0, will always
+convert exactly to the **Easting**, **Northing**, and **Orthogonal Height**
+displayed in this panel.
+
+The **X Axis Abcissa** and **X Axis Ordinate** define the rotation vector from
+**Project North** to **Grid North**. These two numbers combine into a coordinate
+vector pointing along the X axis (i.e. **Project East**). The default is an
+abscissa of 1 and ordinate of 0. This default (1, 0) vector implies **Project
+East** and **Grid East** coalign, which means there is no rotation between
+**Project North** and **Grid North**.
+
+.. image:: xaxisabscissaordinate.png
+
+.. tip::
+
+ To save you the mental struggle of converting to degrees, a calculated
+ rotation is always just below these values. Phew!
+
+The distance measured on site, or the "surface distance" is actually not the
+same as the distance measured between Eastings and Northings. This difference is
+provided by the **Scale** parameter. The **Scale** defines the average combined
+scale factor across the small 1km site that converts from the model's surface
+distances to map grid distances. Note that the **Scale** is actually not a
+constant. However, for the small sites dealt with in vertical construction, it
+may be approximated to be a constant by your surveyor and will typically be a
+value close to, but not exactly 1.
+
+.. note::
+
+ Always check that the surveyor provides a scale factor such that surface
+ distance multiplied by **Scale** equals map grid distances (as opposed to
+ the other way around).
+
+Working with Map Coordinates
+----------------------------
+
+The BlenderBIM Add-on is designed to work with small coordinates (under 1km),
+whereas map coordinates are typically large. When you load an IFC which uses map
+coordinates directly, or when you are working with IFC2X3 and you cannot use a
+map conversion, the BlenderBIM Add-on will autodetect a point on your model to
+use as a false origin.
+
+The XYZ offset used for the false origin will be shown in the **IFC
+Georeferencing** panel under the **Blender Offset** header. It
+is very similar to a **Map Conversion**, but it will not have a scale and only
+temporarily affects your Blender session.
+
+.. image:: blenderoffset.png
+
+.. note::
+
+ A Blender offset is simply a shift in coordinates to reduce large model
+ coordinates to smaller coordinates. It should not be used as an indicator of
+ whether georeferencing is done correctly. Always check the **Projected
+ CRS**, **Map Conversion** and confirm the parameters with your surveyor.
+
+This distance limit of 1km and autodetected false origin may not be appropriate
+for your project. For example, your project may exceed the 1km limit, or you may
+want to federate multiple files together and manually specify a consistent and
+fixed false origin. You can customise these options by choosing **Enable
+Advanced Mode** when loading a project. Then, set the **Distance Limit** (in
+meters) and the **False Origin** coordinate before pressing **Load Project
+Elements**.
+
+.. image:: manualorigin.png
+
+When a false origin is used, there are two possible methods to offset objects by
+the false origin.
+
+The first method is to offset the origin point of objects. We call this the
+**Object Placement** method. The second method is to offset the local
+coordinates of geometry within the objects themselves. We call this the
+**Cartesian Point** method. Sometimes, BIM applications combine both of these
+methods in a single IFC project. To see which workaround was used on an object,
+check the "Blender Offset" property in the **Transform** panel in the **Object
+Properties**. This is an advanced property used by powerusers to debug
+coordinate issues and may be safely ignored by most users.
+
+.. image:: offsetmode.png
+
+Incorrect coordinate use
+------------------------
+
+Sometimes, a model may mix **Map Coordinates** and **Local engineering
+coordinates**. For example, a surveyed pipe may have its placement use **Map
+Coordinates** with large Eastings and Northings. However, the placement of the
+site object may be still set at 0, 0, 0. Since this range of coordinates exceed
+the default 1km distance limit, this creates a problem. Blender needs to choose
+between displaying the pipe accurately and sacrificing precision at the site
+placement, or vice versa, but it is impossible to satisfy both simultaneously in
+the same Blender session.
+
+.. warning::
+
+ Many IFC viewers only show geometry, and don't show object placements. This may
+ give users the false impression that their coordinates in their IFC project
+ do not have such a large range. However, as a native IFC authoring platform,
+ the BlenderBIM Add-on will not accept this inconsistency.
+
+At this point, it is the users responsibility to reconcile this inconsistency in
+their coordinates. Either the user needs to fix their file to consistently
+offset all coordinates, or the user needs to manually tell the BlenderBIM Add-on
+the coordinates of the desired false origin and accept the precision loss.
+
+Converting local and map coordinates
+------------------------------------
+
+You can convert **Local engineering coordinates** to **Map coordinates** and
+vice versa in the **Viewport** panel. First, enable ``View > Sidebar`` then type
+in your coordinate in the **Input** field. Press either the **Local to Global**
+or **Global to Local** button to convert the coordinate. You will see the result
+of the calculation in the **Output** field.
+
+.. image:: coordinateconversion.png
+
+True north
+----------
+
+When **Local engineering coordinates** are used, the model's +Y axis points to
+**Project North** for the convenience of drafting. When **Map Coordinates** are
+used, the model's +Y axis points to **Grid North** for the neccessity of
+surveying.
+
+**Project North** and **Grid North** is different to **True North**. The angle
+to **True North** is not a fixed angle. It will actually vary depending on the
+Eastings and Northings you choose to calculate it from.
+
+However, this variable **True North** is a great source of confusion to
+architects, who typically just want to do a shadow study, solar study, or
+similar and go out for an early lunch. IFC can store a fixed **True North**
+value as a reference to be used for these types of usecases. If one is stored in
+your project, you may see it under the **True North** section of the **IFC
+Georeferencing** panel. Your surveyor will be able to provide the **True North**
+vector, but it should be only used as a reference, never used as a way to
+coordinate model rotations, and always with the understanding that it is not a
+fixed value.
+
+.. image:: truenorth.png
+
+.. warning::
+
+ Fun fact: **Magnetic North** is useless for the purposes of construction.
+
+Coordinate precision limits
+---------------------------
+
+The BlenderBIM Add-on focuses on vertical construction. Vertical construction
+typically uses **Local engineering coordinates** on a small site. The
+buildingSMART georeferencing technical experts panel have determined that a
+small site under 1km square can be assumed to have a constant **Map
+Conversion**.
+
+Therefore, if your model is less than 1km square, you are within the coordinate
+precision limits. This is where the 1km default distance limit is derived from.
+
+If you want to exceed the 1km square surveying limitation, you will need to be
+aware of software limitations that can result in precision loss when large
+coordinate ranges are used.
+
+Blender, and subsequently the BlenderBIM Add-on, is not designed for **Map
+Coordinates**. Blender internally uses single precision floating point
+calculations. A full description of the precision implications are described in
+the `Blender working limits documentation
+`__.
+
+This means that lengths greater than 5,000 meters start to accumulate software
+precision errors that affect the nearest millimeter. Therefore, from a software
+perspective, it is unwise to embark on a project with coordinates ranging
+greater than +/- 5km.
diff --git a/src/blenderbim/docs/users/install-blenderbim-1.png b/src/blenderbim/docs/users/install-blenderbim-1.png
new file mode 100644
index 0000000000..79d1ef91c2
Binary files /dev/null and b/src/blenderbim/docs/users/install-blenderbim-1.png differ
diff --git a/src/blenderbim/docs/users/install-blenderbim-2.png b/src/blenderbim/docs/users/install-blenderbim-2.png
new file mode 100644
index 0000000000..ab81f74adf
Binary files /dev/null and b/src/blenderbim/docs/users/install-blenderbim-2.png differ
diff --git a/src/blenderbim/docs/users/install-blenderbim-3.png b/src/blenderbim/docs/users/install-blenderbim-3.png
new file mode 100644
index 0000000000..7af37299d9
Binary files /dev/null and b/src/blenderbim/docs/users/install-blenderbim-3.png differ
diff --git a/src/blenderbim/docs/users/install-blenderbim-4.png b/src/blenderbim/docs/users/install-blenderbim-4.png
new file mode 100644
index 0000000000..179613072c
Binary files /dev/null and b/src/blenderbim/docs/users/install-blenderbim-4.png differ
diff --git a/src/blenderbim/docs/users/installation.rst b/src/blenderbim/docs/users/installation.rst
new file mode 100644
index 0000000000..26a64468a8
--- /dev/null
+++ b/src/blenderbim/docs/users/installation.rst
@@ -0,0 +1,135 @@
+Installation
+============
+
+1. **Download and install Blender**
+
+ Blender is a free and open-source program for 3D authoring. It works on
+ Linux, Mac, and Windows. It is developed by the Blender community.
+
+.. container:: blockbutton
+
+ `Download Blender `__
+
+.. tip::
+
+ No administrator rights on Windows? Choose the "Portable .zip" option when
+ downloading from the Blender website.
+
+2. **Download the BlenderBIM Add-on**
+
+ The BlenderBIM Add-on extends Blender with OpenBIM related capabilities.
+
+.. container:: blockbutton
+
+ `Download BlenderBIM Add-on `__
+
+.. warning::
+
+ If you are not using Blender version >=3.1, please follow the **Unstable installation** instructions. :doc:`Read more <../devs/installation>`
+
+3. **Install the BlenderBIM Add-on**
+
+ Open up Blender, and click on ``Edit > Preferences``.
+
+ .. image:: install-blenderbim-1.png
+
+ Select the **Add-ons** tab, and press **Install...** on the top right. Navigate
+ to the .zip you downloaded in Step 2, and press **Install Add-on**.
+
+ .. image:: install-blenderbim-2.png
+
+ .. warning::
+
+ You do not need to unzip the add-on file. You should install it as a zipped file.
+
+ You should now see **Import-Export: BlenderBIM** available in your add-ons list. Enable the add-on by pressing the checkbox.
+
+ .. image:: install-blenderbim-3.png
+
+All done! If you check your Scene properties in the panel on the bottom right, you will see a panel to manage your **IFC Project**.
+
+.. image:: install-blenderbim-4.png
+
+You can enable add-ons permanently by using ``Save User Settings`` from the Addons menu.
+
+.. seealso::
+
+ If you are a poweruser, you may be interested in the **Unstable installation** to help with testing. :doc:`Read more <../devs/installation>`
+
+Where is the add-on installed?
+------------------------------
+
+If you downloaded Blender as a ``.zip`` file without running an installer, you
+will find the BlenderBIM Add-on installed in the following directory, where
+``X.XX`` is the Blender version:
+::
+
+ /path/to/blender/X.XX/scripts/addons/
+
+Otherwise, if you installed Blender using an installation package, the add-ons
+folder depends on which operating system you use. On Linux:
+::
+
+ ~/.config/blender/X.XX/scripts/addons/
+
+On Mac:
+::
+
+ /Users/{YOUR_USER}/Library/Application Support/Blender/X.XX/
+
+On Windows:
+::
+
+ C:\Users\{YOUR_USER}\AppData\Roaming\Blender Foundation\X.XX\scripts\addons
+
+Upon installation, the BlenderBIM Add-on is stored in the ``blenderbim/``
+directory.
+
+
+Updating
+--------
+
+First uninstall the current BlenderBIM add-on, then install the latest version.
+
+Uninstalling
+------------
+
+Navigate to ``Edit > Preferences > Add-ons``. Due to a limitation in Blender,
+you have to first disable the BlenderBIM Add-on in your Blender preferences by
+pressing the checkbox next to the add-on, then restart Blender. After
+restarting, you can uninstall the BlenderBIM Add-on by pressing the ``Remove``
+button in the Blender preferences window.
+
+Alternatively, you may uninstall manually by deleting the ``blenderbim/``
+directory in your Blender add-ons directory.
+
+.. warning::
+
+ It is important to follow the sequence of disabling, restarting, then removing.
+ If you do not restart Blender, the add-on will fail to remove correctly, and you
+ will need to uninstall manually.
+
+
+FAQ
+---
+
+1. **I get an error similar to "ImportError: IfcOpenShell not built for 'linux/64bit/python3.7'"**
+
+ Check which BlenderBIM Add-on build you are using. The zip will have either
+ ``py37``, ``py39``, or ``py310`` in the name. See the instructions in the
+ **Unstable installation** section to check that you have installed the
+ correct version.
+
+2. **I am on Ubuntu and get an error similar to "ImportError: /lib/x86_64-linux-gnu/libm.so.6: version GLIBC_2.29 not found"**
+
+ Our latest package which uses IfcOpenShell v0.7.0 is built using Ubuntu 20 LTS.
+ If you have an older Ubuntu version, you can either upgrade to 19.10 or above,
+ or you'll need to compile IfcOpenShell yourself.
+
+3. **The add-on does not install on a Mac with an M1 arm chip**
+
+ The BlenderBIM Add-on uses the IfcOpenShell library, which is not yet
+ available for computers which use an ARM chip. You may
+ compile IfcOpenShell yourself to get this to work, but this may be too
+ difficult for most users. Please consider donating so that we can devote more
+ time to supporting all platforms.
diff --git a/src/blenderbim/docs/users/introduction_to_bim.rst b/src/blenderbim/docs/users/introduction_to_bim.rst
new file mode 100644
index 0000000000..36955d1b60
--- /dev/null
+++ b/src/blenderbim/docs/users/introduction_to_bim.rst
@@ -0,0 +1,59 @@
+Introduction to BIM
+===================
+
+**Building Information Modeling**, or **BIM**, is a way of digitally describing
+our built environment to computers. Aspects of our built environment that can be
+described are:
+
+- **Products**, like walls, doors, and windows
+- **Processes**, like construction or maintenance tasks, and procedures
+- **Resources**, like labour, materials, and equipment
+- **Controls**, like permits, orders, costs, or calendar availability
+- **Actors**, like occupants, clients, architects, and liable parties
+- **Groups**, like systems, inventories, or zones
+
+These objects may have lots of data and relationships. Examples of data might be
+classification systems, physical materials, associated documents, simulation
+results, and construction types. The data may be relevant to multiple
+disciplines, such as architecture, engineering, and construction.
+
+.. note::
+
+ BIM data is very different from a regular 3D model. In fact, geometry is
+ optional, and most data is non-geometric. This means that it is not simply a
+ 3D format that you can import or export from and expect meaningful results.
+
+**Industry Foundation Classes**, or **IFC**, is an international standard for
+**BIM**. **IFC** is the most well-established open digital language for our
+built environment. Most software will be able to describe their **BIM** data
+using **IFC**. Most commonly, **IFC** models will be shared as a ``.ifc`` file.
+
+For example, **IFC** will define a wall as an object that can have a name,
+construction type, and quantities. **IFC** will also describe that a wall that
+be associated with a location, like a building storey, or have an associated
+cost item in a schedule.
+
+When you use the BlenderBIM Add-on, you will be able to view and create **BIM**
+objects and relationships using the **IFC** standard.
+
+Things you can do
+-----------------
+
+The BlenderBIM Add-on is designed to be a full BIM authoring platform. Its
+capabilities have a similar scope to other modeling platforms, costing programs,
+scheduling software, CAD packages, and simulation software. It is too numerous
+to list in full, but an example of what is possible include:
+
+- Viewing models, including spaces, properties, and relationships
+- Edit and extract attributes and properties
+- Moving objects, and changing their geometry
+- Create new objects using library elements
+- Manage classification systems, document and library references
+- Generating 2D drawings, schedules, and creating sheets
+- Investigating and editing structural analysis models
+- Connecting and managing distribution systems and ports
+- Creating construction schedules, critical path analysis, and generating sequence animations
+- Creating cost schedules, using formulas, and deriving quantities from model elements
+- Clash detection and managing issues for model coordination
+
+... and much, much more.
diff --git a/src/blenderbim/docs/users/manualorigin.png b/src/blenderbim/docs/users/manualorigin.png
new file mode 100644
index 0000000000..ba769b7bd0
Binary files /dev/null and b/src/blenderbim/docs/users/manualorigin.png differ
diff --git a/src/blenderbim/docs/users/mapconversion.png b/src/blenderbim/docs/users/mapconversion.png
new file mode 100644
index 0000000000..1e3a5259cd
Binary files /dev/null and b/src/blenderbim/docs/users/mapconversion.png differ
diff --git a/src/blenderbim/docs/users/mapcoordinates.png b/src/blenderbim/docs/users/mapcoordinates.png
new file mode 100644
index 0000000000..b69be329d7
Binary files /dev/null and b/src/blenderbim/docs/users/mapcoordinates.png differ
diff --git a/src/blenderbim/docs/users/materials.png b/src/blenderbim/docs/users/materials.png
new file mode 100644
index 0000000000..8abaf46a04
Binary files /dev/null and b/src/blenderbim/docs/users/materials.png differ
diff --git a/src/blenderbim/docs/users/measure-tool.png b/src/blenderbim/docs/users/measure-tool.png
new file mode 100644
index 0000000000..05a3ff444c
Binary files /dev/null and b/src/blenderbim/docs/users/measure-tool.png differ
diff --git a/src/blenderbim/docs/users/navigate-gizmo.png b/src/blenderbim/docs/users/navigate-gizmo.png
new file mode 100644
index 0000000000..9d9dd1fb07
Binary files /dev/null and b/src/blenderbim/docs/users/navigate-gizmo.png differ
diff --git a/src/blenderbim/docs/users/offsetmode.png b/src/blenderbim/docs/users/offsetmode.png
new file mode 100644
index 0000000000..3731df4383
Binary files /dev/null and b/src/blenderbim/docs/users/offsetmode.png differ
diff --git a/src/blenderbim/docs/users/outliner-filter.png b/src/blenderbim/docs/users/outliner-filter.png
new file mode 100644
index 0000000000..08cafb6632
Binary files /dev/null and b/src/blenderbim/docs/users/outliner-filter.png differ
diff --git a/src/blenderbim/docs/users/outliner-isolate.png b/src/blenderbim/docs/users/outliner-isolate.png
new file mode 100644
index 0000000000..b63fd9cdc8
Binary files /dev/null and b/src/blenderbim/docs/users/outliner-isolate.png differ
diff --git a/src/blenderbim/docs/users/outliner-types.png b/src/blenderbim/docs/users/outliner-types.png
new file mode 100644
index 0000000000..fe951c0dfe
Binary files /dev/null and b/src/blenderbim/docs/users/outliner-types.png differ
diff --git a/src/blenderbim/docs/users/outliner.png b/src/blenderbim/docs/users/outliner.png
new file mode 100644
index 0000000000..d568f9ed07
Binary files /dev/null and b/src/blenderbim/docs/users/outliner.png differ
diff --git a/src/blenderbim/docs/users/projectedcrs.png b/src/blenderbim/docs/users/projectedcrs.png
new file mode 100644
index 0000000000..1997bf51ff
Binary files /dev/null and b/src/blenderbim/docs/users/projectedcrs.png differ
diff --git a/src/blenderbim/docs/users/properties-loadproject.png b/src/blenderbim/docs/users/properties-loadproject.png
new file mode 100644
index 0000000000..88f5be2ddc
Binary files /dev/null and b/src/blenderbim/docs/users/properties-loadproject.png differ
diff --git a/src/blenderbim/docs/users/properties-types.png b/src/blenderbim/docs/users/properties-types.png
new file mode 100644
index 0000000000..dd719f80a1
Binary files /dev/null and b/src/blenderbim/docs/users/properties-types.png differ
diff --git a/src/blenderbim/docs/users/psets.png b/src/blenderbim/docs/users/psets.png
new file mode 100644
index 0000000000..cae396693e
Binary files /dev/null and b/src/blenderbim/docs/users/psets.png differ
diff --git a/src/blenderbim/docs/users/qtos.png b/src/blenderbim/docs/users/qtos.png
new file mode 100644
index 0000000000..5746f10a83
Binary files /dev/null and b/src/blenderbim/docs/users/qtos.png differ
diff --git a/src/blenderbim/docs/users/scene-statistics.png b/src/blenderbim/docs/users/scene-statistics.png
new file mode 100644
index 0000000000..52362f050f
Binary files /dev/null and b/src/blenderbim/docs/users/scene-statistics.png differ
diff --git a/src/blenderbim/docs/users/snap-targets.png b/src/blenderbim/docs/users/snap-targets.png
new file mode 100644
index 0000000000..4a58a6193d
Binary files /dev/null and b/src/blenderbim/docs/users/snap-targets.png differ
diff --git a/src/blenderbim/docs/users/spatial-container.png b/src/blenderbim/docs/users/spatial-container.png
new file mode 100644
index 0000000000..a8c2c74e83
Binary files /dev/null and b/src/blenderbim/docs/users/spatial-container.png differ
diff --git a/src/blenderbim/docs/users/truenorth.png b/src/blenderbim/docs/users/truenorth.png
new file mode 100644
index 0000000000..a1700b5946
Binary files /dev/null and b/src/blenderbim/docs/users/truenorth.png differ
diff --git a/src/blenderbim/docs/users/type-local-view.png b/src/blenderbim/docs/users/type-local-view.png
new file mode 100644
index 0000000000..e448279567
Binary files /dev/null and b/src/blenderbim/docs/users/type-local-view.png differ
diff --git a/src/blenderbim/docs/users/viewport-dimensions.png b/src/blenderbim/docs/users/viewport-dimensions.png
new file mode 100644
index 0000000000..52dda47f68
Binary files /dev/null and b/src/blenderbim/docs/users/viewport-dimensions.png differ
diff --git a/src/blenderbim/docs/users/viewport-sidebar.png b/src/blenderbim/docs/users/viewport-sidebar.png
new file mode 100644
index 0000000000..7931d22a93
Binary files /dev/null and b/src/blenderbim/docs/users/viewport-sidebar.png differ
diff --git a/src/blenderbim/docs/users/x-ray-mode.png b/src/blenderbim/docs/users/x-ray-mode.png
new file mode 100644
index 0000000000..70afad3446
Binary files /dev/null and b/src/blenderbim/docs/users/x-ray-mode.png differ
diff --git a/src/blenderbim/docs/users/xaxisabscissaordinate.png b/src/blenderbim/docs/users/xaxisabscissaordinate.png
new file mode 100644
index 0000000000..abd0771a76
Binary files /dev/null and b/src/blenderbim/docs/users/xaxisabscissaordinate.png differ
diff --git a/src/blenderbim/icon-cad.blend b/src/blenderbim/icon-cad.blend
new file mode 100644
index 0000000000..92dc30b4a1
Binary files /dev/null and b/src/blenderbim/icon-cad.blend differ
diff --git a/src/blenderbim/pytest.ini b/src/blenderbim/pytest.ini
index 5cd46aa294..4ad6fa92de 100644
--- a/src/blenderbim/pytest.ini
+++ b/src/blenderbim/pytest.ini
@@ -10,6 +10,7 @@ markers =
document
drawing
geometry
+ georeference
library
material
misc
@@ -29,3 +30,4 @@ markers =
type
unit
void
+addopts = --color=yes --code-highlight=yes
diff --git a/src/blenderbim/scripts/au-library.blend b/src/blenderbim/scripts/au-library.blend
new file mode 100644
index 0000000000..0bf2d9f7b7
Binary files /dev/null and b/src/blenderbim/scripts/au-library.blend differ
diff --git a/src/blenderbim/demo-library.blend b/src/blenderbim/scripts/demo-library.blend
similarity index 100%
rename from src/blenderbim/demo-library.blend
rename to src/blenderbim/scripts/demo-library.blend
diff --git a/src/blenderbim/dxf2ifc.py b/src/blenderbim/scripts/dxf2ifc.py
similarity index 100%
rename from src/blenderbim/dxf2ifc.py
rename to src/blenderbim/scripts/dxf2ifc.py
diff --git a/src/blenderbim/extract.py b/src/blenderbim/scripts/extract.py
similarity index 100%
rename from src/blenderbim/extract.py
rename to src/blenderbim/scripts/extract.py
diff --git a/src/blenderbim/gbxml.py b/src/blenderbim/scripts/gbxml.py
similarity index 100%
rename from src/blenderbim/gbxml.py
rename to src/blenderbim/scripts/gbxml.py
diff --git a/src/blenderbim/scripts/generate_au_library.py b/src/blenderbim/scripts/generate_au_library.py
new file mode 100644
index 0000000000..7dab03c45f
--- /dev/null
+++ b/src/blenderbim/scripts/generate_au_library.py
@@ -0,0 +1,254 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2020, 2021 Dion Moult
+#
+# This file is part of BlenderBIM Add-on.
+#
+# BlenderBIM Add-on is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# BlenderBIM Add-on is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with BlenderBIM Add-on. If not, see .
+
+# fmt: off
+# pylint: skip-file
+
+import bpy
+import ifcopenshell
+import ifcopenshell.api
+import blenderbim.tool as tool
+
+
+class LibraryGenerator:
+ def generate(self):
+ ifcopenshell.api.pre_listeners = {}
+ ifcopenshell.api.post_listeners = {}
+
+ self.materials = {}
+
+ self.file = ifcopenshell.api.run("project.create_file")
+ self.project = ifcopenshell.api.run(
+ "root.create_entity", self.file, ifc_class="IfcProject", name="Australian Library"
+ )
+ self.library = ifcopenshell.api.run(
+ "root.create_entity", self.file, ifc_class="IfcProjectLibrary", name="Australian Library"
+ )
+ ifcopenshell.api.run(
+ "project.assign_declaration", self.file, definition=self.library, relating_context=self.project
+ )
+ unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI")
+ ifcopenshell.api.run("unit.assign_unit", self.file, units=[unit])
+
+ model = ifcopenshell.api.run("context.add_context", self.file, context_type="Model")
+ plan = ifcopenshell.api.run("context.add_context", self.file, context_type="Plan")
+ self.representations = {
+ "body": ifcopenshell.api.run(
+ "context.add_context",
+ self.file,
+ context_type="Model",
+ context_identifier="Body",
+ target_view="MODEL_VIEW",
+ parent=model,
+ ),
+ "annotation": ifcopenshell.api.run(
+ "context.add_context",
+ self.file,
+ context_type="Plan",
+ context_identifier="Annotation",
+ target_view="PLAN_VIEW",
+ parent=plan,
+ ),
+ }
+
+ self.materials = {
+ "TBD": {"Category": "other"},
+ "BRK": {"Category": "brick"},
+ "BLK": {"Category": "block"},
+ "CON": {"Category": "concrete"},
+ "WD": {"Category": "wood"},
+ "ST": {"Category": "steel"},
+ "AL": {"Category": "aluminium"},
+ "GLZ": {"Category": "glass"},
+ "PBD": {"Category": "gypsum"},
+ "AIR": {"Category": "air"},
+ "MEM": {"Category": "membrane"},
+ }
+
+ for name, data in self.materials.items():
+ self.materials[name]["ifc"] = ifcopenshell.api.run("material.add_material", self.file, name=name)
+ self.materials[name]["ifc"].Category = data["Category"]
+
+ self.layer_types = {
+ "WAL01": {"type": "IfcWallType", "Description": "110mm Single brick",
+ "Layers": [("LoadBearing", "BRK", 110)]},
+ "WAL02": {"type": "IfcWallType", "Description": "250mm Brick veneer",
+ "Layers": [("Finish", "BRK", 110), ("Ventilation", "AIR", 50), ("LoadBearing", "WD", 90)]},
+ "WAL03": {"type": "IfcWallType", "Description": "270mm Double brick",
+ "Layers": [("Finish", "BRK", 110), ("Ventilation", "AIR", 50), ("LoadBearing", "BRK", 110)]},
+ "WAL04": {"type": "IfcWallType", "Description": "150mm Reinforced concrete",
+ "Layers": [("LoadBearing", "CON", 150)]},
+ "WAL05": {"type": "IfcWallType", "Description": "200mm Reinforced concrete",
+ "Layers": [("LoadBearing", "CON", 200)]},
+ "WAL06": {"type": "IfcWallType", "Description": "250mm Reinforced concrete",
+ "Layers": [("LoadBearing", "CON", 250)]},
+ "WAL07": {"type": "IfcWallType", "Description": "300mm Reinforced concrete",
+ "Layers": [("LoadBearing", "CON", 300)]},
+ "WAL08": {"type": "IfcWallType", "Description": "140mm Hollow core block",
+ "Layers": [("LoadBearing", "BLK", 190)]},
+ "WAL09": {"type": "IfcWallType", "Description": "140mm Core filled block",
+ "Layers": [("LoadBearing", "BLK", 190)]},
+ "WAL10": {"type": "IfcWallType", "Description": "190mm Hollow core block",
+ "Layers": [("LoadBearing", "BLK", 190)]},
+ "WAL11": {"type": "IfcWallType", "Description": "190mm Core filled block",
+ "Layers": [("LoadBearing", "BLK", 190)]},
+ "WAL12": {"type": "IfcWallType", "Description": "90mm Wooden frame",
+ "Layers": [("LoadBearing", "WD", 90)]},
+ "WAL13": {"type": "IfcWallType", "Description": "64mm Steel frame",
+ "Layers": [("LoadBearing", "ST", 64)]},
+ "CLD01": {"type": "IfcCoveringType", "Description": "110mm Face brick",
+ "Layers": [("LoadBearing", "BRK", 110)]},
+ "CLD02": {"type": "IfcCoveringType", "Description": "13mm Plasterboard lining",
+ "Layers": [("LoadBearing", "PBD", 13)]},
+ }
+
+ for name, data in self.layer_types.items():
+ self.layer_types[name]["ifc"] = self.create_layer_set_type(name, data)
+
+ product = self.create_layer_type("IfcCoveringType", "DEMO20", 0.02)
+ pset = ifcopenshell.api.run("pset.add_pset", self.file, product=product, name="EPset_Parametric")
+ ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"LayerSetDirection": "AXIS2"})
+
+ product = self.create_layer_type("IfcCoveringType", "DEMO30", 0.03)
+ pset = ifcopenshell.api.run("pset.add_pset", self.file, product=product, name="EPset_Parametric")
+ ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"LayerSetDirection": "AXIS3"})
+
+ self.create_layer_type("IfcRampType", "DEMO200", 0.2)
+
+ profile = self.file.create_entity("IfcCircleProfileDef", ProfileType="AREA", Radius=0.3)
+ self.create_profile_type("IfcPileType", "DEMO1", profile)
+
+ self.create_layer_type("IfcSlabType", "DEMO150", 0.2)
+ self.create_layer_type("IfcSlabType", "DEMO250", 0.3)
+
+ profile = self.file.create_entity("IfcRectangleProfileDef", ProfileType="AREA", XDim=0.5, YDim=0.6)
+ self.create_profile_type("IfcColumnType", "DEMO1", profile)
+
+ profile = self.file.create_entity(
+ "IfcCircleHollowProfileDef", ProfileType="AREA", Radius=0.25, WallThickness=0.005
+ )
+ self.create_profile_type("IfcColumnType", "DEMO2", profile)
+
+ profile = self.file.create_entity(
+ "IfcRectangleHollowProfileDef",
+ ProfileType="AREA",
+ XDim=0.075,
+ YDim=0.15,
+ WallThickness=0.005,
+ InnerFilletRadius=0.005,
+ OuterFilletRadius=0.005,
+ )
+ self.create_profile_type("IfcColumnType", "DEMO3", profile)
+
+ profile = self.file.create_entity(
+ "IfcIShapeProfileDef",
+ ProfileName="DEMO-I",
+ ProfileType="AREA",
+ OverallWidth=0.1,
+ OverallDepth=0.2,
+ WebThickness=0.005,
+ FlangeThickness=0.01,
+ FilletRadius=0.005,
+ )
+ self.create_profile_type("IfcBeamType", "DEMO1", profile)
+
+ profile = self.file.create_entity(
+ "IfcCShapeProfileDef",
+ ProfileName="DEMO-C",
+ ProfileType="AREA",
+ Depth=0.2,
+ Width=0.1,
+ WallThickness=0.0015,
+ Girth=0.03,
+ InternalFilletRadius=0.005,
+ )
+ self.create_profile_type("IfcBeamType", "DEMO2", profile)
+
+ self.create_type("IfcWindowType", "DEMO1", {"body": "Window", "annotation": "Window-Annotation"})
+ self.create_type("IfcDoorType", "DEMO1", {"body": "Door", "annotation": "Door-Annotation"})
+ self.create_type("IfcFurnitureType", "BUNNY", {"body": "Bunny", "annotation": "Bunny-Annotation"})
+
+ self.file.write("blenderbim-au-library.ifc")
+
+ def create_layer_set_type(self, name, data):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=data["type"], name=name)
+ element.Description = data["Description"]
+ rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet")
+ layer_set = rel.RelatingMaterial
+ for layer_data in data["Layers"]:
+ layer = ifcopenshell.api.run(
+ "material.add_layer", self.file, layer_set=layer_set, material=self.materials[layer_data[1]]["ifc"]
+ )
+ layer.Name = layer_data[0]
+ layer.LayerThickness = layer_data[2]
+ ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
+ return element
+
+ def create_layer_type(self, ifc_class, name, thickness):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
+ rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialLayerSet")
+ layer_set = rel.RelatingMaterial
+ layer = ifcopenshell.api.run("material.add_layer", self.file, layer_set=layer_set, material=self.materials["TBD"]["ifc"])
+ layer.LayerThickness = thickness
+ ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
+ return element
+
+ def create_profile_type(self, ifc_class, name, profile):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
+ rel = ifcopenshell.api.run("material.assign_material", self.file, product=element, type="IfcMaterialProfileSet")
+ profile_set = rel.RelatingMaterial
+ material_profile = ifcopenshell.api.run(
+ "material.add_profile", self.file, profile_set=profile_set, material=self.materials["TBD"]["ifc"]
+ )
+ ifcopenshell.api.run("material.assign_profile", self.file, material_profile=material_profile, profile=profile)
+ ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
+
+ def create_type(self, ifc_class, name, representations):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class=ifc_class, name=name)
+ for rep_name, obj_name in representations.items():
+ obj = bpy.data.objects.get(obj_name)
+ representation = ifcopenshell.api.run(
+ "geometry.add_representation",
+ self.file,
+ context=self.representations[rep_name],
+ blender_object=obj,
+ geometry=obj.data,
+ total_items=max(1, len(obj.material_slots)),
+ )
+ styles = []
+ for slot in obj.material_slots:
+ style = ifcopenshell.api.run("style.add_style", self.file, name=slot.material.name)
+ ifcopenshell.api.run(
+ "style.add_surface_style",
+ self.file,
+ style=style,
+ ifc_class="IfcSurfaceStyleRendering",
+ attributes=tool.Style.get_surface_rendering_attributes(slot.material),
+ )
+ styles.append(style)
+ if styles:
+ ifcopenshell.api.run(
+ "style.assign_representation_styles", self.file, shape_representation=representation, styles=styles
+ )
+ ifcopenshell.api.run(
+ "geometry.assign_representation", self.file, product=element, representation=representation
+ )
+ ifcopenshell.api.run("project.assign_declaration", self.file, definition=element, relating_context=self.library)
+
+
+LibraryGenerator().generate()
diff --git a/src/blenderbim/generate_demo_library.py b/src/blenderbim/scripts/generate_demo_library.py
similarity index 100%
rename from src/blenderbim/generate_demo_library.py
rename to src/blenderbim/scripts/generate_demo_library.py
diff --git a/src/blenderbim/generate_site_library.py b/src/blenderbim/scripts/generate_site_library.py
similarity index 100%
rename from src/blenderbim/generate_site_library.py
rename to src/blenderbim/scripts/generate_site_library.py
diff --git a/src/blenderbim/generate_util_type_json.py b/src/blenderbim/scripts/generate_util_type_json.py
similarity index 100%
rename from src/blenderbim/generate_util_type_json.py
rename to src/blenderbim/scripts/generate_util_type_json.py
diff --git a/src/blenderbim/getIfcElements.py b/src/blenderbim/scripts/getIfcElements.py
similarity index 100%
rename from src/blenderbim/getIfcElements.py
rename to src/blenderbim/scripts/getIfcElements.py
diff --git a/src/blenderbim/get_description.py b/src/blenderbim/scripts/get_description.py
similarity index 100%
rename from src/blenderbim/get_description.py
rename to src/blenderbim/scripts/get_description.py
diff --git a/src/blenderbim/get_volume_by_material.py b/src/blenderbim/scripts/get_volume_by_material.py
similarity index 100%
rename from src/blenderbim/get_volume_by_material.py
rename to src/blenderbim/scripts/get_volume_by_material.py
diff --git a/src/blenderbim/scripts/headless_import.py b/src/blenderbim/scripts/headless_import.py
new file mode 100644
index 0000000000..07596d6c08
--- /dev/null
+++ b/src/blenderbim/scripts/headless_import.py
@@ -0,0 +1,54 @@
+# This can be run using `blender -b -P headless_import.py`
+
+import bpy
+from blenderbim.bim.ifc import IfcStore
+
+# When federating, you may wish to manually specify the origin to ensure models
+# with different or arbitrary origin conventions will turn up in the right spot.
+
+props = bpy.context.scene.BIMGeoreferenceProperties
+
+# A good idea it to test import a portion of the model (or grids only) and check
+# georeferencing coordinates in the IFC Georeferencing panel before filling out
+# these details.
+
+#props.blender_eastings = '334134181.0'
+#props.blender_northings = '6254570511.0'
+#props.blender_orthogonal_height = '0.0'
+#props.blender_x_axis_abscissa = '0.147487884244522'
+#props.blender_x_axis_ordinate = '0.989063862448262'
+#props.has_blender_offset = True
+
+props = bpy.context.scene.BIMProjectProperties
+
+# Generally recommended to disable caching for stability right now
+props.should_cache = False
+
+# If you are not authoring, it is recommended to enable this.
+# When enabled, types, openings, and non geometric elements are not loaded.
+props.is_coordinating = True
+
+# Merging is a good strategy to keep object counts low in huge models.
+
+# Leave it to none if you do not have a huge model.
+props.merge_mode = "NONE"
+
+# A good merge strategy when the type is significant
+props.merge_mode = "IFC_TYPE"
+
+# A good merge strategy to really get object counts low where types don't matter, like for structural models
+props.merge_mode = "IFC_CLASS"
+
+# If you want to import a range of elements, say the first 30k elements, set the
+# offset and limit here. 30k is generally useful as a guideline to keep models
+# responsive (though my computer can handle 80k).
+props.element_offset = 0
+props.element_limit = 30000
+
+bpy.ops.bim.load_project(filepath="/path/to/your/model/input.ifc")
+
+# In coordination mode, it is unnecessary to maintain a link to the IFC
+bpy.ops.bim.unload_project()
+
+# Save the processed file here
+bpy.ops.wm.save_mainfile(filepath="/path/to/your/model/output.blend")
diff --git a/src/blenderbim/obj2ifc.py b/src/blenderbim/scripts/obj2ifc.py
similarity index 100%
rename from src/blenderbim/obj2ifc.py
rename to src/blenderbim/scripts/obj2ifc.py
diff --git a/src/blenderbim/scripts/setup_pytest.py b/src/blenderbim/scripts/setup_pytest.py
index 8a722bc015..552dbf55b0 100644
--- a/src/blenderbim/scripts/setup_pytest.py
+++ b/src/blenderbim/scripts/setup_pytest.py
@@ -25,3 +25,4 @@ subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"])
subprocess.call([py_exec, "-m", "pip", "install", "pytest"])
subprocess.call([py_exec, "-m", "pip", "install", "pytest-blender"])
subprocess.call([py_exec, "-m", "pip", "install", "pytest-bdd"])
+subprocess.call([py_exec, "-m", "pip", "install", "pygments"])
diff --git a/src/blenderbim/site-library.blend b/src/blenderbim/scripts/site-library.blend
similarity index 100%
rename from src/blenderbim/site-library.blend
rename to src/blenderbim/scripts/site-library.blend
diff --git a/src/blenderbim/scripts/standalone_blender_import.py b/src/blenderbim/scripts/standalone_blender_import.py
new file mode 100644
index 0000000000..c91c2af0da
--- /dev/null
+++ b/src/blenderbim/scripts/standalone_blender_import.py
@@ -0,0 +1,125 @@
+import ifcopenshell
+import ifcopenshell.api
+import ifcopenshell.geom
+import multiprocessing
+
+
+class BlenderImporter:
+ def __init__(self):
+ self.file = ifcopenshell.open('/home/dion/untitled.ifc')
+ self.cache_path = "cache.h5"
+ self.should_use_cpu_multiprocessing = True
+ self.deflection_tolerance = 0.001
+ self.angular_tolerance = 0.5
+
+ def execute(self):
+ self.created_guids = set()
+
+ self.settings = ifcopenshell.geom.settings()
+ self.settings.set_deflection_tolerance(self.deflection_tolerance)
+ self.settings.set_angular_tolerance(self.angular_tolerance)
+ self.settings.set(self.settings.STRICT_TOLERANCE, True)
+ self.settings_2d = ifcopenshell.geom.settings()
+ self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True)
+ self.settings_2d.set(self.settings.STRICT_TOLERANCE, True)
+
+ self.process_element_filter()
+ self.process_context_filter()
+ self.create_elements(self.elements)
+
+ def process_element_filter(self):
+ # Blender users can typically customise a filtered set of elements to import.
+ # For simplicity, let's select everything:
+ self.elements = set(self.file.by_type("IfcProduct"))
+
+ def process_context_filter(self):
+ # Facetation is to accommodate broken Revit files
+ # See https://forums.buildingsmart.org/t/suggestions-on-how-to-improve-clarity-of-representation-context-usage-in-documentation/3663/6?u=moult
+ self.body_contexts = [
+ c.id()
+ for c in self.file.by_type("IfcGeometricRepresentationSubContext")
+ if c.ContextIdentifier in ["Body", "Facetation"]
+ ]
+ # Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly
+ self.body_contexts.extend(
+ [
+ c.id()
+ for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
+ if c.ContextType == "Model"
+ ]
+ )
+ if self.body_contexts:
+ self.settings.set_context_ids(self.body_contexts)
+ # Annotation is to accommodate broken Revit files
+ # See https://github.com/Autodesk/revit-ifc/issues/187
+ self.plan_contexts = [
+ c.id()
+ for c in self.file.by_type("IfcGeometricRepresentationContext")
+ if c.ContextType in ["Plan", "Annotation"]
+ ]
+ if self.plan_contexts:
+ self.settings_2d.set_context_ids(self.plan_contexts)
+
+ def create_elements(self, elements):
+ # Based on my experience in viewing BIM models, representations are prioritised as follows:
+ # 1. 3D Body, 2. 2D Plans, 3. Point clouds, 4. No representation
+ # If an element has a representation that doesn't follow 1, 2, or 3, it will not show by default.
+ # The user can load them later if they want to view them.
+ products = self.create_products(elements)
+ elements -= products
+ products = self.create_products(elements, is_curve=True)
+ elements -= products
+ # Creating point clouds and non geometric objects are specific to Blender so no code is shown.
+ pass
+
+ def create_products(self, products, is_curve=False):
+ results = set()
+ if not products:
+ return results
+ settings = self.settings_2d if is_curve else self.settings
+ if self.should_use_cpu_multiprocessing:
+ iterator = ifcopenshell.geom.iterator(
+ settings, self.file, multiprocessing.cpu_count(), include=products
+ )
+ else:
+ iterator = ifcopenshell.geom.iterator(settings, self.file, include=products)
+ cache = self.get_cache()
+ if cache:
+ iterator.set_cache(cache)
+ valid_file = iterator.initialize()
+ if not valid_file:
+ return results
+ total = 0
+ while True:
+ total += 1
+ if total % 250 == 0:
+ print(f"{total} elements processed")
+ shape = iterator.get()
+ if shape:
+ product = self.file.by_id(shape.guid)
+ if self.body_contexts:
+ print("Created", product)
+ self.created_guids.add(shape.guid)
+ results.add(product)
+ else:
+ if shape.context not in ["Body", "Facetation"] and shape.guid in self.created_guids:
+ # We only load a single context, and we prioritise the Body context. See #1290.
+ pass
+ else:
+ print("Created", product)
+ self.created_guids.add(shape.guid)
+ results.add(product)
+ if not iterator.next():
+ break
+ print("Done creating geometry")
+ return results
+
+ def get_cache(self):
+ cache_settings = ifcopenshell.geom.settings()
+ cache_settings.set(cache_settings.STRICT_TOLERANCE, True)
+ try:
+ return ifcopenshell.geom.serializers.hdf5(self.cache_path, cache_settings)
+ except:
+ return
+
+BlenderImporter().execute()
diff --git a/src/blenderbim/scripts/standalone_section_shader.py b/src/blenderbim/scripts/standalone_section_shader.py
index 8068914183..7d732dad2a 100644
--- a/src/blenderbim/scripts/standalone_section_shader.py
+++ b/src/blenderbim/scripts/standalone_section_shader.py
@@ -13,11 +13,9 @@ bl_info = {
import bpy
-from bpy.types import Operator
-from bpy.props import FloatVectorProperty
+from bpy.types import Operator, PropertyGroup, Panel
from mathutils import Vector, Matrix, Euler
from math import radians
-from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
StringProperty,
@@ -30,62 +28,131 @@ from bpy.props import (
)
-def update_section_color(self, context):
- section_node_group = bpy.data.node_groups.get("Section Override")
- if section_node_group is None:
- return
- try:
- emission_node = next(n for n in section_node_group.nodes if isinstance(n, bpy.types.ShaderNodeEmission))
- emission_node.inputs[0].default_value = list(self.section_plane_colour) + [1]
- except StopIteration:
- pass
+class SectionCutawayManager:
+ @staticmethod
+ def get_section_tree():
+ return bpy.data.node_groups.get("Section Override")
+ @staticmethod
+ def get_sections_map():
+ section_tree = SectionCutawayManager.get_section_tree()
+ return {n: n.object for n in section_tree.nodes if isinstance(n, bpy.types.ShaderNodeTexCoord)}
-class BIM_OT_add_section_plane(bpy.types.Operator):
- """Add a temporary empty object as a section cutaway. Cull all geometry rendering below the empty's local Z axis"""
+ @staticmethod
+ def purge_all_section_data():
+ section_override_mat = bpy.data.materials.get("Section Override")
+ if section_override_mat:
+ bpy.data.materials.remove(section_override_mat)
+ for material in bpy.data.materials:
+ if not material.node_tree:
+ continue
+ override = material.node_tree.nodes.get("Section Override")
+ if not override:
+ continue
+ material.node_tree.links.new(
+ override.inputs[0].links[0].from_socket, override.outputs[0].links[0].to_socket
+ )
+ material.node_tree.nodes.remove(override)
+ bpy.data.node_groups.remove(SectionCutawayManager.get_section_tree())
+ bpy.data.node_groups.remove(bpy.data.node_groups.get("Section Compare"))
- bl_idname = "bim.add_section_plane"
- bl_label = "Add Temporary Section Cutaway"
- bl_options = {"REGISTER", "UNDO"}
+ @staticmethod
+ def clean():
+ sections_map = SectionCutawayManager.get_sections_map()
+ sections_map_iterator = list(sections_map.items())
+ for tex_coord_node, section_obj in sections_map_iterator:
+ if section_obj is None or section_obj.users == 1:
+ SectionCutawayManager.unplug(tex_coord_node)
+ sections_map.pop(tex_coord_node)
+ if not any(o for o in sections_map.values() if o.users > 1):
+ SectionCutawayManager.purge_all_section_data()
- def execute(self, context):
- obj = self.create_section_obj(context)
- if not self.has_section_override_node():
- self.create_section_compare_node()
- self.create_section_override_node(obj, context)
- else:
- self.append_obj_to_section_override_node(obj)
- self.add_default_material_if_none_exists(context)
- self.override_materials()
- return {"FINISHED"}
+ @staticmethod
+ def unplug(tex_coords):
+ section_tree = SectionCutawayManager.get_section_tree()
+ section_compare = tex_coords.outputs["Object"].links[0].to_node
+ if section_compare.inputs[0].links:
+ previous_section_compare = section_compare.inputs[0].links[0].from_node
+ next_section_compare = section_compare.outputs[0].links[0].to_node
+ section_tree.links.new(previous_section_compare.outputs[0], next_section_compare.inputs[0])
+ SectionCutawayManager.offset_previous_nodes(section_compare, offset_x=200)
+ section_tree.nodes.remove(section_compare)
+ section_tree.nodes.remove(tex_coords)
- def create_section_obj(self, context):
+ @staticmethod
+ def offset_previous_nodes(section_compare, offset_x=0, offset_y=0):
+ if section_compare.inputs[0].links:
+ previous_section_compare = section_compare.inputs[0].links[0].from_node
+ previous_section_compare.location += Vector((offset_x, offset_y))
+ if previous_section_compare.inputs[1].links:
+ previous_section_compare.inputs[1].links[0].from_node.location += Vector((offset_x, offset_y))
+ SectionCutawayManager.offset_previous_nodes(previous_section_compare, offset_x, offset_y)
+
+ @staticmethod
+ def override_materials():
+ override = SectionCutawayManager.get_section_tree()
+ for material in bpy.data.materials:
+ material.use_nodes = True
+ if material.node_tree.nodes.get(override.name):
+ continue
+ material.blend_method = "HASHED"
+ material.shadow_method = "HASHED"
+ material_output = next((n for n in material.node_tree.nodes if n.type == "OUTPUT_MATERIAL"), None)
+ if material_output is None:
+ continue
+ from_socket = material_output.inputs[0].links[0].from_socket
+ section_override = material.node_tree.nodes.new(type="ShaderNodeGroup")
+ section_override.name = "Section Override"
+ section_override.node_tree = override
+ material.node_tree.links.new(from_socket, section_override.inputs[0])
+ material.node_tree.links.new(section_override.outputs[0], material_output.inputs[0])
+
+ @staticmethod
+ def create_section_obj(from_obj, scene):
section = bpy.data.objects.new("Section", None)
section.empty_display_type = "SINGLE_ARROW"
section.empty_display_size = 5
section.show_in_front = True
- if (
- context.active_object
- and context.active_object.select_get()
- and isinstance(context.active_object.data, bpy.types.Camera)
- ):
- section.matrix_world = (
- context.active_object.matrix_world @ Euler((radians(180.0), 0.0, 0.0), "XYZ").to_matrix().to_4x4()
- )
+ if from_obj and from_obj.select_get() and isinstance(from_obj.data, bpy.types.Camera):
+ section.matrix_world = from_obj.matrix_world @ Euler((radians(180.0), 0.0, 0.0), "XYZ").to_matrix().to_4x4()
else:
section.rotation_euler = Euler((radians(180.0), 0.0, 0.0), "XYZ")
- section.location = context.scene.cursor.location
+ section.location = scene.cursor.location
collection = bpy.data.collections.get("Sections")
if not collection:
collection = bpy.data.collections.new("Sections")
- context.scene.collection.children.link(collection)
+ scene.collection.children.link(collection)
collection.objects.link(section)
return section
- def has_section_override_node(self):
- return bpy.data.node_groups.get("Section Override")
+ @classmethod
+ def add_section_plane(cls, context):
+ obj = cls.create_section_obj(context.active_object, context.scene)
+ if cls.get_section_tree() is not None:
+ cls.append_obj_to_section_override_node(obj)
+ else:
+ cls.create_section_compare_node()
+ cls.create_section_override_node(obj, context)
+ cls.add_default_material_if_none_exists(context)
+ cls.override_materials()
- def create_section_compare_node(self):
+ @classmethod
+ def remove_all_section_planes(cls):
+ objs = [o for o in cls.get_sections_map().values() if o is not None]
+ cls.remove_section_planes(objs)
+
+ @classmethod
+ def remove_section_planes(cls, objects):
+ sections_map = cls.get_sections_map()
+ for obj in objects:
+ tex_coords = next((n for n in sections_map if sections_map[n] == obj), None)
+ if tex_coords is not None:
+ cls.unplug(tex_coords)
+ sections_map.pop(tex_coords)
+ bpy.data.objects.remove(obj)
+
+ @staticmethod
+ def create_section_compare_node():
group = bpy.data.node_groups.new("Section Compare", type="ShaderNodeTree")
group_input = group.nodes.new(type="NodeGroupInput")
group_input.location = 0, 50
@@ -112,7 +179,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
group.links.new(greater.outputs[0], multiply.inputs[1])
group.links.new(multiply.outputs[0], group_output.inputs[""])
- def create_section_override_node(self, obj, context):
+ @staticmethod
+ def create_section_override_node(obj, context):
group = bpy.data.node_groups.new("Section Override", type="ShaderNodeTree")
links = group.links
nodes = group.nodes
@@ -121,45 +189,53 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
group_output = nodes.new(type="NodeGroupOutput")
group_output.location = 600, 250
+ hide_mix = group.nodes.new(type="ShaderNodeMixShader")
+ hide_mix.name = "Hide Mix"
+ hide_mix.inputs[0].default_value = 0
+ hide_mix.location = group_output.location - Vector((200, 0))
+
backfacing_mix = nodes.new(type="ShaderNodeMixShader")
- backfacing_mix.location = group_output.location - Vector((400, 350))
+ backfacing_mix.location = group_output.location - Vector((600, 350))
backfacing = nodes.new(type="ShaderNodeNewGeometry")
- backfacing.location = backfacing_mix.location + Vector((-200, 200))
- group_input.location = backfacing_mix.location - Vector((200, 50))
+ backfacing.location = backfacing_mix.location + Vector((-400, 200))
+ group_input.location = backfacing_mix.location - Vector((400, 50))
emission = nodes.new(type="ShaderNodeEmission")
emission.inputs[0].default_value = list(context.scene.SectionProperties.section_plane_colour) + [1]
- emission.location = backfacing_mix.location - Vector((200, 150))
+ emission.location = backfacing_mix.location - Vector((400, 150))
transparent = nodes.new(type="ShaderNodeBsdfTransparent")
- transparent.location = group_output.location - Vector((400, 100))
+ transparent.location = group_output.location - Vector((600, 100))
section_mix = group.nodes.new(type="ShaderNodeMixShader")
section_mix.name = "Section Mix"
section_mix.inputs[0].default_value = 1 # Directly pass input shader when there is no cutaway
- section_mix.location = group_output.location - Vector((200, 0))
+ section_mix.location = group_output.location - Vector((400, 0))
cut_obj = nodes.new(type="ShaderNodeTexCoord")
cut_obj.object = obj
- cut_obj.location = group_output.location - Vector((800, 150))
+ cut_obj.location = group_output.location - Vector((1000, 150))
section_compare = nodes.new(type="ShaderNodeGroup")
section_compare.node_tree = bpy.data.node_groups.get("Section Compare")
section_compare.name = "Last Section Compare"
- section_compare.location = group_output.location - Vector((600, 0))
+ section_compare.location = group_output.location - Vector((800, 0))
links.new(cut_obj.outputs["Object"], section_compare.inputs[1])
links.new(backfacing.outputs["Backfacing"], backfacing_mix.inputs[0])
links.new(group_input.outputs[""], backfacing_mix.inputs[1])
links.new(emission.outputs["Emission"], backfacing_mix.inputs[2])
links.new(section_compare.outputs[0], section_mix.inputs[0])
- links.new(transparent.outputs["BSDF"], section_mix.inputs[1])
- links.new(backfacing_mix.outputs["Shader"], section_mix.inputs[2])
- links.new(section_mix.outputs["Shader"], group_output.inputs[""])
+ links.new(transparent.outputs[0], section_mix.inputs[1])
+ links.new(backfacing_mix.outputs[0], section_mix.inputs[2])
+ links.new(section_mix.outputs[0], hide_mix.inputs[1])
+ links.new(group_input.outputs["Shader"], hide_mix.inputs[2])
+ links.new(hide_mix.outputs[0], group_output.inputs[""])
- def append_obj_to_section_override_node(self, obj):
- group = bpy.data.node_groups.get("Section Override")
+ @staticmethod
+ def append_obj_to_section_override_node(obj):
+ group = SectionCutawayManager.get_section_tree()
try:
last_section_node = next(
n
@@ -185,7 +261,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
section_compare.name = "Last Section Compare"
- def add_default_material_if_none_exists(self, context):
+ @staticmethod
+ def add_default_material_if_none_exists(context):
material = bpy.data.materials.get("Section Override")
if not material:
material = bpy.data.materials.new("Section Override")
@@ -208,92 +285,74 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
else:
obj.data.materials.append(material)
- def override_materials(self):
- override = bpy.data.node_groups.get("Section Override")
- for material in bpy.data.materials:
- material.use_nodes = True
- if material.node_tree.nodes.get("Section Override"):
- continue
- material.blend_method = "HASHED"
- material.shadow_method = "HASHED"
- material_output = self.get_node(material.node_tree.nodes, "OUTPUT_MATERIAL")
- if not material_output:
- continue
- from_socket = material_output.inputs[0].links[0].from_socket
- section_override = material.node_tree.nodes.new(type="ShaderNodeGroup")
- section_override.name = "Section Override"
- section_override.node_tree = override
- material.node_tree.links.new(from_socket, section_override.inputs[0])
- material.node_tree.links.new(section_override.outputs[0], material_output.inputs[0])
- def get_node(self, nodes, node_type):
- for node in nodes:
- if node.type == node_type:
- return node
+class BIM_OT_section_plane_refresh(Operator):
+ bl_idname = "bim.section_plane_refresh"
+ bl_label = "Refresh Section Cutaway Shader"
+ bl_options = {"REGISTER", "UNDO"}
+
+ def execute(self, context):
+ update_visibility(None, context)
+ SectionCutawayManager.add_default_material_if_none_exists(context)
+ SectionCutawayManager.clean()
+ return {"FINISHED"}
-class BIM_OT_remove_section_plane(bpy.types.Operator):
- """Remove selected section plane. No effect if executed on a regular object"""
+class BIM_OT_section_plane_add(Operator):
+ """Add a temporary empty object as a section cutaway. Cull all geometry rendering below the empty's local Z axis"""
- bl_idname = "bim.remove_section_plane"
+ bl_idname = "bim.section_plane_add"
+ bl_label = "Add Temporary Section Cutaway"
+ bl_options = {"REGISTER", "UNDO"}
+
+ def execute(self, context):
+ SectionCutawayManager.add_section_plane(context)
+ SectionCutawayManager.clean()
+ return {"FINISHED"}
+
+
+class BIM_OT_section_plane_remove(Operator):
+ bl_idname = "bim.section_plane_remove"
bl_label = "Remove Temporary Section Cutaway"
bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Remove section planes. No effect if executed on a regular object"
+ remove_all: BoolProperty(default=False, options={"HIDDEN"})
@classmethod
def poll(cls, context):
- return context.active_object and bpy.data.node_groups.get("Section Override")
+ return SectionCutawayManager.get_section_tree() is not None
def execute(self, context):
- name = context.active_object.name
- section_override = bpy.data.node_groups.get("Section Override")
- tex_coords = next(
- (
- n
- for n in section_override.nodes
- if isinstance(n, bpy.types.ShaderNodeTexCoord) and n.object.name == name
- ),
- None,
- )
- if tex_coords is not None:
- section_compare = tex_coords.outputs["Object"].links[0].to_node
- if section_compare.inputs[0].links:
- previous_section_compare = section_compare.inputs[0].links[0].from_node
- next_section_compare = section_compare.outputs[0].links[0].to_node
- section_override.links.new(previous_section_compare.outputs[0], next_section_compare.inputs[0])
- self.offset_previous_nodes(section_compare, offset_x=200)
- section_override.nodes.remove(section_compare)
- section_override.nodes.remove(tex_coords)
- bpy.data.objects.remove(context.active_object)
-
+ if self.remove_all:
+ SectionCutawayManager.remove_all_section_planes()
+ else:
+ SectionCutawayManager.remove_section_planes(context.selected_objects)
+ SectionCutawayManager.clean()
return {"FINISHED"}
- def offset_previous_nodes(self, section_compare, offset_x=0, offset_y=0):
- if section_compare.inputs[0].links:
- previous_section_compare = section_compare.inputs[0].links[0].from_node
- previous_section_compare.location += Vector((offset_x, offset_y))
- if previous_section_compare.inputs[1].links:
- previous_section_compare.inputs[1].links[0].from_node.location += Vector((offset_x, offset_y))
- self.offset_previous_nodes(previous_section_compare, offset_x, offset_y)
- def purge_all_section_data(self, context):
- bpy.data.materials.remove(bpy.data.materials.get("Section Override"))
- for material in bpy.data.materials:
- if not material.node_tree:
- continue
- override = material.node_tree.nodes.get("Section Override")
- if not override:
- continue
- material.node_tree.links.new(
- override.inputs[0].links[0].from_socket, override.outputs[0].links[0].to_socket
- )
- material.node_tree.nodes.remove(override)
- bpy.data.node_groups.remove(bpy.data.node_groups.get("Section Override"))
- bpy.data.node_groups.remove(bpy.data.node_groups.get("Section Compare"))
- bpy.ops.object.delete({"selected_objects": [context.active_object]})
+def update_visibility(self, context):
+ tree = SectionCutawayManager.get_section_tree()
+ if tree:
+ node = tree.nodes.get("Hide Mix")
+ if node:
+ node.inputs[0].default_value = int(context.scene.SectionProperties.hide)
+
+
+def update_section_color(self, context):
+ section_node_group = SectionCutawayManager.get_section_tree()
+ if section_node_group is None:
+ return
+ try:
+ emission_node = next(n for n in section_node_group.nodes if isinstance(n, bpy.types.ShaderNodeEmission))
+ emission_node.inputs[0].default_value = list(self.section_plane_colour) + [1]
+ except StopIteration:
+ pass
class SectionProperties(PropertyGroup):
should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False)
+ hide: BoolProperty(default=False, name="Toggle", update=update_visibility)
section_plane_colour: FloatVectorProperty(
name="Temporary Section Cutaway Colour",
subtype="COLOR",
@@ -304,7 +363,7 @@ class SectionProperties(PropertyGroup):
)
-class BIM_PT_section_plane(bpy.types.Panel):
+class BIM_PT_section_plane(Panel):
bl_label = "Temporary Section Cutaways"
bl_idname = "BIM_PT_section_plane"
bl_space_type = "VIEW_3D"
@@ -316,30 +375,31 @@ class BIM_PT_section_plane(bpy.types.Panel):
layout.use_property_split = True
props = context.scene.SectionProperties
- row = layout.row()
- row.prop(props, "should_section_selected_objects")
+ layout.prop(props, "should_section_selected_objects", text="Only Selected", icon="RESTRICT_SELECT_OFF")
+ layout.prop(props, "section_plane_colour", text="Section Color")
+ layout.prop(props, "hide", icon="HIDE_ON" if props.hide else "HIDE_OFF")
- row = layout.row()
- row.prop(props, "section_plane_colour")
-
- row = layout.row(align=True)
- row.operator("bim.add_section_plane")
- row.operator("bim.remove_section_plane")
+ layout.operator("bim.section_plane_refresh", text="Refresh", icon="FILE_REFRESH")
+ layout.operator("bim.section_plane_add", text="Add Section Cutaway", icon="ADD")
+ layout.operator("bim.section_plane_remove", text="Remove Selected Sections", icon="REMOVE").remove_all = False
+ layout.operator("bim.section_plane_remove", text="Remove All Sections", icon="TRASH").remove_all = True
def register():
bpy.utils.register_class(SectionProperties)
- bpy.utils.register_class(BIM_OT_add_section_plane)
- bpy.utils.register_class(BIM_OT_remove_section_plane)
+ bpy.utils.register_class(BIM_OT_section_plane_add)
+ bpy.utils.register_class(BIM_OT_section_plane_remove)
bpy.utils.register_class(BIM_PT_section_plane)
- bpy.types.Scene.SectionProperties = bpy.props.PointerProperty(type=SectionProperties)
+ bpy.utils.register_class(BIM_OT_section_plane_refresh)
+ bpy.types.Scene.SectionProperties = PointerProperty(type=SectionProperties)
def unregister():
- bpy.utils.unregister_class(BIM_OT_add_section_plane)
- bpy.utils.unregister_class(BIM_OT_remove_section_plane)
+ bpy.utils.unregister_class(BIM_OT_section_plane_add)
+ bpy.utils.unregister_class(BIM_OT_section_plane_remove)
bpy.utils.unregister_class(BIM_PT_section_plane)
bpy.utils.unregister_class(SectionProperties)
+ bpy.utils.unregister_class(BIM_OT_section_plane_refresh)
del bpy.types.Scene.SectionProperties
diff --git a/src/blenderbim/test/bim/feature/georeference.feature b/src/blenderbim/test/bim/feature/georeference.feature
new file mode 100644
index 0000000000..85922285d6
--- /dev/null
+++ b/src/blenderbim/test/bim/feature/georeference.feature
@@ -0,0 +1,82 @@
+@georeference
+Feature: Georeference
+
+Scenario: Add georeferencing
+ Given an empty IFC project
+ When I press "bim.add_georeferencing"
+ Then nothing happens
+
+Scenario: Enable editing georeferencing
+ Given an empty IFC project
+ And I press "bim.add_georeferencing"
+ When I press "bim.enable_editing_georeferencing"
+ Then nothing happens
+
+Scenario: Remove georeferencing
+ Given an empty IFC project
+ And I press "bim.add_georeferencing"
+ When I press "bim.remove_georeferencing"
+ Then nothing happens
+
+Scenario: Edit georeferencing
+ Given an empty IFC project
+ And I press "bim.add_georeferencing"
+ And I press "bim.enable_editing_georeferencing"
+ When I press "bim.edit_georeferencing"
+ Then nothing happens
+
+Scenario: Disable editing georeferencing
+ Given an empty IFC project
+ And I press "bim.add_georeferencing"
+ And I press "bim.enable_editing_georeferencing"
+ When I press "bim.disable_editing_georeferencing"
+ Then nothing happens
+
+Scenario: Set IFC grid north
+ Given an empty IFC project
+ And I press "bim.add_georeferencing"
+ And I press "bim.enable_editing_georeferencing"
+ When I press "bim.set_ifc_grid_north"
+ Then nothing happens
+
+Scenario: Set Blender grid north
+ Given an empty IFC project
+ And I press "bim.add_georeferencing"
+ And I press "bim.enable_editing_georeferencing"
+ And I press "bim.set_ifc_grid_north"
+ When I press "bim.set_blender_grid_north"
+ Then nothing happens
+
+Scenario: Get cursor location
+ Given an empty IFC project
+ When I press "bim.get_cursor_location"
+ Then nothing happens
+
+Scenario: Set cursor location
+ Given an empty IFC project
+ When I set "scene.BIMGeoreferenceProperties.coordinate_output" to "0,0,0"
+ And I press "bim.set_cursor_location"
+ Then nothing happens
+
+Scenario: Set IFC true north
+ Given an empty IFC project
+ When I press "bim.set_ifc_true_north"
+ Then nothing happens
+
+Scenario: Set Blender true north
+ Given an empty IFC project
+ And I press "bim.set_ifc_true_north"
+ When I press "bim.set_blender_true_north"
+ Then nothing happens
+
+Scenario: Convert local to global
+ Given an empty IFC project
+ When I set "scene.BIMGeoreferenceProperties.coordinate_input" to "0,0,0"
+ And I press "bim.convert_local_to_global"
+ Then "scene.BIMGeoreferenceProperties.coordinate_output" is "0.0,0.0,0.0"
+
+Scenario: Convert global to local
+ Given an empty IFC project
+ When I set "scene.BIMGeoreferenceProperties.coordinate_input" to "0,0,0"
+ And I press "bim.convert_global_to_local"
+ Then "scene.BIMGeoreferenceProperties.coordinate_output" is "0.0,0.0,0.0"
diff --git a/src/blenderbim/test/bim/feature/material.feature b/src/blenderbim/test/bim/feature/material.feature
index 125369586d..c3d89dbbb1 100644
--- a/src/blenderbim/test/bim/feature/material.feature
+++ b/src/blenderbim/test/bim/feature/material.feature
@@ -118,3 +118,18 @@ Scenario: Select by material
And the variable "material" is "{ifc}.by_type('IfcMaterial')[0].id()"
When I press "bim.select_by_material(material={material})"
Then nothing happens
+
+Scenario: Expand material category
+ Given an empty IFC project
+ And I press "bim.load_materials"
+ And I press "bim.add_material(obj='')"
+ When I press "bim.expand_material_category(category='')"
+ Then nothing happens
+
+Scenario: Contract material category
+ Given an empty IFC project
+ And I press "bim.load_materials"
+ And I press "bim.add_material(obj='')"
+ And I press "bim.expand_material_category(category='')"
+ When I press "bim.contract_material_category(category='')"
+ Then nothing happens
diff --git a/src/blenderbim/test/bim/feature/owner.feature b/src/blenderbim/test/bim/feature/owner.feature
index 3e8f13513f..7852e2d70e 100644
--- a/src/blenderbim/test/bim/feature/owner.feature
+++ b/src/blenderbim/test/bim/feature/owner.feature
@@ -277,3 +277,30 @@ Scenario: Edit actor
And I press "bim.enable_editing_actor(actor={actor})"
When I press "bim.edit_actor"
Then "scene.BIMOwnerProperties.active_actor_id" is "0"
+
+Scenario: Assign actor
+ Given an empty IFC project
+ And I press "bim.add_person"
+ And I press "bim.add_actor"
+ And I add a cube
+ And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
+ And I press "bim.assign_class"
+ And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()"
+ And the object "IfcWall/Cube" is selected
+ When I press "bim.assign_actor(actor={actor})"
+ Then nothing happens
+
+Scenario: Unassign actor
+ Given an empty IFC project
+ And I press "bim.add_person"
+ And I press "bim.add_actor"
+ And I add a cube
+ And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
+ And I press "bim.assign_class"
+ And the variable "actor" is "{ifc}.by_type('IfcActor')[0].id()"
+ And the object "IfcWall/Cube" is selected
+ And I press "bim.assign_actor(actor={actor})"
+ When I press "bim.unassign_actor(actor={actor})"
+ Then nothing happens
diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature
index 189d244bd1..1fbddce925 100644
--- a/src/blenderbim/test/bim/feature/project.feature
+++ b/src/blenderbim/test/bim/feature/project.feature
@@ -269,16 +269,27 @@ Scenario: Load project elements - load with the spatial decomposition collection
Scenario: Load project elements - manual offset of object placements
Given an empty Blender session
And I press "bim.load_project(filepath='{cwd}/test/files/manual-geolocation.ifc', is_advanced=True)"
- When I set "scene.BIMProjectProperties.should_offset_model" to "True"
- And I set "scene.BIMProjectProperties.model_offset_coordinates" to "-268388.5, -5774506.0, -21.899999618530273"
+ When I set "scene.BIMProjectProperties.false_origin" to "268388500, 5774506000, 21900"
And I press "bim.load_project_elements"
Then the object "IfcPlate/1780 x 270 PRECAST WALL" is at "0,0,0"
+Scenario: Load project elements - manual offset of cartesian points
+ Given an empty Blender session
+ And I press "bim.load_project(filepath='{cwd}/test/files/manual-geolocation-coords.ifc', is_advanced=True)"
+ When I set "scene.BIMProjectProperties.false_origin" to "1990711,5971553,22700"
+ And I press "bim.load_project_elements"
+ Then the object "IfcBuildingElementProxy/NAME" is at "0,0,0"
+
Scenario: Load project elements - auto offset of object placements
Given an empty Blender session
When I press "bim.load_project(filepath='{cwd}/test/files/auto-geolocation.ifc')"
Then the object "IfcPlate/1780 x 270 PRECAST WALL" is at "0,0,0"
+Scenario: Load project elements - auto offset of cartesian points
+ Given an empty Blender session
+ When I press "bim.load_project(filepath='{cwd}/test/files/manual-geolocation-coords.ifc')"
+ Then the object "IfcBuildingElementProxy/NAME" is at "0,0,0"
+
Scenario: Unload project
Given an empty Blender session
When I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc', is_advanced=True)"
diff --git a/src/blenderbim/test/bim/feature/style.feature b/src/blenderbim/test/bim/feature/style.feature
index 66ad775f2e..161c39d167 100644
--- a/src/blenderbim/test/bim/feature/style.feature
+++ b/src/blenderbim/test/bim/feature/style.feature
@@ -75,3 +75,13 @@ Scenario: Disable editing styles
And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
When I press "bim.disable_editing_style"
Then nothing happens
+
+Scenario: Select by style
+ Given an empty IFC project
+ And I add a cube
+ And I add a material
+ And I press "bim.add_style"
+ And I press "bim.load_styles(style_type='IfcSurfaceStyle')"
+ And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()"
+ When I press "bim.select_by_style(style={style})"
+ Then nothing happens
diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py
index d18f7e815c..686d23ab65 100644
--- a/src/blenderbim/test/core/bootstrap.py
+++ b/src/blenderbim/test/core/bootstrap.py
@@ -98,6 +98,13 @@ def geometry():
prophet.verify()
+@pytest.fixture
+def georeference():
+ prophet = Prophecy(blenderbim.core.tool.Georeference)
+ yield prophet
+ prophet.verify()
+
+
@pytest.fixture
def library():
prophet = Prophecy(blenderbim.core.tool.Library)
diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py
index a18d3fa140..3746379740 100644
--- a/src/blenderbim/test/core/test_drawing.py
+++ b/src/blenderbim/test/core/test_drawing.py
@@ -43,30 +43,30 @@ class TestEditText:
subject.edit_text(ifc, drawing, obj="obj")
-class TestEnableEditingTextProduct:
+class TestEnableEditingAssignedProduct:
def test_run(self, drawing):
- drawing.enable_editing_text_product("obj").should_be_called()
- drawing.import_text_product("obj").should_be_called()
- subject.enable_editing_text_product(drawing, obj="obj")
+ drawing.enable_editing_assigned_product("obj").should_be_called()
+ drawing.import_assigned_product("obj").should_be_called()
+ subject.enable_editing_assigned_product(drawing, obj="obj")
-class TestDisableEditingTextProduct:
+class TestDisableEditingAssignedProduct:
def test_run(self, drawing):
- drawing.disable_editing_text_product("obj").should_be_called()
- subject.disable_editing_text_product(drawing, obj="obj")
+ drawing.disable_editing_assigned_product("obj").should_be_called()
+ subject.disable_editing_assigned_product(drawing, obj="obj")
-class TestEditTextProduct:
+class TestEditAssignedProduct:
def test_run(self, ifc, drawing):
ifc.get_entity("obj").should_be_called().will_return("element")
- drawing.get_text_product("element").should_be_called().will_return("existing_product")
+ drawing.get_assigned_product("element").should_be_called().will_return("existing_product")
ifc.run(
"drawing.unassign_product", relating_product="existing_product", related_object="element"
).should_be_called()
ifc.run("drawing.assign_product", relating_product="product", related_object="element").should_be_called()
drawing.update_text_value("obj").should_be_called()
- drawing.disable_editing_text_product("obj").should_be_called()
- subject.edit_text_product(ifc, drawing, obj="obj", product="product")
+ drawing.disable_editing_assigned_product("obj").should_be_called()
+ subject.edit_assigned_product(ifc, drawing, obj="obj", product="product")
class TestLoadSheets:
@@ -114,18 +114,83 @@ class TestAddSheet:
class TestOpenSheet:
def test_run(self, drawing):
- drawing.get_sheet_filename("sheet").should_be_called().will_return("filename")
- drawing.open_svg("filename").should_be_called()
+ drawing.get_document_uri("sheet").should_be_called().will_return("uri")
+ drawing.open_svg("uri").should_be_called()
subject.open_sheet(drawing, sheet="sheet")
class TestRemoveSheet:
def test_run(self, ifc, drawing):
- ifc.run("document.remove_document", document="sheet").should_be_called()
+ ifc.run("document.remove_information", information="sheet").should_be_called()
drawing.import_sheets().should_be_called()
subject.remove_sheet(ifc, drawing, sheet="sheet")
+class TestLoadSchedules:
+ def test_run(self, drawing):
+ drawing.import_schedules().should_be_called()
+ drawing.enable_editing_schedules().should_be_called()
+ subject.load_schedules(drawing)
+
+
+class TestDisableEditingSchedules:
+ def test_run(self, drawing):
+ drawing.disable_editing_schedules().should_be_called()
+ subject.disable_editing_schedules(drawing)
+
+
+class TestAddSchedule:
+ def test_run(self, ifc, drawing):
+ ifc.run("document.add_information").should_be_called().will_return("schedule")
+ ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference")
+ ifc.get_schema().should_be_called().will_return("IFC4")
+ ifc.run(
+ "document.edit_information",
+ information="schedule",
+ attributes={"Identification": "X", "Name": "UNTITLED", "Scope": "SCHEDULE", "Location": "uri"},
+ ).should_be_called()
+ drawing.import_schedules().should_be_called()
+ subject.add_schedule(ifc, drawing, uri="uri")
+
+ def test_using_a_document_id_in_ifc2x3(self, ifc, drawing):
+ ifc.run("document.add_information").should_be_called().will_return("schedule")
+ ifc.run("document.add_reference", information="schedule").should_be_called().will_return("reference")
+ ifc.get_schema().should_be_called().will_return("IFC2X3")
+ ifc.run(
+ "document.edit_information",
+ information="schedule",
+ attributes={"DocumentId": "X", "Name": "UNTITLED", "Scope": "SCHEDULE"},
+ ).should_be_called()
+ ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called()
+ drawing.import_schedules().should_be_called()
+ subject.add_schedule(ifc, drawing, uri="uri")
+
+
+class TestRemoveSchedule:
+ def test_run(self, ifc, drawing):
+ ifc.run("document.remove_information", information="schedule").should_be_called()
+ drawing.import_schedules().should_be_called()
+ subject.remove_schedule(ifc, drawing, schedule="schedule")
+
+
+class TestOpenSchedule:
+ def test_run(self, drawing):
+ drawing.get_schedule_location("schedule").should_be_called().will_return("uri")
+ drawing.open_spreadsheet("uri").should_be_called()
+ subject.open_schedule(drawing, schedule="schedule")
+
+
+class TestUpdateScheduleName:
+ def test_do_not_update_if_name_unchanged(self, ifc, drawing):
+ drawing.get_name("schedule").should_be_called().will_return("name")
+ subject.update_schedule_name(ifc, drawing, schedule="schedule", name="name")
+
+ def test_run(self, ifc, drawing):
+ drawing.get_name("schedule").should_be_called().will_return("oldname")
+ ifc.run("document.edit_information", information="schedule", attributes={"Name": "name"}).should_be_called()
+ subject.update_schedule_name(ifc, drawing, schedule="schedule", name="name")
+
+
class TestLoadDrawings:
def test_run(self, drawing):
drawing.import_drawings().should_be_called()
@@ -162,7 +227,17 @@ class TestAddDrawing:
collector.assign("obj").should_be_called()
ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset")
ifc.run(
- "pset.edit_pset", pset="pset", properties={"TargetView": "target_view", "Scale": "1/100"}
+ "pset.edit_pset",
+ pset="pset",
+ properties={
+ "TargetView": "target_view",
+ "Scale": "1/100",
+ "HumanScale": "1:100",
+ "HasUnderlay": False,
+ "HasLinework": True,
+ "HasAnnotation": True,
+ "GlobalReferencing": True,
+ },
).should_be_called()
drawing.import_drawings().should_be_called()
subject.add_drawing(ifc, collector, drawing, target_view="target_view", location_hint="location_hint")
@@ -206,7 +281,7 @@ class TestAddAnnotation:
drawing.show_decorations().should_be_called()
drawing.get_drawing_target_view("drawing").should_be_called().will_return("target_view")
drawing.get_annotation_context("target_view").should_be_called().will_return("context")
- drawing.create_annotation_object("object_type").should_be_called().will_return("obj")
+ drawing.create_annotation_object("drawing", "object_type").should_be_called().will_return("obj")
ifc.get_entity("obj").should_be_called().will_return(None)
drawing.get_ifc_representation_class("object_type").should_be_called().will_return("ifc_representation_class")
drawing.run_root_assign_class(
diff --git a/src/blenderbim/test/core/test_geometry.py b/src/blenderbim/test/core/test_geometry.py
index bb3da2f854..a22267cf8f 100644
--- a/src/blenderbim/test/core/test_geometry.py
+++ b/src/blenderbim/test/core/test_geometry.py
@@ -60,8 +60,8 @@ class TestAddRepresentation:
profile_set_usage="profile_set_usage",
).should_be_called().will_return("representation")
- # Styles are relevant for meshes with faces only
- geometry.does_object_have_mesh_with_faces("obj").should_be_called().will_return(True)
+ # Styles are relevant for body representations only (as a simplification)
+ geometry.is_body_representation("representation").should_be_called().will_return(True)
# Add styles
geometry.get_object_materials_without_styles("obj").should_be_called().will_return(["material"])
@@ -102,7 +102,7 @@ class TestAddRepresentation:
== "representation"
)
- def test_not_handling_styles_if_representation_has_no_faces(self, ifc, geometry, style, surveyor):
+ def test_not_handling_styles_if_not_a_body_representation(self, ifc, geometry, style, surveyor):
TestEditObjectPlacement.predict(self, ifc, geometry, surveyor)
# Add representation
@@ -126,8 +126,8 @@ class TestAddRepresentation:
profile_set_usage="profile_set_usage",
).should_be_called().will_return("representation")
- # Styles are relevant for meshes with faces only
- geometry.does_object_have_mesh_with_faces("obj").should_be_called().will_return(False)
+ # Styles are relevant for body representations only (as a simplification)
+ geometry.is_body_representation("representation").should_be_called().will_return(False)
# Assign representation to product
ifc.run("geometry.assign_representation", product="element", representation="representation").should_be_called()
diff --git a/src/blenderbim/test/core/test_georeference.py b/src/blenderbim/test/core/test_georeference.py
new file mode 100644
index 0000000000..957e43b232
--- /dev/null
+++ b/src/blenderbim/test/core/test_georeference.py
@@ -0,0 +1,114 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of BlenderBIM Add-on.
+#
+# BlenderBIM Add-on is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# BlenderBIM Add-on is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with BlenderBIM Add-on. If not, see .
+
+import blenderbim.core.georeference as subject
+from test.core.bootstrap import ifc, georeference
+
+
+class TestAddGeoreferencing:
+ def test_run(self, ifc):
+ ifc.run("georeference.add_georeferencing").should_be_called()
+ subject.add_georeferencing(ifc)
+
+
+class TestEnableGeoreferencing:
+ def test_run(self, georeference):
+ georeference.import_projected_crs().should_be_called()
+ georeference.import_map_conversion().should_be_called()
+ georeference.import_true_north().should_be_called()
+ georeference.enable_editing().should_be_called()
+ subject.enable_editing_georeferencing(georeference)
+
+
+class TestRemoveGeoreferencing:
+ def test_run(self, ifc):
+ ifc.run("georeference.remove_georeferencing").should_be_called()
+ subject.remove_georeferencing(ifc)
+
+
+class TestEditGeoreferencing:
+ def test_run(self, ifc, georeference):
+ georeference.get_projected_crs_attributes().should_be_called().will_return("projected_crs_attributes")
+ georeference.get_map_conversion_attributes().should_be_called().will_return("map_conversion_attributes")
+ georeference.get_true_north_attributes().should_be_called().will_return("true_north_attributes")
+ ifc.run(
+ "georeference.edit_georeferencing",
+ projected_crs="projected_crs_attributes",
+ map_conversion="map_conversion_attributes",
+ true_north="true_north_attributes",
+ ).should_be_called()
+ georeference.disable_editing().should_be_called()
+ subject.edit_georeferencing(ifc, georeference)
+
+
+class TestSetIfcGridNorth:
+ def test_run(self, georeference):
+ georeference.set_ifc_grid_north().should_be_called()
+ subject.set_ifc_grid_north(georeference)
+
+
+class TestSetBlenderGridNorth:
+ def test_run(self, georeference):
+ georeference.set_blender_grid_north().should_be_called()
+ subject.set_blender_grid_north(georeference)
+
+
+class TestSetIfcTrueNorth:
+ def test_run(self, georeference):
+ georeference.set_ifc_true_north().should_be_called()
+ subject.set_ifc_true_north(georeference)
+
+
+class TestSetBlenderTrueNorth:
+ def test_run(self, georeference):
+ georeference.set_blender_true_north().should_be_called()
+ subject.set_blender_true_north(georeference)
+
+
+class TestGetCursorLocation:
+ def test_run(self, georeference):
+ georeference.get_cursor_location().should_be_called().will_return("coordinates")
+ georeference.set_coordinates("input", "coordinates").should_be_called()
+ subject.get_cursor_location(georeference)
+
+
+class TestSetCursorLocation:
+ def test_run(self, georeference):
+ georeference.get_coordinates("output").should_be_called().will_return("coordinates")
+ georeference.set_cursor_location("coordinates").should_be_called()
+ subject.set_cursor_location(georeference)
+
+
+class TestConvertLocalToGlobal:
+ def test_run(self, georeference):
+ georeference.get_coordinates("input").should_be_called().will_return("coordinates")
+ georeference.get_map_conversion().should_be_called().will_return("map_conversion")
+ georeference.xyz2enh("coordinates", "map_conversion").should_be_called().will_return("enh")
+ georeference.set_coordinates("output", "enh").should_be_called()
+ georeference.set_cursor_location("enh").should_be_called()
+ subject.convert_local_to_global(georeference)
+
+
+class TestConvertGlobalToLocal:
+ def test_run(self, georeference):
+ georeference.get_coordinates("input").should_be_called().will_return("coordinates")
+ georeference.get_map_conversion().should_be_called().will_return("map_conversion")
+ georeference.enh2xyz("coordinates", "map_conversion").should_be_called().will_return("xyz")
+ georeference.set_coordinates("output", "xyz").should_be_called()
+ georeference.set_cursor_location("xyz").should_be_called()
+ subject.convert_global_to_local(georeference)
diff --git a/src/blenderbim/test/core/test_owner.py b/src/blenderbim/test/core/test_owner.py
index 91e23281c6..0587e3b8cd 100644
--- a/src/blenderbim/test/core/test_owner.py
+++ b/src/blenderbim/test/core/test_owner.py
@@ -259,3 +259,15 @@ class TestEditActor:
ifc.run("owner.edit_actor", actor="actor", attributes="attributes").should_be_called()
owner.clear_actor().should_be_called()
subject.edit_actor(ifc, owner)
+
+
+class TestAssignActor:
+ def test_run(self, ifc, owner):
+ ifc.run("owner.assign_actor", relating_actor="actor", related_object="element").should_be_called()
+ subject.assign_actor(ifc, actor="actor", element="element")
+
+
+class TestUnassignActor:
+ def test_run(self, ifc, owner):
+ ifc.run("owner.unassign_actor", relating_actor="actor", related_object="element").should_be_called()
+ subject.unassign_actor(ifc, actor="actor", element="element")
diff --git a/src/blenderbim/test/core/test_root.py b/src/blenderbim/test/core/test_root.py
index 79abb8bcf6..1e37e1d926 100644
--- a/src/blenderbim/test/core/test_root.py
+++ b/src/blenderbim/test/core/test_root.py
@@ -28,6 +28,7 @@ class TestCopyClass:
def test_copy_with_new_geometry_derived_from_the_type(self, ifc, collector, root):
ifc.get_entity("obj").should_be_called().will_return("original_element")
+ root.get_object_representation("obj").should_be_called().will_return("representation")
ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element")
ifc.link("element", "obj").should_be_called()
root.get_element_type("element").should_be_called().will_return("type")
@@ -41,12 +42,12 @@ class TestCopyClass:
def test_copy_with_new_geometry_added_afresh_for_speed(self, ifc, collector, geometry, root):
ifc.get_entity("obj").should_be_called().will_return("original_element")
+ root.get_object_representation("obj").should_be_called().will_return("representation")
ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element")
ifc.link("element", "obj").should_be_called()
root.get_element_type("element").should_be_called().will_return("type")
root.does_type_have_representations("type").should_be_called().will_return(False)
- root.get_object_representation("obj").should_be_called().will_return("representation")
root.get_representation_context("representation").should_be_called().will_return("context")
geometry.get_ifc_representation_class("element", "representation").should_be_called().will_return(
"ifc_representation_class"
@@ -64,17 +65,18 @@ class TestCopyClass:
def test_copy_with_no_new_geometry(self, ifc, collector, geometry, root):
ifc.get_entity("obj").should_be_called().will_return("original_element")
+ root.get_object_representation("obj").should_be_called().will_return(None)
ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element")
ifc.link("element", "obj").should_be_called()
root.get_element_type("element").should_be_called().will_return("type")
root.does_type_have_representations("type").should_be_called().will_return(False)
- root.get_object_representation("obj").should_be_called().will_return(None)
collector.assign("obj").should_be_called()
root.is_opening_element("element").should_be_called().will_return(False)
subject.copy_class(ifc, collector, geometry, root, obj="obj")
def test_copied_openings_have_dynamic_voids_added(self, ifc, collector, root):
ifc.get_entity("obj").should_be_called().will_return("original_element")
+ root.get_object_representation("obj").should_be_called().will_return(None)
ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element")
ifc.link("element", "obj").should_be_called()
root.get_element_type("element").should_be_called().will_return("type")
diff --git a/src/blenderbim/test/core/test_system.py b/src/blenderbim/test/core/test_system.py
index 2cf955bf4b..01c4171def 100644
--- a/src/blenderbim/test/core/test_system.py
+++ b/src/blenderbim/test/core/test_system.py
@@ -91,30 +91,73 @@ class TestSelectSystemProducts:
class TestShowPorts:
- def test_run(self, system):
+ def test_run(self, ifc, system):
+ ifc.get_object("element").should_be_called().will_return("obj")
+ ifc.is_moved("obj").should_be_called().will_return(False)
+
system.get_ports("element").should_be_called().will_return(["port"])
- system.load_ports(["port"]).should_be_called()
+ system.load_ports("element", ["port"]).should_be_called()
system.select_elements(["port"]).should_be_called()
- subject.show_ports(system, element="element")
+ subject.show_ports(ifc, system, element="element")
+
+ def test_syncing_locations_if_objects_moved_prior_to_showing_ports(self, ifc, system):
+ ifc.get_object("element").should_be_called().will_return("obj")
+ ifc.is_moved("obj").should_be_called().will_return(True)
+ system.run_geometry_edit_object_placement(obj="obj").should_be_called()
+
+ system.get_ports("element").should_be_called().will_return(["port"])
+ system.load_ports("element", ["port"]).should_be_called()
+ system.select_elements(["port"]).should_be_called()
+ subject.show_ports(ifc, system, element="element")
class TestHidePorts:
- def test_run(self, system):
+ def test_run(self, ifc, system):
+ ifc.get_object("element").should_be_called().will_return("obj")
+ ifc.is_moved("obj").should_be_called().will_return(False)
+
system.get_ports("element").should_be_called().will_return(["port"])
+
+ ifc.get_object("port").should_be_called().will_return("port_obj")
+ ifc.is_moved("port_obj").should_be_called().will_return(True)
+ system.run_geometry_edit_object_placement(obj="port_obj").should_be_called()
+
system.delete_element_objects(["port"]).should_be_called()
- subject.hide_ports(system, element="element")
+ subject.hide_ports(ifc, system, element="element")
+
+
+ def test_syncing_locations_if_objects_moved_prior_to_hiding_ports(self, ifc, system):
+ ifc.get_object("element").should_be_called().will_return("obj")
+ ifc.is_moved("obj").should_be_called().will_return(True)
+ system.run_geometry_edit_object_placement(obj="obj").should_be_called()
+
+ system.get_ports("element").should_be_called().will_return(["port"])
+
+ ifc.get_object("port").should_be_called().will_return("port_obj")
+ ifc.is_moved("port_obj").should_be_called().will_return(True)
+ system.run_geometry_edit_object_placement(obj="port_obj").should_be_called()
+
+ system.delete_element_objects(["port"]).should_be_called()
+ subject.hide_ports(ifc, system, element="element")
class TestAddPort:
def test_run(self, ifc, system):
system.get_ports("element").should_be_called().will_return(["port"])
- system.load_ports(["port"]).should_be_called()
+ system.load_ports("element", ["port"]).should_be_called()
system.create_empty_at_cursor_with_element_orientation("element").should_be_called().will_return("obj")
system.run_root_assign_class(obj="obj", ifc_class="IfcDistributionPort").should_be_called().will_return("port")
ifc.run("system.assign_port", element="element", port="port").should_be_called()
subject.add_port(ifc, system, element="element")
+class TestRemovePort:
+ def test_run(self, ifc, system):
+ system.delete_element_objects(["port"]).should_be_called()
+ ifc.run("root.remove_product", product="port").should_be_called()
+ subject.remove_port(ifc, system, port="port")
+
+
class TestSetFlowDirection:
def test_run(self, ifc, system):
system.get_connected_port("port").should_be_called().will_return("port2")
diff --git a/src/blenderbim/test/core/test_unit.py b/src/blenderbim/test/core/test_unit.py
index 5c7143ceaa..3e77d582a8 100644
--- a/src/blenderbim/test/core/test_unit.py
+++ b/src/blenderbim/test/core/test_unit.py
@@ -142,7 +142,7 @@ class TestDisableEditingUnit:
class TestEditUnit:
def test_editing_monetary_units(self, ifc, unit):
unit.export_unit_attributes().should_be_called().will_return("attributes")
- unit.get_unit_class("unit").should_be_called().will_return("IfcMonetaryUnit")
+ unit.is_unit_class("unit", "IfcMonetaryUnit").should_be_called().will_return(True)
ifc.run("unit.edit_monetary_unit", unit="unit", attributes="attributes").should_be_called()
unit.import_units().should_be_called()
unit.clear_active_unit().should_be_called()
@@ -150,7 +150,8 @@ class TestEditUnit:
def test_editing_derived_units(self, ifc, unit):
unit.export_unit_attributes().should_be_called().will_return("attributes")
- unit.get_unit_class("unit").should_be_called().will_return("IfcDerivedUnit")
+ unit.is_unit_class("unit", "IfcMonetaryUnit").should_be_called().will_return(False)
+ unit.is_unit_class("unit", "IfcDerivedUnit").should_be_called().will_return(True)
ifc.run("unit.edit_derived_unit", unit="unit", attributes="attributes").should_be_called()
unit.import_units().should_be_called()
unit.clear_active_unit().should_be_called()
@@ -158,7 +159,9 @@ class TestEditUnit:
def test_editing_named_units(self, ifc, unit):
unit.export_unit_attributes().should_be_called().will_return("attributes")
- unit.get_unit_class("unit").should_be_called().will_return("IfcNamedUnit")
+ unit.is_unit_class("unit", "IfcMonetaryUnit").should_be_called().will_return(False)
+ unit.is_unit_class("unit", "IfcDerivedUnit").should_be_called().will_return(False)
+ unit.is_unit_class("unit", "IfcNamedUnit").should_be_called().will_return(True)
ifc.run("unit.edit_named_unit", unit="unit", attributes="attributes").should_be_called()
unit.import_units().should_be_called()
unit.clear_active_unit().should_be_called()
diff --git a/src/blenderbim/test/files/manual-geolocation-coords.ifc b/src/blenderbim/test/files/manual-geolocation-coords.ifc
new file mode 100644
index 0000000000..c0d843baf7
--- /dev/null
+++ b/src/blenderbim/test/files/manual-geolocation-coords.ifc
@@ -0,0 +1,122 @@
+ISO-10303-21;
+HEADER;
+FILE_DESCRIPTION ((''), '2;1');
+FILE_NAME ('', '2022-04-06T07:53:12', (''), (''), 'Xbim File Processor version 4.0.0.0', 'Xbim version 4.0.0.0', '');
+FILE_SCHEMA (('IFC2X3'));
+ENDSEC;
+DATA;
+#1=IFCBUILDINGELEMENTPROXY('0WxtKqfTD7uRQVvprDAkUF',#13,'NAME','PourObject','PourObject',#14,#26,'ID20ef7534-a5d3-47e1-b69f-e73d4d2ae78f',.ELEMENT.);
+#2=IFCOWNERHISTORY(#5,#6,$,.ADDED.,$,$,$,0);
+#3=IFCPERSON($,'Davies','Tim',$,$,$,$,$);
+#4=IFCORGANIZATION($,'ORG1',$,$,$);
+#5=IFCPERSONANDORGANIZATION(#3,#4,$);
+#7=IFCORGANIZATION($,'ORG2',$,$,$);
+#6=IFCAPPLICATION(#7,'1.1','IfcTool','1234567890');
+#8=IFCOWNERHISTORY(#9,#12,$,.NOCHANGE.,$,$,$,1629180000);
+#9=IFCPERSONANDORGANIZATION(#10,#11,$);
+#10=IFCPERSON('PERSON_NAME','Undefined',$,$,$,$,$,$);
+#11=IFCORGANIZATION($,'Trimble Solutions Corporation',$,$,$);
+#12=IFCAPPLICATION(#11,'VERSION','APPLICATION','Multi material modeling');
+#13=IFCOWNERHISTORY(#5,#6,$,.MODIFIED.,$,$,$,0);
+#14=IFCLOCALPLACEMENT(#15,#24);
+#15=IFCLOCALPLACEMENT(#16,#23);
+#16=IFCLOCALPLACEMENT(#17,#22);
+#17=IFCLOCALPLACEMENT($,#18);
+#18=IFCAXIS2PLACEMENT3D(#19,#20,#21);
+#19=IFCCARTESIANPOINT((0.,0.,0.));
+#20=IFCDIRECTION((0.,0.,1.));
+#21=IFCDIRECTION((1.,0.,0.));
+#22=IFCAXIS2PLACEMENT3D(#19,#20,#21);
+#23=IFCAXIS2PLACEMENT3D(#19,#20,#21);
+#24=IFCAXIS2PLACEMENT3D(#19,#25,#21);
+#25=IFCDIRECTION((0.,1.,0.));
+#26=IFCPRODUCTDEFINITIONSHAPE($,$,(#27));
+#27=IFCSHAPEREPRESENTATION(#28,'Body','Brep',(#32));
+#28=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#29,$,.MODEL_VIEW.,$);
+#29=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#30,#31);
+#30=IFCAXIS2PLACEMENT3D(#19,#20,#21);
+#31=IFCDIRECTION((0.,1.));
+#32=IFCFACETEDBREP(#33);
+#33=IFCCLOSEDSHELL((#34,#41,#46,#51,#54,#57));
+#34=IFCFACE((#35));
+#35=IFCFACEOUTERBOUND(#36,.T.);
+#36=IFCPOLYLOOP((#37,#38,#39,#40));
+#37=IFCCARTESIANPOINT((1990711.,-22699.9999999999,5971553.));
+#38=IFCCARTESIANPOINT((1990711.,-24899.9999999999,5971553.));
+#39=IFCCARTESIANPOINT((1989711.,-24899.9999999999,5971553.));
+#40=IFCCARTESIANPOINT((1989711.,-22699.9999999999,5971553.));
+#41=IFCFACE((#42));
+#42=IFCFACEOUTERBOUND(#43,.T.);
+#43=IFCPOLYLOOP((#38,#44,#45,#39));
+#44=IFCCARTESIANPOINT((1990711.,-24899.9999999999,5972053.));
+#45=IFCCARTESIANPOINT((1989711.,-24899.9999999999,5972053.));
+#46=IFCFACE((#47));
+#47=IFCFACEOUTERBOUND(#48,.T.);
+#48=IFCPOLYLOOP((#44,#49,#50,#45));
+#49=IFCCARTESIANPOINT((1990711.,-22699.9999999999,5972053.));
+#50=IFCCARTESIANPOINT((1989711.,-22699.9999999999,5972053.));
+#51=IFCFACE((#52));
+#52=IFCFACEOUTERBOUND(#53,.T.);
+#53=IFCPOLYLOOP((#49,#37,#40,#50));
+#54=IFCFACE((#55));
+#55=IFCFACEOUTERBOUND(#56,.T.);
+#56=IFCPOLYLOOP((#45,#50,#40,#39));
+#57=IFCFACE((#58));
+#58=IFCFACEOUTERBOUND(#59,.T.);
+#59=IFCPOLYLOOP((#49,#44,#38,#37));
+#60=IFCSTYLEDITEM(#32,(#61),$);
+#61=IFCPRESENTATIONSTYLEASSIGNMENT((#62));
+#62=IFCSURFACESTYLE($,.BOTH.,(#63));
+#63=IFCSURFACESTYLERENDERING(#64,0.,$,$,$,$,$,$,.NOTDEFINED.);
+#64=IFCCOLOURRGB($,0.333333333333333,1.,0.333333333333333);
+#65=IFCRELAGGREGATES('3dUdx5Kov6leXMbv_Evj3F',#13,$,$,#72,(#74));
+#66=IFCOWNERHISTORY(#67,#70,$,.ADDED.,$,$,$,1648232845);
+#67=IFCPERSONANDORGANIZATION(#68,#69,$);
+#68=IFCPERSON($,'',$,$,$,$,$,$);
+#69=IFCORGANIZATION($,'Simplebim',$,$,$);
+#70=IFCAPPLICATION(#71,'Not Defined','Not Defined','Not Defined');
+#71=IFCORGANIZATION($,'Datacubist',$,$,$);
+#72=IFCBUILDING('1cVfOSwd0DwZRaHgXu8a_f',#13,'Undefined',$,'',#16,$,$,.ELEMENT.,$,$,$);
+#73=IFCOWNERHISTORY(#9,#12,$,.NOCHANGE.,$,$,$,1648135363);
+#74=IFCBUILDINGSTOREY('1PRG1GisebkPvVjTc0dKhA',#13,'Undefined',$,'',#15,$,$,.ELEMENT.,0.);
+#75=IFCRELAGGREGATES('0mzX8SC9TE2x9h5brYnb63',#13,$,$,#76,(#72));
+#76=IFCSITE('2AlxJCEVKnYUYG6VRdFCmy',#13,'SITE_NAME',$,'',#17,$,$,.ELEMENT.,(0,0,0),(0,0,0),$,$,$);
+#77=IFCRELAGGREGATES('10$gKGL8H5nRoXtsiwnv6O',#13,$,$,#78,(#76));
+#78=IFCPROJECT('2tm8iIrI_$r8NRzOY_fHvI',#13,'PROJECT',$,$,$,$,(#29,#79),#81);
+#79=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',3,1.E-05,#80,$);
+#80=IFCAXIS2PLACEMENT3D(#19,#20,#21);
+#81=IFCUNITASSIGNMENT((#82,#83,#84,#85,#86,#87,#88,#89,#90,#91,#92,#93,#94,#98,#101,#106));
+#82=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
+#83=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
+#84=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
+#85=IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
+#86=IFCSIUNIT(*,.POWERUNIT.,$,.WATT.);
+#87=IFCSIUNIT(*,.PRESSUREUNIT.,.KILO.,.PASCAL.);
+#88=IFCSIUNIT(*,.FORCEUNIT.,.KILO.,.NEWTON.);
+#89=IFCSIUNIT(*,.ELECTRICVOLTAGEUNIT.,$,.VOLT.);
+#90=IFCSIUNIT(*,.ELECTRICCURRENTUNIT.,$,.AMPERE.);
+#91=IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
+#92=IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
+#93=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
+#94=IFCDERIVEDUNIT((#95,#97),.LINEARVELOCITYUNIT.,$);
+#95=IFCDERIVEDUNITELEMENT(#96,1);
+#96=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
+#97=IFCDERIVEDUNITELEMENT(#92,-1);
+#98=IFCDERIVEDUNIT((#99,#100),.VOLUMETRICFLOWRATEUNIT.,$);
+#99=IFCDERIVEDUNITELEMENT(#93,1);
+#100=IFCDERIVEDUNITELEMENT(#92,-1);
+#101=IFCDERIVEDUNIT((#102,#103,#105),.THERMALTRANSMITTANCEUNIT.,$);
+#102=IFCDERIVEDUNITELEMENT(#86,1);
+#103=IFCDERIVEDUNITELEMENT(#104,-1);
+#104=IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.KELVIN.);
+#105=IFCDERIVEDUNITELEMENT(#83,-1);
+#106=IFCDERIVEDUNIT((#107),.SOUNDPRESSUREUNIT.,$);
+#107=IFCDERIVEDUNITELEMENT(#108,1);
+#108=IFCSIUNIT(*,.PRESSUREUNIT.,$,.PASCAL.);
+#109=IFCRELCONTAINEDINSPATIALSTRUCTURE('2et5jeJgD1OBJkFAZevcaR',#13,$,$,(#1),#74);
+#159=IFCRELDEFINESBYTYPE('0l7quQJkD3hv69HoK6sZxD',#13,$,$,(#1),#160);
+#160=IFCBUILDINGELEMENTPROXYTYPE('0wvZ60EKXBihi$_raqzBCm',#13,'PourObject',$,$,$,$,$,$,.NOTDEFINED.);
+#161=IFCRELASSOCIATESMATERIAL('1RfoofYsj288$ePu$MZZC_',#13,$,$,(#1),#162);
+#162=IFCMATERIAL('Undefined/C40/50');
+ENDSEC;
+END-ISO-10303-21;
diff --git a/src/blenderbim/test/tool/test_drawing.py b/src/blenderbim/test/tool/test_drawing.py
index 91b96f46dd..64048c8192 100644
--- a/src/blenderbim/test/tool/test_drawing.py
+++ b/src/blenderbim/test/tool/test_drawing.py
@@ -51,7 +51,7 @@ class TestCreateCamera(NewFile):
class TestCreateSvgSheet(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
- document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR")
+ document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION")
subject.create_svg_sheet(document, "A1")
assert os.path.isfile(os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg"))
@@ -87,6 +87,13 @@ class TestDisableEditingDrawings(NewFile):
assert bpy.context.scene.DocProperties.is_editing_drawings == False
+class TestDisableEditingSchedules(NewFile):
+ def test_run(self):
+ bpy.context.scene.DocProperties.is_editing_schedules = True
+ subject.disable_editing_schedules()
+ assert bpy.context.scene.DocProperties.is_editing_schedules == False
+
+
class TestDisableEditingSheets(NewFile):
def test_run(self):
bpy.context.scene.DocProperties.is_editing_sheets = True
@@ -102,12 +109,12 @@ class TestDisableEditingText(NewFile):
assert obj.BIMTextProperties.is_editing == False
-class TestDisableEditingTextProduct(NewFile):
+class TestDisableEditingAssignedProduct(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
- obj.BIMTextProperties.is_editing_product = True
- subject.disable_editing_text_product(obj)
- assert obj.BIMTextProperties.is_editing_product == False
+ obj.BIMAssignedProductProperties.is_editing_product = True
+ subject.disable_editing_assigned_product(obj)
+ assert obj.BIMAssignedProductProperties.is_editing_product == False
class TestEnableEditing(NewFile):
@@ -125,6 +132,13 @@ class TestEnableEditingDrawings(NewFile):
assert bpy.context.scene.DocProperties.is_editing_drawings == True
+class TestEnableEditingSchedules(NewFile):
+ def test_run(self):
+ bpy.context.scene.DocProperties.is_editing_schedules = False
+ subject.enable_editing_schedules()
+ assert bpy.context.scene.DocProperties.is_editing_schedules == True
+
+
class TestEnableEditingSheets(NewFile):
def test_run(self):
bpy.context.scene.DocProperties.is_editing_sheets = False
@@ -139,11 +153,11 @@ class TestEnableEditingText(NewFile):
assert obj.BIMTextProperties.is_editing == True
-class TestEnableEditingTextProduct(NewFile):
+class TestEnableEditingAssignedProduct(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
- subject.enable_editing_text_product(obj)
- assert obj.BIMTextProperties.is_editing_product == True
+ subject.enable_editing_assigned_product(obj)
+ assert obj.BIMAssignedProductProperties.is_editing_product == True
class TestEnsureUniqueDrawingName(NewFile):
@@ -207,6 +221,26 @@ class TestGetBodyContext(NewFile):
assert subject.get_body_context() == context
+class TestGetDocumentUri(NewFile):
+ def test_get_sheet_uri(self):
+ ifc = ifcopenshell.file()
+ document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION")
+ result = subject.get_document_uri(document)
+ assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg")
+
+ def test_get_schedule_uri(self):
+ ifc = ifcopenshell.file()
+ document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE")
+ result = subject.get_document_uri(document)
+ assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", "X - FOOBAR.svg")
+
+ def test_run_ifc2x3(self):
+ ifc = ifcopenshell.file(schema="IFC2X3")
+ document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="DOCUMENTATION")
+ result = subject.get_document_uri(document)
+ assert result == os.path.join(bpy.context.scene.BIMProperties.data_dir, "sheets", "X - FOOBAR.svg")
+
+
class TestGetDrawingCollection(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
@@ -263,16 +297,21 @@ class TestGetName(NewFile):
assert subject.get_name(ifc.createIfcWall(Name="Foobar")) == "Foobar"
-class TestGetSheetFilename(NewFile):
+class TestGetScheduleLocation(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
- document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="DOCUMENTATION")
- subject.get_sheet_filename(document) == "X - FOOBAR"
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ schedule = ifcopenshell.api.run("document.add_information", ifc)
+ schedule.Location = "uri"
+ reference = ifcopenshell.api.run("document.add_reference", ifc, information=schedule)
+ subject.get_schedule_location(schedule) == "uri"
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
- document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="DOCUMENTATION")
- subject.get_sheet_filename(document) == "X - FOOBAR"
+ tool.Ifc.set(ifc)
+ reference = ifc.createIfcDocumentReference(Location="uri")
+ schedule = ifc.createIfcDocumentInformation(DocumentReferences=[reference])
+ subject.get_schedule_location(schedule) == "uri"
class TestGenerateDrawingMatrix(NewFile):
@@ -384,14 +423,14 @@ class TestGetTextLiteral(NewFile):
assert subject.get_text_literal(obj) == item
-class TestGetTextProduct(NewFile):
+class TestGetAssignedProduct(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
label = ifc.createIfcAnnotation()
ifcopenshell.api.run("drawing.assign_product", ifc, relating_product=wall, related_object=label)
- assert subject.get_text_product(label) == wall
+ assert subject.get_assigned_product(label) == wall
class TestImportDrawings(NewFile):
@@ -408,6 +447,30 @@ class TestImportDrawings(NewFile):
assert props.drawings[0].target_view == "PLAN_VIEW"
+class TestImportSchedules(NewFile):
+ def test_run(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ")
+ document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE")
+ subject.import_schedules()
+ props = bpy.context.scene.DocProperties
+ assert props.schedules[0].ifc_definition_id == document.id()
+ assert props.schedules[0].identification == "X"
+ assert props.schedules[0].name == "FOOBAR"
+
+ def test_run_ifc2x3(self):
+ ifc = ifcopenshell.file(schema="IFC2X3")
+ tool.Ifc.set(ifc)
+ ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ")
+ document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SCHEDULE")
+ subject.import_schedules()
+ props = bpy.context.scene.DocProperties
+ assert props.schedules[0].ifc_definition_id == document.id()
+ assert props.schedules[0].identification == "X"
+ assert props.schedules[0].name == "FOOBAR"
+
+
class TestImportSheets(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
@@ -451,7 +514,7 @@ class TestImportTextAttributes(NewFile):
assert props.attributes.get("BoxAlignment").string_value == "BoxAlignment"
-class TestImportTextProduct(NewFile):
+class TestImportAssignedProduct(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
@@ -462,8 +525,8 @@ class TestImportTextProduct(NewFile):
label_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(wall, wall_obj)
tool.Ifc.link(label, label_obj)
- subject.import_text_product(label_obj)
- assert label_obj.BIMTextProperties.relating_product == wall_obj
+ subject.import_assigned_product(label_obj)
+ assert label_obj.BIMAssignedProductProperties.relating_product == wall_obj
def test_doing_nothing_if_no_product_to_import(self):
ifc = ifcopenshell.file()
@@ -471,8 +534,13 @@ class TestImportTextProduct(NewFile):
label = ifc.createIfcAnnotation()
label_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(label, label_obj)
- subject.import_text_product(label_obj)
- assert label_obj.BIMTextProperties.relating_product is None
+ subject.import_assigned_product(label_obj)
+ assert label_obj.BIMAssignedProductProperties.relating_product is None
+
+
+class TestOpenSchedule(NewFile):
+ def open_spreadsheet(self):
+ pass
class TestOpenSvg(NewFile):
diff --git a/src/blenderbim/test/tool/test_geometry.py b/src/blenderbim/test/tool/test_geometry.py
index 43bb06c419..b6bd2ce162 100644
--- a/src/blenderbim/test/tool/test_geometry.py
+++ b/src/blenderbim/test/tool/test_geometry.py
@@ -102,25 +102,6 @@ class TestDeleteData(NewFile):
assert not bpy.data.meshes.get("Mesh")
-class TestDoesObjectHaveMeshWithFaces(NewFile):
- def test_empties_return_false(self):
- obj = bpy.data.objects.new("Object", None)
- assert subject.does_object_have_mesh_with_faces(obj) is False
-
- def test_non_meshes_return_false(self):
- obj = bpy.data.objects.new("Object", bpy.data.cameras.new("Curve"))
- assert subject.does_object_have_mesh_with_faces(obj) is False
-
- def test_meshes_without_faces_return_false(self):
- obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
- assert subject.does_object_have_mesh_with_faces(obj) is False
-
- def test_meshes_with_faces_return_true(self):
- bpy.ops.mesh.primitive_cube_add()
- obj = bpy.data.objects.get("Cube")
- assert subject.does_object_have_mesh_with_faces(obj) is True
-
-
class TestDoesRepresentationIdExist(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
diff --git a/src/blenderbim/test/tool/test_georeference.py b/src/blenderbim/test/tool/test_georeference.py
new file mode 100644
index 0000000000..4dd8661e25
--- /dev/null
+++ b/src/blenderbim/test/tool/test_georeference.py
@@ -0,0 +1,381 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2021 Dion Moult
+#
+# This file is part of BlenderBIM Add-on.
+#
+# BlenderBIM Add-on is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# BlenderBIM Add-on is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with BlenderBIM Add-on. If not, see .
+
+import bpy
+import math
+import ifcopenshell
+import blenderbim.core.tool
+import blenderbim.tool as tool
+from mathutils import Vector
+from test.bim.bootstrap import NewFile
+from blenderbim.tool.georeference import Georeference as subject
+
+
+class TestImplementsTool(NewFile):
+ def test_run(self):
+ assert isinstance(subject(), blenderbim.core.tool.Georeference)
+
+
+class TestImportProjectedCRS(NewFile):
+ def test_importing_nothing_with_no_georeferencing(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ subject.import_projected_crs()
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ assert len(props.projected_crs) == 0
+
+ def test_importing_projected_crs(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ ifcopenshell.api.run("georeference.add_georeferencing", ifc)
+ projected_crs = ifc.by_type("IfcProjectedCRS")[0]
+ projected_crs.Name = "Name"
+ projected_crs.Description = "Description"
+ projected_crs.GeodeticDatum = "GeodeticDatum"
+ projected_crs.VerticalDatum = "VerticalDatum"
+ projected_crs.MapProjection = "MapProjection"
+ projected_crs.MapZone = "MapZone"
+ unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", name="METRE")
+ projected_crs.MapUnit = unit
+ subject.import_projected_crs()
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ assert props.projected_crs.get("Name").string_value == "Name"
+ assert props.projected_crs.get("Description").string_value == "Description"
+ assert props.projected_crs.get("GeodeticDatum").string_value == "GeodeticDatum"
+ assert props.projected_crs.get("VerticalDatum").string_value == "VerticalDatum"
+ assert props.projected_crs.get("MapProjection").string_value == "MapProjection"
+ assert props.projected_crs.get("MapZone").string_value == "MapZone"
+ assert props.projected_crs.get("MapUnit").enum_value == str(unit.id())
+
+ def test_run_ifc2x3(self):
+ ifc = ifcopenshell.file(schema="IFC2X3")
+ tool.Ifc.set(ifc)
+ subject.import_projected_crs()
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ assert len(props.projected_crs) == 0
+
+
+class TestImportMapConversion(NewFile):
+ def test_importing_nothing_with_no_georeferencing(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ subject.import_map_conversion()
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ assert len(props.map_conversion) == 0
+
+ def test_importing_map_conversion(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ ifcopenshell.api.run("georeference.add_georeferencing", ifc)
+ map_conversion = ifc.by_type("IfcMapConversion")[0]
+ map_conversion.Eastings = 1
+ map_conversion.Northings = 2
+ map_conversion.OrthogonalHeight = 3
+ map_conversion.XAxisAbscissa = 4
+ map_conversion.XAxisOrdinate = 5
+ map_conversion.Scale = 6
+ subject.import_map_conversion()
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ assert props.map_conversion.get("Eastings").string_value == "1.0"
+ assert props.map_conversion.get("Northings").string_value == "2.0"
+ assert props.map_conversion.get("OrthogonalHeight").string_value == "3.0"
+ assert props.map_conversion.get("XAxisAbscissa").string_value == "4.0"
+ assert props.map_conversion.get("XAxisOrdinate").string_value == "5.0"
+ assert props.map_conversion.get("Scale").string_value == "6.0"
+
+ def test_run_ifc2x3(self):
+ ifc = ifcopenshell.file(schema="IFC2X3")
+ tool.Ifc.set(ifc)
+ subject.import_map_conversion()
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ assert len(props.map_conversion) == 0
+
+
+class TestImportTrueNorth(NewFile):
+ def test_detecting_no_true_north(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ subject.import_true_north()
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ assert props.has_true_north is False
+
+ def test_run(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ context.TrueNorth = ifc.createIfcDirection((1.0, 2.0, 0.0))
+ subject.import_true_north()
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ assert props.true_north_abscissa == "1.0"
+ assert props.true_north_ordinate == "2.0"
+ assert props.has_true_north is True
+
+
+class TestGetProjectedCRSAttributes(NewFile):
+ def test_run(self):
+ TestImportProjectedCRS().test_importing_projected_crs()
+ assert subject.get_projected_crs_attributes() == {
+ "Name": "Name",
+ "Description": "Description",
+ "GeodeticDatum": "GeodeticDatum",
+ "VerticalDatum": "VerticalDatum",
+ "MapProjection": "MapProjection",
+ "MapZone": "MapZone",
+ "MapUnit": tool.Ifc.get().by_type("IfcNamedUnit")[0],
+ }
+
+
+class TestGetMapConversionAttributes(NewFile):
+ def test_run(self):
+ TestImportMapConversion().test_importing_map_conversion()
+ assert subject.get_map_conversion_attributes() == {
+ "Eastings": 1.0,
+ "Northings": 2.0,
+ "OrthogonalHeight": 3.0,
+ "XAxisAbscissa": 4.0,
+ "XAxisOrdinate": 5.0,
+ "Scale": 6.0,
+ }
+
+class TestGetTrueNorthAttributes(NewFile):
+ def test_run(self):
+ TestImportTrueNorth().test_run()
+ assert subject.get_true_north_attributes() == [1.0, 2.0]
+
+
+class TestEnableEditing(NewFile):
+ def test_run(self):
+ bpy.context.scene.BIMGeoreferenceProperties.is_editing = False
+ subject.enable_editing()
+ assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is True
+
+
+class TestDisableEditing(NewFile):
+ def test_run(self):
+ bpy.context.scene.BIMGeoreferenceProperties.is_editing = True
+ subject.disable_editing()
+ assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is False
+
+
+class TestSetCoordinates(NewFile):
+ def test_run(self):
+ subject.set_coordinates("input", [1., 2., 3.])
+ assert bpy.context.scene.BIMGeoreferenceProperties.coordinate_input == "1.0,2.0,3.0"
+ subject.set_coordinates("output", [4., 5., 6.])
+ assert bpy.context.scene.BIMGeoreferenceProperties.coordinate_output == "4.0,5.0,6.0"
+
+
+class TestGetCoordinates(NewFile):
+ def test_run(self):
+ bpy.context.scene.BIMGeoreferenceProperties.coordinate_input = "1.0,2.0,3.0"
+ assert subject.get_coordinates("input") == [1., 2., 3.]
+ bpy.context.scene.BIMGeoreferenceProperties.coordinate_output = "4.0,5.0,6.0"
+ assert subject.get_coordinates("output") == [4., 5., 6.]
+
+
+class TestGetCursorLocation(NewFile):
+ def test_run(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", prefix="MILLI", name="METRE")
+ ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit])
+ bpy.context.scene.cursor.location = (1., 2., 3.)
+ assert subject.get_cursor_location() == [1000., 2000., 3000.]
+
+
+class TestSetCursorLocation(NewFile):
+ def test_run(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", prefix="MILLI", name="METRE")
+ ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit])
+ subject.set_cursor_location([1000., 2000., 3000.])
+ assert list(bpy.context.scene.cursor.location) == [1., 2., 3.]
+
+
+class TestSetIfcTrueNorth(NewFile):
+ def test_run(self):
+ subject.set_ifc_true_north()
+ assert round(float(bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa), 3) == 0.0
+ assert round(float(bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate), 3) == 1.0
+ bpy.context.scene.sun_pos_properties.north_offset = math.radians(45)
+ subject.set_ifc_true_north()
+ assert round(float(bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa), 3) == 0.707
+ assert round(float(bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate), 3) == 0.707
+
+
+class TestSetBlenderTrueNorth(NewFile):
+ def test_run(self):
+ bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = "0.0"
+ bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = "1.0"
+ subject.set_blender_true_north()
+ assert bpy.context.scene.sun_pos_properties.north_offset == 0
+ bpy.context.scene.BIMGeoreferenceProperties.true_north_abscissa = "0.707"
+ bpy.context.scene.BIMGeoreferenceProperties.true_north_ordinate = "0.707"
+ subject.set_blender_true_north()
+ assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == 45
+
+
+class TestGetMapConversion(NewFile):
+ def test_run(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ ifcopenshell.api.run("georeference.add_georeferencing", ifc)
+ assert subject.get_map_conversion().is_a("IfcMapConversion")
+
+
+class TestXyz2Enh(NewFile):
+ def test_run(self):
+ assert subject.xyz2enh([0., 0., 0.], None) == [0., 0., 0.]
+
+ def test_using_the_blender_offset(self):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ props.has_blender_offset = True
+ props.blender_eastings = "1.0"
+ assert subject.xyz2enh([0., 0., 0.], None) == (1., 0., 0.)
+
+ def test_using_the_map_conversion(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ ifcopenshell.api.run("georeference.add_georeferencing", ifc)
+ map_conversion = ifc.by_type("IfcMapConversion")[0]
+ map_conversion.Eastings = 1.0
+ assert subject.xyz2enh([0., 0., 0.], map_conversion) == (1., 0., 0.)
+
+ def test_applying_both_blender_offset_and_map_conversion(self):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ props.has_blender_offset = True
+ props.blender_eastings = "1.0"
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ ifcopenshell.api.run("georeference.add_georeferencing", ifc)
+ map_conversion = ifc.by_type("IfcMapConversion")[0]
+ map_conversion.Northings = 1.0
+ assert subject.xyz2enh([0., 0., 0.], map_conversion) == (1., 1., 0.)
+
+
+class TestEnh2Xyz(NewFile):
+ def test_run(self):
+ assert subject.enh2xyz([0., 0., 0.], None) == [0., 0., 0.]
+
+ def test_using_the_blender_offset(self):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ props.has_blender_offset = True
+ props.blender_eastings = "1.0"
+ assert subject.enh2xyz([0., 0., 0.], None) == (-1., 0., 0.)
+
+ def test_using_the_map_conversion(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ ifcopenshell.api.run("georeference.add_georeferencing", ifc)
+ map_conversion = ifc.by_type("IfcMapConversion")[0]
+ map_conversion.Eastings = 1.0
+ assert subject.enh2xyz([0., 0., 0.], map_conversion) == (-1., 0., 0.)
+
+ def test_applying_both_blender_offset_and_map_conversion(self):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ props.has_blender_offset = True
+ props.blender_eastings = "1.0"
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
+ ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
+ ifcopenshell.api.run("georeference.add_georeferencing", ifc)
+ map_conversion = ifc.by_type("IfcMapConversion")[0]
+ map_conversion.Northings = 1.0
+ assert subject.enh2xyz([0., 0., 0.], map_conversion) == (-1., -1., 0.)
+
+
+class TestSetIfcGridNorth(NewFile):
+ def test_run(self):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ new = props.map_conversion.add()
+ new.name = "XAxisAbscissa"
+ new = props.map_conversion.add()
+ new.name = "XAxisOrdinate"
+ subject.set_ifc_grid_north()
+ assert round(float(props.map_conversion.get("XAxisAbscissa").string_value), 3) == 1.0
+ assert round(float(props.map_conversion.get("XAxisOrdinate").string_value), 3) == 0.0
+ bpy.context.scene.sun_pos_properties.north_offset = math.radians(-45)
+ subject.set_ifc_grid_north()
+ assert round(float(props.map_conversion.get("XAxisAbscissa").string_value), 3) == 0.707
+ assert round(float(props.map_conversion.get("XAxisOrdinate").string_value), 3) == -0.707
+ bpy.context.scene.sun_pos_properties.north_offset = math.radians(45)
+ subject.set_ifc_grid_north()
+ assert round(float(props.map_conversion.get("XAxisAbscissa").string_value), 3) == 0.707
+ assert round(float(props.map_conversion.get("XAxisOrdinate").string_value), 3) == 0.707
+ bpy.context.scene.sun_pos_properties.north_offset = math.radians(135)
+ subject.set_ifc_grid_north()
+ assert round(float(props.map_conversion.get("XAxisAbscissa").string_value), 3) == -0.707
+ assert round(float(props.map_conversion.get("XAxisOrdinate").string_value), 3) == 0.707
+ bpy.context.scene.sun_pos_properties.north_offset = math.radians(-135)
+ subject.set_ifc_grid_north()
+ assert round(float(props.map_conversion.get("XAxisAbscissa").string_value), 3) == -0.707
+ assert round(float(props.map_conversion.get("XAxisOrdinate").string_value), 3) == -0.707
+
+
+class TestSetBlenderGridNorth(NewFile):
+ def test_run(self):
+ props = bpy.context.scene.BIMGeoreferenceProperties
+ new = props.map_conversion.add()
+ new.name = "XAxisAbscissa"
+ new = props.map_conversion.add()
+ new.name = "XAxisOrdinate"
+ props.map_conversion.get("XAxisAbscissa").string_value = "1.0"
+ props.map_conversion.get("XAxisOrdinate").string_value = "0.0"
+ subject.set_blender_grid_north()
+ assert bpy.context.scene.sun_pos_properties.north_offset == 0
+ props.map_conversion.get("XAxisAbscissa").string_value = "0.707"
+ props.map_conversion.get("XAxisOrdinate").string_value = "-0.707"
+ subject.set_blender_grid_north()
+ assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == -45
+ props.map_conversion.get("XAxisAbscissa").string_value = "0.707"
+ props.map_conversion.get("XAxisOrdinate").string_value = "0.707"
+ subject.set_blender_grid_north()
+ assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == 45
+ props.map_conversion.get("XAxisAbscissa").string_value = "-0.707"
+ props.map_conversion.get("XAxisOrdinate").string_value = "0.707"
+ subject.set_blender_grid_north()
+ assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == 135
+ props.map_conversion.get("XAxisAbscissa").string_value = "-0.707"
+ props.map_conversion.get("XAxisOrdinate").string_value = "-0.707"
+ subject.set_blender_grid_north()
+ assert round(math.degrees(bpy.context.scene.sun_pos_properties.north_offset)) == -135
diff --git a/src/blenderbim/test/tool/test_material.py b/src/blenderbim/test/tool/test_material.py
index 4ce5af138f..430acbb04e 100644
--- a/src/blenderbim/test/tool/test_material.py
+++ b/src/blenderbim/test/tool/test_material.py
@@ -84,15 +84,30 @@ class TestGetName(NewFile):
class TestImportMaterialDefinitions(NewFile):
- def test_import_materials(self):
+ def test_import_materials_grouped_by_categories(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
- material = ifc.createIfcMaterial(Name="Name")
+ material = ifc.createIfcMaterial(Name="Name", Category="Category")
subject.import_material_definitions("IfcMaterial")
props = bpy.context.scene.BIMMaterialProperties
- assert props.materials[0].ifc_definition_id == material.id()
- assert props.materials[0].name == "Name"
- assert props.materials[0].total_elements == 0
+ assert props.materials[0].ifc_definition_id == 0
+ assert props.materials[0].name == "Category"
+ assert props.materials[0].is_category is True
+ assert props.materials[0].is_expanded is False
+ assert len(props.materials) == 1
+
+ def test_importing_expanded_material_categories(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ material = ifc.createIfcMaterial(Name="Name", Category="Category")
+ subject.import_material_definitions("IfcMaterial")
+ props = bpy.context.scene.BIMMaterialProperties
+ props.materials[0].is_expanded = True
+ subject.import_material_definitions("IfcMaterial")
+ assert len(props.materials) == 2
+ assert props.materials[1].ifc_definition_id == material.id()
+ assert props.materials[1].name == "Name"
+ assert props.materials[1].total_elements == 0
def test_import_material_layer_sets(self):
ifc = ifcopenshell.file()
diff --git a/src/blenderbim/test/tool/test_style.py b/src/blenderbim/test/tool/test_style.py
index dd39292dc7..53e147f9d9 100644
--- a/src/blenderbim/test/tool/test_style.py
+++ b/src/blenderbim/test/tool/test_style.py
@@ -95,6 +95,20 @@ class TestGetContext(NewFile):
assert subject.get_context("obj") == context
+class TestGetElementsByStyle(NewFile):
+ def test_run(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc.set(ifc)
+ element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
+ style = ifc.createIfcSurfaceStyle()
+ item = ifc.createIfcExtrudedAreaSolid()
+ ifc.createIfcStyledItem(Item=item, Styles=[style])
+ element.Representation = ifc.createIfcProductDefinitionShape(
+ Representations=[ifc.createIfcShapeRepresentation(Items=[item])]
+ )
+ assert subject.get_elements_by_style(style) == {element}
+
+
class TestGetName(NewFile):
def test_run(self):
assert subject.get_name(bpy.data.materials.new("Material")) == "Material"
@@ -426,3 +440,15 @@ class TestIsEditingStyles(NewFile):
subject.is_editing_styles() is False
bpy.context.scene.BIMStylesProperties.is_editing = True
subject.is_editing_styles() is True
+
+
+class TestSelectElements(NewFile):
+ def test_run(self):
+ ifc = ifcopenshell.file()
+ tool.Ifc().set(ifc)
+ element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcPump")
+ obj = bpy.data.objects.new("Object", None)
+ bpy.context.scene.collection.objects.link(obj)
+ tool.Ifc.link(element, obj)
+ subject.select_elements([element])
+ assert obj in bpy.context.selected_objects
diff --git a/src/blenderbim/test/tool/test_system.py b/src/blenderbim/test/tool/test_system.py
index 0e8fc3f021..2d70c6adde 100644
--- a/src/blenderbim/test/tool/test_system.py
+++ b/src/blenderbim/test/tool/test_system.py
@@ -177,15 +177,21 @@ class TestLoadPorts(NewFile):
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
ifcopenshell.api.run("unit.assign_unit", ifc)
tool.Ifc().set(ifc)
+ element = ifc.createIfcChiller()
port = ifc.createIfcDistributionPort()
- subject.load_ports([port])
+ subject.load_ports(element, [port])
obj = tool.Ifc.get_object(port)
assert obj
assert obj.users_collection
assert list(obj.location) == [0, 0, 0]
-class TestRunAssignClassOperator(NewFile):
+class TestRunGeometryEditObjectPlacement(NewFile):
+ def test_nothing(self):
+ pass
+
+
+class TestRunRootAssignClass(NewFile):
def test_nothing(self):
pass
diff --git a/src/blenderbim/test/tool/test_unit.py b/src/blenderbim/test/tool/test_unit.py
index 41250a9d77..626b6d0247 100644
--- a/src/blenderbim/test/tool/test_unit.py
+++ b/src/blenderbim/test/tool/test_unit.py
@@ -154,13 +154,6 @@ class TestGetSINameFromUnitType:
assert subject.get_si_name_from_unit_type(key) == value
-class TestGetUnitClass:
- def test_run(self):
- ifc = ifcopenshell.file()
- assert subject.get_unit_class(ifc.createIfcSIUnit()) == "IfcSIUnit"
- assert subject.get_unit_class(ifc.createIfcNamedUnit()) == "IfcNamedUnit"
-
-
class TestImportUnitAttributes(NewFile):
def test_importing_derived_units(self):
ifc = ifcopenshell.file()
@@ -307,6 +300,13 @@ class TestIsSceneUnitMetric(NewFile):
assert subject.is_scene_unit_metric() is True
+class TestIsUnitClass:
+ def test_run(self):
+ ifc = ifcopenshell.file()
+ assert subject.is_unit_class(ifc.createIfcSIUnit(), "IfcNamedUnit") is True
+ assert subject.is_unit_class(ifc.createIfcSIUnit(), "IfcMonetaryUnit") is False
+
+
class TestSetActiveUnit(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
diff --git a/src/ifc4d/ifc4d/ifc2p6.py b/src/ifc4d/ifc4d/ifc2p6.py
index 8ff8d426a1..eb93fc3116 100644
--- a/src/ifc4d/ifc4d/ifc2p6.py
+++ b/src/ifc4d/ifc4d/ifc2p6.py
@@ -34,6 +34,8 @@ class Ifc2P6:
self.element_map = {}
self.hours_per_day = 8
self.project_tasks = []
+ self.starts = []
+ self.finishes = []
# P6XML does not understand patterns in holiday dates. Instead, it
# expects a list of every day that is a holiday between a date range.
# This allows you to specify the date range where the export should
@@ -51,8 +53,8 @@ class Ifc2P6:
self.schedule = self.file.by_type("IfcWorkSchedule")[0]
- self.create_obs()
- self.create_calendars()
+ # It seems as though OBSes aren't necessary and cause permission issues on P6 enterprise setups
+ # self.create_obs()
self.create_projects()
tree = ET.ElementTree(self.root)
@@ -65,14 +67,23 @@ class Ifc2P6:
ET.SubElement(obs, "Id").text = element.Name or "X"
ET.SubElement(obs, "Name").text = element.LongName or "Unnamed"
- def create_calendars(self):
+ def create_calendars(self, work_schedule):
for calendar in self.file.by_type("IfcWorkCalendar"):
- self.create_calendar(calendar)
+ self.create_calendar(calendar, work_schedule)
- def create_calendar(self, calendar):
- el = ET.SubElement(self.root, "Calendar")
+ def create_calendar(self, calendar, work_schedule):
+ el = ET.SubElement(self.element_map[work_schedule], "Calendar")
ET.SubElement(el, "Name").text = calendar.Name or "Unnamed"
self.link_element(calendar, el)
+ ET.SubElement(el, "BaseCalendarObjectId")
+ ET.SubElement(el, "HoursPerDay").text = "8"
+ ET.SubElement(el, "HoursPerMonth").text = "172"
+ ET.SubElement(el, "HoursPerWeek").text = "40"
+ ET.SubElement(el, "HoursPerYear").text = "2000"
+ ET.SubElement(el, "IsDefault").text = "0"
+ ET.SubElement(el, "IsPersonal").text = "0"
+ ET.SubElement(el, "ProjectObjectId").text = self.id_map[work_schedule]
+ ET.SubElement(el, "Type").text = "Project"
working_week = self.auto_detect_working_week(calendar)
@@ -81,13 +92,17 @@ class Ifc2P6:
work_week = ET.SubElement(el, "StandardWorkWeek")
- for day, time_periods in working_week.items():
+ for day in ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]:
work_hours = ET.SubElement(work_week, "StandardWorkHours")
- day = ET.SubElement(work_hours, "DayOfWeek").text = day
- for time_period in time_periods:
- work_time = ET.SubElement(work_hours, "WorkTime")
- ET.SubElement(work_time, "Start").text = time_period["Start"]
- ET.SubElement(work_time, "Finish").text = time_period["Finish"]
+ ET.SubElement(work_hours, "DayOfWeek").text = day
+ if day in working_week:
+ print(working_week)
+ for time_period in working_week[day]:
+ work_time = ET.SubElement(work_hours, "WorkTime")
+ ET.SubElement(work_time, "Start").text = time_period["Start"]
+ ET.SubElement(work_time, "Finish").text = time_period["Finish"]
+ else:
+ ET.SubElement(work_hours, "WorkTime")
holidays = ET.SubElement(el, "HolidayOrExceptions")
for holiday in self.auto_detect_holidays(calendar):
@@ -110,7 +125,7 @@ class Ifc2P6:
6: "Saturday",
7: "Sunday",
}
- for working_time in calendar.WorkingTimes:
+ for working_time in calendar.WorkingTimes or []:
if not working_time.RecurrencePattern or working_time.RecurrencePattern.RecurrenceType != "WEEKLY":
continue
@@ -152,24 +167,34 @@ class Ifc2P6:
def create_project(self, work_schedule):
project = ET.SubElement(self.root, "Project")
self.link_element(work_schedule, project)
-
- ET.SubElement(project, "ActivityDefaultCalendarObjectId").text = self.id_map[
- self.file.by_type("IfcWorkCalendar")[0]
- ]
+ activity_calendar = ET.SubElement(project, "ActivityDefaultCalendarObjectId")
ET.SubElement(project, "Name").text = work_schedule.Name or "Unnamed"
ET.SubElement(project, "Id").text = work_schedule.Identification or "X"
- ET.SubElement(project, "OBSObjectId").text = self.id_map[self.file.by_type("IfcProject")[0]]
+ data_date = ET.SubElement(project, "DataDate")
+ planned_start_date = ET.SubElement(project, "PlannedStartDate")
+ # ET.SubElement(project, "OBSObjectId").text = self.id_map[self.file.by_type("IfcProject")[0]]
+ # Note: order matters for P6 enterprise imports.
+ # Root elements like Name must come first. Then calendar, WBS, activities, and relationships.
+ # Changing the order will result in import failures (but not for all P6 versions, it seems?)
+ self.create_calendars(work_schedule)
self.project_tasks = []
+ self.activities = []
self.create_wbs(self.get_subtasks(work_schedule), work_schedule)
+ self.create_activities()
self.create_relationships(self.project_tasks, work_schedule)
+ activity_calendar.text = self.id_map[self.file.by_type("IfcWorkCalendar")[0]]
+ start_date = sorted(self.starts)[0]
+ data_date.text = start_date
+ planned_start_date.text = start_date
+
def create_wbs(self, tasks, work_schedule, parent=None):
self.project_tasks.extend(tasks)
for task in tasks:
subtasks = self.get_subtasks(task)
- if not subtasks:
- self.create_activity(task, work_schedule, parent=parent)
+ if not subtasks and task.TaskTime:
+ self.activities.append({"task": task, "work_schedule": work_schedule, "parent": parent})
continue
wbs = ET.SubElement(self.element_map[work_schedule], "WBS")
self.link_element(task, wbs)
@@ -181,36 +206,29 @@ class Ifc2P6:
ET.SubElement(wbs, "ProjectObjectId").text = self.id_map[work_schedule]
self.create_wbs(subtasks, work_schedule, parent=task)
+ def create_activities(self):
+ for activity in self.activities:
+ self.create_activity(activity["task"], activity["work_schedule"], parent=activity["parent"])
+
def create_activity(self, task, work_schedule, parent=None):
- if not parent:
- return
activity = ET.SubElement(self.element_map[work_schedule], "Activity")
self.link_element(task, activity)
ET.SubElement(activity, "Id").text = task.Identification or "X"
ET.SubElement(activity, "Name").text = task.Name or "Unnamed"
if parent:
ET.SubElement(activity, "WBSObjectId").text = self.id_map[parent]
- if task.TaskTime:
- if task.TaskTime.ScheduleDuration:
- duration = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration)
- ET.SubElement(activity, "PlannedDuration").text = str(duration.days * self.hours_per_day)
- ET.SubElement(activity, "RemainingDuration").text = str(duration.days * self.hours_per_day)
- if task.TaskTime.ScheduleStart:
- ET.SubElement(activity, "PlannedStartDate").text = task.TaskTime.ScheduleStart
- ET.SubElement(activity, "StartDate").text = task.TaskTime.ScheduleStart
- if task.TaskTime.ScheduleFinish:
- ET.SubElement(activity, "PlannedFinishDate").text = task.TaskTime.ScheduleFinish
- ET.SubElement(activity, "FinishDate").text = task.TaskTime.ScheduleFinish
- ET.SubElement(activity, "ProjectObjectId").text = self.id_map[work_schedule]
- ET.SubElement(activity, "ActualDuration").text = "0"
- ET.SubElement(activity, "ActualFinishDate")
- ET.SubElement(activity, "ActualStartDate")
- calendar = ifcopenshell.util.sequence.derive_calendar(task)
- ET.SubElement(activity, "CalendarObjectId").text = self.id_map[calendar]
- ET.SubElement(activity, "DurationPercentComplete").text = "0"
- ET.SubElement(activity, "Type").text = "Task Dependent"
- ET.SubElement(activity, "Status").text = "Not Started"
-
+ if task.TaskTime.ScheduleDuration:
+ duration = ifcopenshell.util.date.ifc2datetime(task.TaskTime.ScheduleDuration)
+ ET.SubElement(activity, "PlannedDuration").text = str(duration.days * self.hours_per_day)
+ ET.SubElement(activity, "RemainingDuration").text = str(duration.days * self.hours_per_day)
+ if task.TaskTime.ScheduleStart:
+ ET.SubElement(activity, "PlannedStartDate").text = task.TaskTime.ScheduleStart
+ ET.SubElement(activity, "StartDate").text = task.TaskTime.ScheduleStart
+ self.starts.append(task.TaskTime.ScheduleStart)
+ if task.TaskTime.ScheduleFinish:
+ ET.SubElement(activity, "PlannedFinishDate").text = task.TaskTime.ScheduleFinish
+ ET.SubElement(activity, "FinishDate").text = task.TaskTime.ScheduleFinish
+ self.finishes.append(task.TaskTime.ScheduleFinish)
data_map = {
"RemainingEarlyStartDate": task.TaskTime.EarlyStart,
"RemainingEarlyFinishDate": task.TaskTime.EarlyFinish,
@@ -218,9 +236,19 @@ class Ifc2P6:
"RemainingLateFinishDate": task.TaskTime.LateFinish,
}
for key, value in data_map.items():
- el = ET.SubElement(activity, "RemainingEarlyStartDate")
+ el = ET.SubElement(activity, key)
if value:
el.text = value
+ ET.SubElement(activity, "ProjectObjectId").text = self.id_map[work_schedule]
+ ET.SubElement(activity, "ActualDuration").text = "0"
+ ET.SubElement(activity, "ActualFinishDate")
+ ET.SubElement(activity, "ActualStartDate")
+ calendar = ifcopenshell.util.sequence.derive_calendar(task)
+ if calendar:
+ ET.SubElement(activity, "CalendarObjectId").text = self.id_map[calendar]
+ ET.SubElement(activity, "DurationPercentComplete").text = "0"
+ ET.SubElement(activity, "Type").text = "Task Dependent"
+ ET.SubElement(activity, "Status").text = "Not Started"
def create_relationships(self, tasks, work_schedule):
for task in tasks:
diff --git a/src/ifc4d/ifc4d/msp2ifc.py b/src/ifc4d/ifc4d/msp2ifc.py
index 8a4a7d4587..470bcc435b 100644
--- a/src/ifc4d/ifc4d/msp2ifc.py
+++ b/src/ifc4d/ifc4d/msp2ifc.py
@@ -110,7 +110,9 @@ class MSP2Ifc:
for calendar in project.find("pr:Calendars", self.ns).findall("pr:Calendar", self.ns):
calendar_id = calendar.find("pr:UID", self.ns).text
week_days = []
- for week_day in calendar.find("pr:WeekDays", self.ns).findall("pr:WeekDay", self.ns):
+ week_days_element = calendar.find("pr:WeekDays", self.ns)
+ week_day_elements = week_days_element.findall("pr:WeekDay", self.ns) if week_days_element else []
+ for week_day in week_day_elements:
working_times = []
if week_day.find("pr:WorkingTimes", self.ns):
for working_time in week_day.find("pr:WorkingTimes", self.ns).findall("pr:WorkingTime", self.ns):
diff --git a/src/ifcgeom/IfcAdvancedBrep.cpp b/src/ifcgeom/IfcAdvancedBrep.cpp
new file mode 100644
index 0000000000..a165bc7ee4
--- /dev/null
+++ b/src/ifcgeom/IfcAdvancedBrep.cpp
@@ -0,0 +1,30 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+#ifdef SCHEMA_HAS_IfcAdvancedBrep
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcAdvancedBrep* l, TopoDS_Shape& shape) {
+ return convert(l->Outer(), shape);
+}
+
+#endif
diff --git a/src/ifcgeom/IfcAnnotationFillArea.cpp b/src/ifcgeom/IfcAnnotationFillArea.cpp
new file mode 100644
index 0000000000..1fd33ba896
--- /dev/null
+++ b/src/ifcgeom/IfcAnnotationFillArea.cpp
@@ -0,0 +1,55 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcAnnotationFillArea* l, TopoDS_Shape& face) {
+ TopoDS_Wire outer_boundary;
+ if (!convert_wire(l->OuterBoundary(), outer_boundary)) {
+ return false;
+ }
+
+ assert_closed_wire(outer_boundary);
+
+ BRepBuilderAPI_MakeFace mf(outer_boundary);
+
+ if (l->InnerBoundaries()) {
+ IfcSchema::IfcCurve::list::ptr inner_boundaries = *l->InnerBoundaries();
+
+ for(IfcSchema::IfcCurve::list::it it = inner_boundaries->begin(); it != inner_boundaries->end(); ++it) {
+ TopoDS_Wire hole;
+ if (convert_wire(*it, hole)) {
+ assert_closed_wire(hole);
+ mf.Add(hole);
+ }
+ }
+ }
+
+ ShapeFix_Shape sfs(mf.Face());
+ sfs.Perform();
+ face = sfs.Shape();
+
+ return true;
+}
diff --git a/src/ifcgeom/IfcArbitraryClosedProfileDef.cpp b/src/ifcgeom/IfcArbitraryClosedProfileDef.cpp
new file mode 100644
index 0000000000..fb4955164e
--- /dev/null
+++ b/src/ifcgeom/IfcArbitraryClosedProfileDef.cpp
@@ -0,0 +1,39 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcArbitraryClosedProfileDef* l, TopoDS_Shape& face) {
+ TopoDS_Wire wire;
+ if (!convert_wire(l->OuterCurve(), wire)) {
+ return false;
+ }
+
+ assert_closed_wire(wire);
+
+ TopoDS_Compound f;
+ bool success = convert_wire_to_faces(wire, f);
+ if (success) {
+ face = f;
+ }
+ return success;
+}
diff --git a/src/ifcgeom/IfcArbitraryOpenProfileDef.cpp b/src/ifcgeom/IfcArbitraryOpenProfileDef.cpp
new file mode 100644
index 0000000000..a68ab93085
--- /dev/null
+++ b/src/ifcgeom/IfcArbitraryOpenProfileDef.cpp
@@ -0,0 +1,27 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include "../ifcgeom/IfcGeom.h"
+
+#define _USE_MATH_DEFINES
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcArbitraryOpenProfileDef* l, TopoDS_Wire& result) {
+ return convert_wire(l->Curve(), result);
+}
diff --git a/src/ifcgeom/IfcArbitraryProfileDefWithVoids.cpp b/src/ifcgeom/IfcArbitraryProfileDefWithVoids.cpp
new file mode 100644
index 0000000000..c418edf369
--- /dev/null
+++ b/src/ifcgeom/IfcArbitraryProfileDefWithVoids.cpp
@@ -0,0 +1,53 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcArbitraryProfileDefWithVoids* l, TopoDS_Shape& face) {
+ TopoDS_Wire profile;
+ if (!convert_wire(l->OuterCurve(), profile)) {
+ return false;
+ }
+
+ assert_closed_wire(profile);
+
+ BRepBuilderAPI_MakeFace mf(profile);
+
+ IfcSchema::IfcCurve::list::ptr voids = l->InnerCurves();
+
+ for(IfcSchema::IfcCurve::list::it it = voids->begin(); it != voids->end(); ++it) {
+ TopoDS_Wire hole;
+ if (convert_wire(*it, hole)) {
+ assert_closed_wire(hole);
+ mf.Add(hole);
+ }
+ }
+
+ ShapeFix_Shape sfs(mf.Face());
+ sfs.Perform();
+ face = sfs.Shape();
+
+ return true;
+}
diff --git a/src/ifcgeom/IfcAxis1Placement.cpp b/src/ifcgeom/IfcAxis1Placement.cpp
new file mode 100644
index 0000000000..55973e2de3
--- /dev/null
+++ b/src/ifcgeom/IfcAxis1Placement.cpp
@@ -0,0 +1,41 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcAxis1Placement* l, gp_Ax1& ax) {
+ IN_CACHE(IfcAxis1Placement,l,gp_Ax1,ax)
+ gp_Pnt o;gp_Dir axis = gp_Dir(0,0,1);
+
+ if (!l->Location()->declaration().is("IfcCartesianPoint")) {
+ // only applicable to 4x3 rc2
+ Logger::Error("Not implemented", l->Location());
+ return false;
+ }
+
+ IfcGeom::Kernel::convert((const IfcSchema::IfcCartesianPoint*) l->Location(),o);
+ if ( l->Axis() ) IfcGeom::Kernel::convert(l->Axis(), axis);
+ ax = gp_Ax1(o, axis);
+ CACHE(IfcAxis1Placement,l,ax)
+ return true;
+}
diff --git a/src/ifcgeom/IfcAxis2Placement2D.cpp b/src/ifcgeom/IfcAxis2Placement2D.cpp
new file mode 100644
index 0000000000..fe8a849a59
--- /dev/null
+++ b/src/ifcgeom/IfcAxis2Placement2D.cpp
@@ -0,0 +1,53 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcAxis2Placement2D* l, gp_Trsf2d& trsf) {
+ IN_CACHE(IfcAxis2Placement2D,l,gp_Trsf2d,trsf)
+ gp_Pnt P; gp_Dir V (1,0,0);
+
+ if (!l->Location()->declaration().is("IfcCartesianPoint")) {
+ // only applicable to 4x3 rc2
+ Logger::Error("Not implemented", l->Location());
+ return false;
+ }
+
+ IfcGeom::Kernel::convert((const IfcSchema::IfcCartesianPoint*) l->Location(),P);
+ if (l->RefDirection()) {
+ IfcGeom::Kernel::convert(l->RefDirection(), V);
+ }
+
+ gp_Ax2d axis(gp_Pnt2d(P.X(),P.Y()), gp_Dir2d(V.X(),V.Y()));
+
+ if (!axis_equal(axis, gp_Ax2d(), getValue(GV_PRECISION))) {
+ trsf.SetTransformation(axis, gp_Ax2d());
+ }
+
+ CACHE(IfcAxis2Placement2D,l,trsf)
+ return true;
+}
diff --git a/src/ifcgeom/IfcAxis2Placement3D.cpp b/src/ifcgeom/IfcAxis2Placement3D.cpp
new file mode 100644
index 0000000000..8b41e67cf2
--- /dev/null
+++ b/src/ifcgeom/IfcAxis2Placement3D.cpp
@@ -0,0 +1,75 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcAxis2Placement3D* l, gp_Trsf& trsf) {
+ IN_CACHE(IfcAxis2Placement3D, l, gp_Trsf, trsf)
+
+ gp_Pnt o;
+ gp_Dir axis(0, 0, 1);
+ gp_Dir refDirection;
+
+ if (!l->Location()->declaration().is("IfcCartesianPoint")) {
+ // only applicable to 4x3 rc2
+ Logger::Error("Not implemented", l->Location());
+ return false;
+ }
+
+ IfcGeom::Kernel::convert((const IfcSchema::IfcCartesianPoint*) l->Location(), o);
+ const bool hasAxis = !!l->Axis();
+ const bool hasRef = !!l->RefDirection();
+
+ if (hasAxis != hasRef) {
+ Logger::Warning("Axis and RefDirection should be specified together", l);
+ }
+
+ if (hasAxis) {
+ IfcGeom::Kernel::convert(l->Axis(), axis);
+ }
+
+ if (hasRef) {
+ IfcGeom::Kernel::convert(l->RefDirection(), refDirection);
+ } else {
+ if (!axis.IsParallel(gp::DX(), 1.e-5)) {
+ refDirection = gp::DX();
+ } else {
+ refDirection = gp::DZ();
+ }
+ gp_Vec Xvec = axis.Dot(refDirection) * axis;
+ gp_Vec Xaxis = refDirection.XYZ() - Xvec.XYZ();
+ refDirection = Xaxis;
+ }
+
+ gp_Ax3 ax3(o, axis, refDirection);
+
+ if (!axis_equal(ax3, (gp_Ax3) gp::XOY(), getValue(GV_PRECISION))) {
+ trsf.SetTransformation(ax3, gp::XOY());
+ }
+
+ CACHE(IfcAxis2Placement3D,l,trsf)
+ return true;
+}
diff --git a/src/ifcgeom/IfcBSplineCurveWithKnots.cpp b/src/ifcgeom/IfcBSplineCurveWithKnots.cpp
new file mode 100644
index 0000000000..b6f536606f
--- /dev/null
+++ b/src/ifcgeom/IfcBSplineCurveWithKnots.cpp
@@ -0,0 +1,83 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+#include
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+#ifdef SCHEMA_HAS_IfcBSplineCurveWithKnots
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcBSplineCurveWithKnots* l, Handle(Geom_Curve)& curve) {
+
+ const bool is_rational = l->declaration().is(IfcSchema::IfcRationalBSplineCurveWithKnots::Class());
+
+ const IfcSchema::IfcCartesianPoint::list::ptr cps = l->ControlPointsList();
+ const std::vector mults = l->KnotMultiplicities();
+ const std::vector knots = l->Knots();
+
+ TColgp_Array1OfPnt Poles(0, cps->size() - 1);
+ TColStd_Array1OfReal Weights(0, cps->size() - 1);
+ TColStd_Array1OfReal Knots(0, (int)knots.size() - 1);
+ TColStd_Array1OfInteger Mults(0, (int)mults.size() - 1);
+ Standard_Integer Degree = l->Degree();
+ Standard_Boolean Periodic = false;
+ // @tfk: it appears to be wrong to expect a period curve when the curve is closed, see #586
+ // Standard_Boolean Periodic = l->ClosedCurve();
+
+ int i;
+
+ if (is_rational) {
+ IfcSchema::IfcRationalBSplineCurveWithKnots* rl = (IfcSchema::IfcRationalBSplineCurveWithKnots*)l;
+ std::vector weights = rl->WeightsData();
+
+ i = 0;
+ for (std::vector::const_iterator it = weights.begin(); it != weights.end(); ++it, ++i) {
+ Weights(i) = *it;
+ }
+ }
+
+ i = 0;
+ for (IfcSchema::IfcCartesianPoint::list::it it = cps->begin(); it != cps->end(); ++it, ++i) {
+ gp_Pnt pnt;
+ if (!convert(*it, pnt)) return false;
+ Poles(i) = pnt;
+ }
+
+ i = 0;
+ for (std::vector::const_iterator it = mults.begin(); it != mults.end(); ++it, ++i) {
+ Mults(i) = *it;
+ }
+
+ i = 0;
+ for (std::vector::const_iterator it = knots.begin(); it != knots.end(); ++it, ++i) {
+ Knots(i) = *it;
+ }
+
+ if (is_rational) {
+ curve = new Geom_BSplineCurve(Poles, Weights, Knots, Mults, Degree, Periodic);
+ } else {
+ curve = new Geom_BSplineCurve(Poles, Knots, Mults, Degree, Periodic);
+ }
+ return true;
+}
+#endif
\ No newline at end of file
diff --git a/src/ifcgeom/IfcBSplineSurfaceWithKnots.cpp b/src/ifcgeom/IfcBSplineSurfaceWithKnots.cpp
new file mode 100644
index 0000000000..910e713939
--- /dev/null
+++ b/src/ifcgeom/IfcBSplineSurfaceWithKnots.cpp
@@ -0,0 +1,85 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+#include
+#include
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+#ifdef SCHEMA_HAS_IfcBSplineSurfaceWithKnots
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcBSplineSurfaceWithKnots* l, TopoDS_Shape& face) {
+ boost::shared_ptr< aggregate_of_aggregate_of > cps = l->ControlPointsList();
+ std::vector uknots = l->UKnots();
+ std::vector vknots = l->VKnots();
+ std::vector umults = l->UMultiplicities();
+ std::vector vmults = l->VMultiplicities();
+
+ TColgp_Array2OfPnt Poles (0, (int)cps->size() - 1, 0, (int)(*cps->begin()).size() - 1);
+ TColStd_Array1OfReal UKnots(0, (int)uknots.size() - 1);
+ TColStd_Array1OfReal VKnots(0, (int)vknots.size() - 1);
+ TColStd_Array1OfInteger UMults(0, (int)umults.size() - 1);
+ TColStd_Array1OfInteger VMults(0, (int)vmults.size() - 1);
+ Standard_Integer UDegree = l->UDegree();
+ Standard_Integer VDegree = l->VDegree();
+
+ int i = 0, j;
+ for (aggregate_of_aggregate_of::outer_it it = cps->begin(); it != cps->end(); ++it, ++i) {
+ j = 0;
+ for (aggregate_of_aggregate_of::inner_it jt = (*it).begin(); jt != (*it).end(); ++jt, ++j) {
+ IfcSchema::IfcCartesianPoint* p = *jt;
+ gp_Pnt pnt;
+ if (!convert(p, pnt)) return false;
+ Poles(i, j) = pnt;
+ }
+ }
+ i = 0;
+ for (std::vector::const_iterator it = uknots.begin(); it != uknots.end(); ++it, ++i) {
+ UKnots(i) = *it;
+ }
+ i = 0;
+ for (std::vector::const_iterator it = vknots.begin(); it != vknots.end(); ++it, ++i) {
+ VKnots(i) = *it;
+ }
+ i = 0;
+ for (std::vector::const_iterator it = umults.begin(); it != umults.end(); ++it, ++i) {
+ UMults(i) = *it;
+ }
+ i = 0;
+ for (std::vector::const_iterator it = vmults.begin(); it != vmults.end(); ++it, ++i) {
+ VMults(i) = *it;
+ }
+ Handle_Geom_Surface surf = new Geom_BSplineSurface(Poles, UKnots, VKnots, UMults, VMults, UDegree, VDegree);
+
+#if OCC_VERSION_HEX < 0x60502
+ face = BRepBuilderAPI_MakeFace(surf);
+#else
+ face = BRepBuilderAPI_MakeFace(surf, getValue(GV_PRECISION));
+#endif
+
+ return true;
+}
+
+#endif
diff --git a/src/ifcgeom/IfcBlock.cpp b/src/ifcgeom/IfcBlock.cpp
new file mode 100644
index 0000000000..0007b97729
--- /dev/null
+++ b/src/ifcgeom/IfcBlock.cpp
@@ -0,0 +1,39 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcBlock* l, TopoDS_Shape& shape) {
+ const double dx = l->XLength() * getValue(GV_LENGTH_UNIT);
+ const double dy = l->YLength() * getValue(GV_LENGTH_UNIT);
+ const double dz = l->ZLength() * getValue(GV_LENGTH_UNIT);
+
+ BRepPrimAPI_MakeBox builder(dx, dy, dz);
+ gp_Trsf trsf;
+ IfcGeom::Kernel::convert(l->Position(),trsf);
+
+ // IfcCsgPrimitive3D.Position has unit scale factor
+ shape = builder.Solid().Moved(trsf);
+
+ return true;
+}
diff --git a/src/ifcgeom/IfcBooleanResult.cpp b/src/ifcgeom/IfcBooleanResult.cpp
new file mode 100644
index 0000000000..046c5d647a
--- /dev/null
+++ b/src/ifcgeom/IfcBooleanResult.cpp
@@ -0,0 +1,231 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcBooleanResult* l, TopoDS_Shape& shape) {
+
+ TopoDS_Shape s1;
+ IfcRepresentationShapeItems items1;
+ TopoDS_Wire boundary_wire;
+ IfcSchema::IfcBooleanOperand* operand1 = l->FirstOperand();
+ IfcSchema::IfcBooleanOperand* operand2 = l->SecondOperand();
+ bool has_halfspace_operand = false;
+
+ BOPAlgo_Operation occ_op;
+
+ const IfcSchema::IfcBooleanOperator::Value op = l->Operator();
+ if (op == IfcSchema::IfcBooleanOperator::IfcBooleanOperator_DIFFERENCE) {
+ occ_op = BOPAlgo_CUT;
+ } else if (op == IfcSchema::IfcBooleanOperator::IfcBooleanOperator_INTERSECTION) {
+ occ_op = BOPAlgo_COMMON;
+ } else if (op == IfcSchema::IfcBooleanOperator::IfcBooleanOperator_UNION) {
+ occ_op = BOPAlgo_FUSE;
+ } else {
+ return false;
+ }
+
+ std::vector second_operands;
+ second_operands.push_back(operand2);
+
+ if (occ_op == BOPAlgo_CUT) {
+ int n_half_space_operands = 0;
+ bool process_as_list = true;
+ while (true) {
+ auto res1 = operand1->as();
+ if (res1 && res1->SecondOperand()->as() && ++n_half_space_operands > 8) {
+ // There is something peculiar about many half space subtraction operands that OCCT does not like.
+ // Often these are used to create a semi-curved arch, as is the case in 693. Supplying all these
+ // operands at once apparently leads to too many edge-edge interference checks.
+ process_as_list = false;
+ break;
+ }
+ if (res1) {
+ if (res1->Operator() == op) {
+ operand1 = res1->FirstOperand();
+ second_operands.push_back(res1->SecondOperand());
+ } else {
+ process_as_list = false;
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ if (!process_as_list) {
+ operand1 = l->FirstOperand();
+ second_operands = { operand2 };
+ }
+ }
+
+ if ( shape_type(operand1) == ST_SHAPELIST ) {
+ if (!(convert_shapes(operand1, items1) && flatten_shape_list(items1, s1, true))) {
+ return false;
+ }
+ } else if ( shape_type(operand1) == ST_SHAPE ) {
+ if ( ! convert_shape(operand1, s1) ) {
+ return false;
+ }
+ { TopoDS_Solid temp_solid;
+ s1 = ensure_fit_for_subtraction(s1, temp_solid); }
+ } else {
+ Logger::Message(Logger::LOG_ERROR, "Invalid representation item for boolean operation", operand1);
+ return false;
+ }
+
+ if (getValue(GV_DISABLE_BOOLEAN_RESULT) > 0.0) {
+ shape = s1;
+ return true;
+ }
+
+ const double first_operand_volume = shape_volume(s1);
+ if (first_operand_volume <= ALMOST_ZERO) {
+ Logger::Message(Logger::LOG_WARNING, "Empty solid for:", l->FirstOperand());
+ }
+
+ TopTools_ListOfShape second_operand_shapes;
+
+ for (auto& op2 : second_operands) {
+ TopoDS_Shape s2;
+
+ bool shape2_processed = false;
+
+ bool is_halfspace = op2->declaration().is(IfcSchema::IfcHalfSpaceSolid::Class());
+ bool is_unbounded_halfspace = is_halfspace && !op2->declaration().is(IfcSchema::IfcPolygonalBoundedHalfSpace::Class());
+ has_halfspace_operand |= is_halfspace;
+
+ {
+ if (shape_type(op2) == ST_SHAPELIST) {
+ IfcRepresentationShapeItems items2;
+ shape2_processed = convert_shapes(op2, items2) && flatten_shape_list(items2, s2, true);
+ } else if (shape_type(op2) == ST_SHAPE) {
+ shape2_processed = convert_shape(op2, s2);
+ if (shape2_processed) {
+ TopoDS_Solid temp_solid;
+ s2 = ensure_fit_for_subtraction(s2, temp_solid);
+ }
+ } else {
+ Logger::Message(Logger::LOG_ERROR, "Invalid representation item for boolean operation", op2);
+ }
+ }
+
+ if (is_unbounded_halfspace) {
+ TopoDS_Shape temp;
+ double d;
+ if (fit_halfspace(s1, s2, temp, d)) {
+ if (d < getValue(GV_PRECISION)) {
+ Logger::Message(Logger::LOG_WARNING, "Halfspace subtraction yields unchanged volume:", l);
+ continue;
+ } else {
+ s2 = temp;
+ }
+ }
+ }
+
+ if (!shape2_processed) {
+ Logger::Message(Logger::LOG_ERROR, "Failed to convert SecondOperand:", op2);
+ continue;
+ }
+
+ if (op2->declaration().is(IfcSchema::IfcHalfSpaceSolid::Class())) {
+ const double second_operand_volume = shape_volume(s2);
+ if (second_operand_volume <= ALMOST_ZERO) {
+ Logger::Message(Logger::LOG_WARNING, "Empty solid for:", op2);
+ }
+ }
+
+ second_operand_shapes.Append(s2);
+ }
+
+ /*
+ // TK: A little debugging trick to output both operands for visual inspection
+
+ BRep_Builder builder;
+ TopoDS_Compound compound;
+ builder.MakeCompound(compound);
+ builder.Add(compound, s1);
+ for (const auto& s2 : second_operand_shapes) {
+ builder.Add(compound, s2);
+ }
+ shape = compound;
+ return true;
+ */
+
+#if OCC_VERSION_HEX < 0x60900
+ // @todo: this currently does not compile anymore, do we still need this?
+ bool valid_result = boolean_operation(s1, s2, occ_op, shape);
+#else
+
+ bool valid_result;
+
+ if (s1.ShapeType() == TopAbs_COMPOUND && TopoDS_Iterator(s1).More() && util::is_nested_compound_of_solid(s1)) {
+ TopoDS_Compound C;
+ BRep_Builder B;
+ B.MakeCompound(C);
+ TopoDS_Iterator it(s1);
+ valid_result = true;
+ for (; it.More(); it.Next()) {
+ TopoDS_Shape part;
+ if (boolean_operation(it.Value(), second_operand_shapes, occ_op, part)) {
+ B.Add(C, part);
+ } else {
+ valid_result = false;
+ }
+ }
+ shape = C;
+ } else {
+ valid_result = boolean_operation(s1, second_operand_shapes, occ_op, shape);
+ }
+
+#endif
+
+ if (op == IfcSchema::IfcBooleanOperator::IfcBooleanOperator_DIFFERENCE) {
+ // In case of a subtraction, a check on volume is performed.
+ if (valid_result) {
+ const double volume_after_subtraction = shape_volume(shape);
+ if ( ALMOST_THE_SAME(first_operand_volume,volume_after_subtraction) )
+ Logger::Message(Logger::LOG_WARNING,"Subtraction yields unchanged volume:",l);
+ } else {
+ Logger::Message(Logger::LOG_ERROR,"Failed to process subtraction:",l);
+ shape = s1;
+ }
+ // NB: After issuing error the first operand is returned!
+ return true;
+ } else if (op == IfcSchema::IfcBooleanOperator::IfcBooleanOperator_UNION && !valid_result) {
+ BRep_Builder B;
+ TopoDS_Compound C;
+ B.MakeCompound(C);
+ B.Add(C, s1);
+ TopTools_ListIteratorOfListOfShape it(second_operand_shapes);
+ for (; it.More(); it.Next()) {
+ B.Add(C, it.Value());
+ }
+ Logger::Message(Logger::LOG_ERROR, "Failed to process union, creating compound:", l);
+ shape = C;
+ return true;
+ } else {
+ return valid_result;
+ }
+ return false;
+}
diff --git a/src/ifcgeom/IfcBoundingBox.cpp b/src/ifcgeom/IfcBoundingBox.cpp
new file mode 100644
index 0000000000..5c164f5d91
--- /dev/null
+++ b/src/ifcgeom/IfcBoundingBox.cpp
@@ -0,0 +1,38 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcBoundingBox* l, TopoDS_Shape& shape) {
+ const double dx = l->XDim() * getValue(GV_LENGTH_UNIT);
+ const double dy = l->YDim() * getValue(GV_LENGTH_UNIT);
+ const double dz = l->ZDim() * getValue(GV_LENGTH_UNIT);
+
+ gp_Pnt corner;
+ IfcGeom::Kernel::convert(l->Corner(), corner);
+ BRepPrimAPI_MakeBox builder(corner, dx, dy, dz);
+
+ shape = builder.Solid();
+
+ return true;
+}
diff --git a/src/ifcgeom/IfcCShapeProfileDef.cpp b/src/ifcgeom/IfcCShapeProfileDef.cpp
new file mode 100644
index 0000000000..cab56aae3e
--- /dev/null
+++ b/src/ifcgeom/IfcCShapeProfileDef.cpp
@@ -0,0 +1,56 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCShapeProfileDef* l, TopoDS_Shape& face) {
+ const double y = l->Depth() / 2.0f * getValue(GV_LENGTH_UNIT);
+ const double x = l->Width() / 2.0f * getValue(GV_LENGTH_UNIT);
+ const double d1 = l->WallThickness() * getValue(GV_LENGTH_UNIT);
+ const double d2 = l->Girth() * getValue(GV_LENGTH_UNIT);
+ bool doFillet = !!l->InternalFilletRadius();
+ double f1 = 0;
+ double f2 = 0;
+ if ( doFillet ) {
+ f1 = *l->InternalFilletRadius() * getValue(GV_LENGTH_UNIT);
+ f2 = f1 + d1;
+ }
+
+ if ( x < ALMOST_ZERO || y < ALMOST_ZERO || d1 < ALMOST_ZERO || d2 < ALMOST_ZERO ) {
+ Logger::Message(Logger::LOG_NOTICE,"Skipping zero sized profile:",l);
+ return false;
+ }
+
+ gp_Trsf2d trsf2d;
+ bool has_position = true;
+#ifdef SCHEMA_IfcParameterizedProfileDef_Position_IS_OPTIONAL
+ has_position = l->Position() != nullptr;
+#endif
+ if (has_position) {
+ IfcGeom::Kernel::convert(l->Position(), trsf2d);
+ }
+
+ double coords[24] = {-x,-y,x,-y,x,-y+d2,x-d1,-y+d2,x-d1,-y+d1,-x+d1,-y+d1,-x+d1,y-d1,x-d1,y-d1,x-d1,y-d2,x,y-d2,x,y,-x,y};
+ int fillets[8] = {0,1,4,5,6,7,10,11};
+ double radii[8] = {f2,f2,f1,f1,f1,f1,f2,f2};
+ return profile_helper(12,coords,doFillet ? 8 : 0,fillets,radii,trsf2d,face);
+}
diff --git a/src/ifcgeom/IfcCartesianPoint.cpp b/src/ifcgeom/IfcCartesianPoint.cpp
new file mode 100644
index 0000000000..93a6ae50ba
--- /dev/null
+++ b/src/ifcgeom/IfcCartesianPoint.cpp
@@ -0,0 +1,35 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCartesianPoint* l, gp_Pnt& point) {
+ IN_CACHE(IfcCartesianPoint,l,gp_Pnt,point)
+ std::vector xyz = l->Coordinates();
+ point = gp_Pnt(
+ xyz.size() ? (xyz[0]*getValue(GV_LENGTH_UNIT)) : 0.0f,
+ xyz.size() > 1 ? (xyz[1]*getValue(GV_LENGTH_UNIT)) : 0.0f,
+ xyz.size() > 2 ? (xyz[2]*getValue(GV_LENGTH_UNIT)) : 0.0f
+ );
+ CACHE(IfcCartesianPoint,l,point)
+ return true;
+}
diff --git a/src/ifcgeom/IfcCartesianTransformationOperator2D.cpp b/src/ifcgeom/IfcCartesianTransformationOperator2D.cpp
new file mode 100644
index 0000000000..d91e0c707f
--- /dev/null
+++ b/src/ifcgeom/IfcCartesianTransformationOperator2D.cpp
@@ -0,0 +1,69 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCartesianTransformationOperator2D* l, gp_Trsf2d& trsf) {
+ IN_CACHE(IfcCartesianTransformationOperator2D,l,gp_Trsf2d,trsf)
+
+ gp_Pnt origin;
+ gp_Dir axis1 (1.,0.,0.);
+ gp_Dir axis2 (0.,1.,0.);
+
+ IfcGeom::Kernel::convert(l->LocalOrigin(),origin);
+ if ( l->Axis1() ) IfcGeom::Kernel::convert(l->Axis1(),axis1);
+ if ( l->Axis2() ) IfcGeom::Kernel::convert(l->Axis2(),axis2);
+
+ const gp_Pnt2d origin2d(origin.X(), origin.Y());
+ const gp_Dir2d axis12d(axis1.X(), axis1.Y());
+ const gp_Dir2d axis22d(axis2.X(), axis2.Y());
+
+ // A better match to represent the IfcCartesianTransformationOperator2D would
+ // be the gp_Ax22d, but to my knowledge no easy way exists to convert it into
+ // a gp_Trsf2d. Easiest would probably be to simply update the underlying
+ // gp_Mat2d directly.
+
+ const gp_Ax2d ax2d (origin2d, axis12d);
+ trsf.SetTransformation(ax2d);
+
+ if ( ax2d.Direction().Rotated(M_PI / 2.).Dot(axis22d) < 0. ) {
+ gp_Trsf2d mirror; mirror.SetMirror(ax2d);
+ trsf.Multiply(mirror);
+ }
+
+ trsf.Invert();
+ if (l->Scale() && !ALMOST_THE_SAME(*l->Scale(), 1.)) {
+ trsf.SetScaleFactor(*l->Scale());
+ }
+
+ if (is_identity(trsf, getValue(GV_PRECISION))) {
+ trsf = gp_Trsf2d();
+ }
+
+ CACHE(IfcCartesianTransformationOperator2D,l,trsf)
+ return true;
+}
diff --git a/src/ifcgeom/IfcCartesianTransformationOperator2DnonUniform.cpp b/src/ifcgeom/IfcCartesianTransformationOperator2DnonUniform.cpp
new file mode 100644
index 0000000000..c1735399c4
--- /dev/null
+++ b/src/ifcgeom/IfcCartesianTransformationOperator2DnonUniform.cpp
@@ -0,0 +1,70 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCartesianTransformationOperator2DnonUniform* l, gp_GTrsf2d& gtrsf) {
+ IN_CACHE(IfcCartesianTransformationOperator2DnonUniform,l,gp_GTrsf2d,gtrsf)
+
+ gp_Trsf2d trsf;
+ gp_Pnt origin;
+ gp_Dir axis1 (1.,0.,0.);
+ gp_Dir axis2 (0.,1.,0.);
+
+ IfcGeom::Kernel::convert(l->LocalOrigin(),origin);
+ if ( l->Axis1() ) IfcGeom::Kernel::convert(l->Axis1(),axis1);
+ if ( l->Axis2() ) IfcGeom::Kernel::convert(l->Axis2(),axis2);
+
+ const gp_Pnt2d origin2d(origin.X(), origin.Y());
+ const gp_Dir2d axis12d(axis1.X(), axis1.Y());
+ const gp_Dir2d axis22d(axis2.X(), axis2.Y());
+
+ const gp_Ax2d ax2d (origin2d, axis12d);
+ trsf.SetTransformation(ax2d);
+
+ if ( ax2d.Direction().Rotated(M_PI / 2.).Dot(axis22d) < 0. ) {
+ gp_Trsf2d mirror; mirror.SetMirror(ax2d);
+ trsf.Multiply(mirror);
+ }
+
+ trsf.Invert();
+
+ const double scale1 = l->Scale().get_value_or(1.);
+ const double scale2 = l->Scale2().get_value_or(scale1);
+ gtrsf = gp_GTrsf2d();
+ gtrsf.SetValue(1,1,scale1);
+ gtrsf.SetValue(2,2,scale2);
+ gtrsf.Multiply(trsf);
+
+ if (is_identity(gtrsf, getValue(GV_PRECISION))) {
+ gtrsf = gp_GTrsf2d();
+ }
+
+ CACHE(IfcCartesianTransformationOperator2DnonUniform,l,gtrsf)
+ return true;
+}
diff --git a/src/ifcgeom/IfcCartesianTransformationOperator3D.cpp b/src/ifcgeom/IfcCartesianTransformationOperator3D.cpp
new file mode 100644
index 0000000000..730a2e9960
--- /dev/null
+++ b/src/ifcgeom/IfcCartesianTransformationOperator3D.cpp
@@ -0,0 +1,52 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCartesianTransformationOperator3D* l, gp_Trsf& trsf) {
+ IN_CACHE(IfcCartesianTransformationOperator3D,l,gp_Trsf,trsf)
+ gp_Pnt origin;
+ IfcGeom::Kernel::convert(l->LocalOrigin(),origin);
+ gp_Dir axis1 (1.,0.,0.);
+ gp_Dir axis2 (0.,1.,0.);
+ gp_Dir axis3 (0.,0.,1.);
+ if ( l->Axis1() ) IfcGeom::Kernel::convert(l->Axis1(),axis1);
+ if ( l->Axis2() ) IfcGeom::Kernel::convert(l->Axis2(),axis2);
+ if ( l->Axis3() ) IfcGeom::Kernel::convert(l->Axis3(),axis3);
+ gp_Ax3 ax3 (origin,axis3,axis1);
+ if ( axis2.Dot(ax3.YDirection()) < 0 ) ax3.YReverse();
+
+ if (!axis_equal(ax3, (gp_Ax3) gp::XOY(), getValue(GV_PRECISION))) {
+ trsf.SetTransformation(ax3);
+ trsf.Invert();
+ }
+
+ if (l->Scale() && !ALMOST_THE_SAME(*l->Scale(), 1.)) {
+ trsf.SetScaleFactor(*l->Scale());
+ }
+
+ CACHE(IfcCartesianTransformationOperator3D,l,trsf)
+ return true;
+}
diff --git a/src/ifcgeom/IfcCartesianTransformationOperator3DnonUniform.cpp b/src/ifcgeom/IfcCartesianTransformationOperator3DnonUniform.cpp
new file mode 100644
index 0000000000..9bedf1c115
--- /dev/null
+++ b/src/ifcgeom/IfcCartesianTransformationOperator3DnonUniform.cpp
@@ -0,0 +1,59 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCartesianTransformationOperator3DnonUniform* l, gp_GTrsf& gtrsf) {
+ IN_CACHE(IfcCartesianTransformationOperator3DnonUniform,l,gp_GTrsf,gtrsf)
+ gp_Trsf trsf;
+ gp_Pnt origin;
+ IfcGeom::Kernel::convert(l->LocalOrigin(),origin);
+ gp_Dir axis1 (1.,0.,0.);
+ gp_Dir axis2 (0.,1.,0.);
+ gp_Dir axis3 (0.,0.,1.);
+ if ( l->Axis1() ) IfcGeom::Kernel::convert(l->Axis1(),axis1);
+ if ( l->Axis2() ) IfcGeom::Kernel::convert(l->Axis2(),axis2);
+ if ( l->Axis3() ) IfcGeom::Kernel::convert(l->Axis3(),axis3);
+ gp_Ax3 ax3 (origin,axis3,axis1);
+ if ( axis2.Dot(ax3.YDirection()) < 0 ) ax3.YReverse();
+ trsf.SetTransformation(ax3);
+ trsf.Invert();
+ const double scale1 = l->Scale().get_value_or(1.);
+ const double scale2 = l->Scale2().get_value_or(scale1);
+ const double scale3 = l->Scale3().get_value_or(scale1);
+ gtrsf = gp_GTrsf();
+ gtrsf.SetValue(1,1,scale1);
+ gtrsf.SetValue(2,2,scale2);
+ gtrsf.SetValue(3,3,scale3);
+ gtrsf.PreMultiply(trsf);
+
+ if (is_identity(gtrsf, getValue(GV_PRECISION))) {
+ gtrsf = gp_GTrsf();
+ }
+
+ CACHE(IfcCartesianTransformationOperator3DnonUniform,l,gtrsf)
+ return true;
+}
diff --git a/src/ifcgeom/IfcCenterLineProfileDef.cpp b/src/ifcgeom/IfcCenterLineProfileDef.cpp
new file mode 100644
index 0000000000..03f701a3ee
--- /dev/null
+++ b/src/ifcgeom/IfcCenterLineProfileDef.cpp
@@ -0,0 +1,84 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCenterLineProfileDef* l, TopoDS_Shape& face) {
+ const double d = l->Thickness() * getValue(GV_LENGTH_UNIT) / 2.;
+
+ TopoDS_Wire wire;
+ if (!convert_wire(l->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));
+ }
+ return true;
+}
diff --git a/src/ifcgeom/IfcCircle.cpp b/src/ifcgeom/IfcCircle.cpp
new file mode 100644
index 0000000000..731fa13130
--- /dev/null
+++ b/src/ifcgeom/IfcCircle.cpp
@@ -0,0 +1,45 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCircle* l, Handle(Geom_Curve)& curve) {
+ const double r = l->Radius() * getValue(GV_LENGTH_UNIT);
+ if ( r < ALMOST_ZERO ) {
+ Logger::Message(Logger::LOG_ERROR, "Radius not greater than zero for:", l);
+ return false;
+ }
+ gp_Trsf trsf;
+ IfcSchema::IfcAxis2Placement* placement = l->Position();
+ if (placement->as()) {
+ IfcGeom::Kernel::convert(placement->as(),trsf);
+ } else {
+ gp_Trsf2d trsf2d;
+ IfcGeom::Kernel::convert(placement->as(),trsf2d);
+ trsf = trsf2d;
+ }
+ gp_Ax2 ax = gp_Ax2().Transformed(trsf);
+ curve = new Geom_Circle(ax, r);
+ return true;
+}
diff --git a/src/ifcgeom/IfcCircleHollowProfileDef.cpp b/src/ifcgeom/IfcCircleHollowProfileDef.cpp
new file mode 100644
index 0000000000..6a9e7fbf3e
--- /dev/null
+++ b/src/ifcgeom/IfcCircleHollowProfileDef.cpp
@@ -0,0 +1,67 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCircleHollowProfileDef* l, TopoDS_Shape& face) {
+ const double r = l->Radius() * getValue(GV_LENGTH_UNIT);
+ const double t = l->WallThickness() * getValue(GV_LENGTH_UNIT);
+
+ if ( r == 0.0f || t == 0.0f ) {
+ Logger::Message(Logger::LOG_NOTICE,"Skipping zero sized profile:",l);
+ return false;
+ }
+
+ gp_Trsf2d trsf2d;
+ bool has_position = true;
+#ifdef SCHEMA_IfcParameterizedProfileDef_Position_IS_OPTIONAL
+ has_position = l->Position() != nullptr;
+#endif
+ if (has_position) {
+ IfcGeom::Kernel::convert(l->Position(), trsf2d);
+ }
+
+ gp_Ax2 ax = gp_Ax2().Transformed(trsf2d);
+
+ BRepBuilderAPI_MakeWire outer;
+ Handle(Geom_Circle) outerCircle = new Geom_Circle(ax, r);
+ outer.Add(BRepBuilderAPI_MakeEdge(outerCircle));
+ BRepBuilderAPI_MakeFace mf(outer.Wire(), false);
+
+ BRepBuilderAPI_MakeWire inner;
+ Handle(Geom_Circle) innerCirlce = new Geom_Circle(ax, r-t);
+ inner.Add(BRepBuilderAPI_MakeEdge(innerCirlce));
+ mf.Add(inner);
+
+ ShapeFix_Shape sfs(mf.Face());
+ sfs.Perform();
+ face = TopoDS::Face(sfs.Shape());
+ return true;
+}
diff --git a/src/ifcgeom/IfcCircleProfileDef.cpp b/src/ifcgeom/IfcCircleProfileDef.cpp
new file mode 100644
index 0000000000..d97ff05041
--- /dev/null
+++ b/src/ifcgeom/IfcCircleProfileDef.cpp
@@ -0,0 +1,58 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCircleProfileDef* l, TopoDS_Shape& face) {
+ const double r = l->Radius() * getValue(GV_LENGTH_UNIT);
+ if ( r == 0.0f ) {
+ Logger::Message(Logger::LOG_NOTICE,"Skipping zero sized profile:",l);
+ return false;
+ }
+
+ gp_Trsf2d trsf2d;
+ bool has_position = true;
+#ifdef SCHEMA_IfcParameterizedProfileDef_Position_IS_OPTIONAL
+ has_position = l->Position() != nullptr;
+#endif
+ if (has_position) {
+ IfcGeom::Kernel::convert(l->Position(), trsf2d);
+ }
+ gp_Ax2 ax = gp_Ax2().Transformed(trsf2d);
+
+
+ Handle(Geom_Circle) circle = new Geom_Circle(ax, r);
+ TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(circle);
+
+ BRepBuilderAPI_MakeWire w;
+ w.Add(edge);
+
+ TopoDS_Face f;
+ bool success = convert_wire_to_face(w, f);
+ if (success) face = f;
+ return success;
+}
diff --git a/src/ifcgeom/IfcCompositeCurve.cpp b/src/ifcgeom/IfcCompositeCurve.cpp
new file mode 100644
index 0000000000..a6345c3b02
--- /dev/null
+++ b/src/ifcgeom/IfcCompositeCurve.cpp
@@ -0,0 +1,183 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#include "../ifcgeom_schema_agnostic/wire_builder.h"
+
+#define _USE_MATH_DEFINES
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCompositeCurve* l, TopoDS_Wire& wire) {
+ if ( getValue(GV_PLANEANGLE_UNIT)<0 ) {
+ Logger::Message(Logger::LOG_WARNING,"Creating a composite curve without unit information:",l);
+
+ // Temporarily pretend we do have unit information
+ setValue(GV_PLANEANGLE_UNIT,1.0);
+
+ bool succes_radians = false;
+ bool succes_degrees = false;
+ bool use_radians = false;
+ bool use_degrees = false;
+
+ // First try radians
+ TopoDS_Wire wire_radians, wire_degrees;
+ try {
+ succes_radians = IfcGeom::Kernel::convert(l,wire_radians);
+ } catch (const std::exception& e) {
+ Logger::Notice(e);
+ } catch (const Standard_Failure& e) {
+ if (e.GetMessageString() && strlen(e.GetMessageString())) {
+ Logger::Notice(e.GetMessageString());
+ } else {
+ Logger::Notice("Unknown error using radians");
+ }
+ } catch (...) {
+ Logger::Notice("Unknown error using radians");
+ }
+
+ // Now try degrees
+ setValue(GV_PLANEANGLE_UNIT,0.0174532925199433);
+ try {
+ succes_degrees = IfcGeom::Kernel::convert(l,wire_degrees);
+ } catch (const std::exception& e) {
+ Logger::Notice(e);
+ } catch (const Standard_Failure& e) {
+ if (e.GetMessageString() && strlen(e.GetMessageString())) {
+ Logger::Notice(e.GetMessageString());
+ } else {
+ Logger::Notice("Unknown error using degrees");
+ }
+ } catch (...) {
+ Logger::Notice("Unknown error using degrees");
+ }
+
+ // Restore to unknown unit state
+ setValue(GV_PLANEANGLE_UNIT,-1.0);
+
+ if ( succes_degrees && ! succes_radians ) {
+ use_degrees = true;
+ } else if ( succes_radians && ! succes_degrees ) {
+ use_radians = true;
+ } else if ( succes_radians && succes_degrees ) {
+ if ( wire_degrees.Closed() && ! wire_radians.Closed() ) {
+ use_degrees = true;
+ } else if ( wire_radians.Closed() && ! wire_degrees.Closed() ) {
+ use_radians = true;
+ } else {
+ // No heuristic left to prefer the one over the other,
+ // apparently both variants are equally successful.
+ // The curve might be composed of only straight segments.
+ // Let's go with the wire created using radians as that
+ // at least is a SI unit.
+ use_radians = true;
+ }
+ }
+
+ if ( use_radians ) {
+ Logger::Message(Logger::LOG_NOTICE,"Used radians to create composite curve");
+ wire = wire_radians;
+ } else if ( use_degrees ) {
+ Logger::Message(Logger::LOG_NOTICE,"Used degrees to create composite curve");
+ wire = wire_degrees;
+ }
+
+ return use_radians || use_degrees;
+ }
+
+#ifdef SCHEMA_HAS_IfcSegment
+ // 4x3
+ IfcSchema::IfcSegment::list::ptr segments = l->Segments();
+#else
+ IfcSchema::IfcCompositeCurveSegment::list::ptr segments = l->Segments();
+#endif
+
+ TopTools_ListOfShape converted_segments;
+
+ for (auto it = segments->begin(); it != segments->end(); ++it) {
+
+ if (!(*it)->declaration().is(IfcSchema::IfcCompositeCurveSegment::Class())) {
+ Logger::Error("Not implemented", *it);
+ return false;
+ }
+
+ IfcSchema::IfcCurve* curve = ((IfcSchema::IfcCompositeCurveSegment*)(*it))->ParentCurve();
+
+ // The type of ParentCurve is IfcCurve, but the documentation says:
+ // ParentCurve: The *bounded curve* which defines the geometry of the segment.
+ // At least let's exclude IfcLine as an infinite linear segment
+ // definitely does not make any sense.
+ TopoDS_Wire segment;
+
+ if (curve->as()) {
+ Logger::Notice("Infinite IfcLine used as ParentCurve of segment, treating as a segment", *it);
+ Handle_Geom_Curve handle;
+ convert_curve(curve, handle);
+ double u0 = 0.0;
+ double u1 = curve->as()->Dir()->Magnitude() * getValue(GV_LENGTH_UNIT);
+ if (u1 < getValue(GV_PRECISION)) {
+ Logger::Warning("Segment length below tolerance", *it);
+ }
+ BRepBuilderAPI_MakeEdge me(handle, u0, u1);
+ if (me.IsDone()) {
+ BRep_Builder B;
+ B.MakeWire(segment);
+ B.Add(segment, me.Edge());
+ }
+ } else if (!convert_wire(curve, segment)) {
+ const bool failed_on_purpose = curve->as() && !segment.IsNull();
+ Logger::Message(failed_on_purpose ? Logger::LOG_WARNING : Logger::LOG_ERROR, "Failed to convert curve:", curve);
+ continue;
+ }
+
+ if (!((IfcSchema::IfcCompositeCurveSegment*)(*it))->SameSense()) {
+ segment.Reverse();
+ }
+
+ ShapeFix_ShapeTolerance FTol;
+ FTol.SetTolerance(segment, getValue(GV_PRECISION), TopAbs_WIRE);
+
+ converted_segments.Append(segment);
+
+ }
+
+ if (converted_segments.Extent() == 0) {
+ Logger::Message(Logger::LOG_ERROR, "No segment succesfully converted:", l);
+ return false;
+ }
+
+ BRepBuilderAPI_MakeWire w;
+ TopoDS_Vertex wire_first_vertex, wire_last_vertex, edge_first_vertex, edge_last_vertex;
+
+ TopTools_ListIteratorOfListOfShape it(converted_segments);
+
+ aggregate_of_instance::ptr profile = l->data().getInverse(&IfcSchema::IfcProfileDef::Class(), -1);
+ const bool force_close = profile && profile->size() > 0;
+
+ util::wire_builder bld(getValue(GV_PRECISION), l);
+ util::shape_pair_enumerate(it, bld, force_close);
+ wire = bld.wire();
+
+ return true;
+}
diff --git a/src/ifcgeom/IfcCompositeProfileDef.cpp b/src/ifcgeom/IfcCompositeProfileDef.cpp
new file mode 100644
index 0000000000..4c3ae3ea57
--- /dev/null
+++ b/src/ifcgeom/IfcCompositeProfileDef.cpp
@@ -0,0 +1,53 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCompositeProfileDef* l, TopoDS_Shape& face) {
+ // BRepBuilderAPI_MakeFace mf;
+
+ TopoDS_Compound compound;
+ BRep_Builder builder;
+ builder.MakeCompound(compound);
+
+ IfcSchema::IfcProfileDef::list::ptr profiles = l->Profiles();
+ //bool first = true;
+ for (IfcSchema::IfcProfileDef::list::it it = profiles->begin(); it != profiles->end(); ++it) {
+ TopoDS_Face f;
+ if (convert_face(*it, f)) {
+ builder.Add(compound, f);
+ /* TopExp_Explorer exp(f, TopAbs_WIRE);
+ for (; exp.More(); exp.Next()) {
+ const TopoDS_Wire& wire = TopoDS::Wire(exp.Current());
+ if (first) {
+ mf.Init(BRepBuilderAPI_MakeFace(wire));
+ } else {
+ mf.Add(wire);
+ }
+ first = false;
+ } */
+ }
+ }
+
+ face = compound;
+ return !face.IsNull();
+}
diff --git a/src/ifcgeom/IfcConnectedFaceSet.cpp b/src/ifcgeom/IfcConnectedFaceSet.cpp
new file mode 100644
index 0000000000..b9a419c990
--- /dev/null
+++ b/src/ifcgeom/IfcConnectedFaceSet.cpp
@@ -0,0 +1,114 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+#include
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcConnectedFaceSet* l, TopoDS_Shape& shape) {
+ std::unique_ptr> helper_scope;
+
+ IfcSchema::IfcCartesianPoint::list::ptr points = IfcParse::traverse((IfcUtil::IfcBaseClass*) l)->as();
+ std::vector points_(points->begin(), points->end());
+
+ IfcSchema::IfcPolyLoop::list::ptr loops = IfcParse::traverse((IfcUtil::IfcBaseClass*)l)->as();
+ std::vector loops_(loops->begin(), loops->end());
+
+ helper_scope.reset(new faceset_helper<>(
+ this,
+ points_,
+ loops_,
+ l->declaration().is(IfcSchema::IfcClosedShell::Class())
+ ));
+
+ faceset_helper_ = helper_scope.get();
+
+ IfcSchema::IfcFace::list::ptr faces = l->CfsFaces();
+
+ double min_face_area = faceset_helper_
+ ? (faceset_helper_->epsilon() * faceset_helper_->epsilon() / 20.)
+ : getValue(GV_MINIMAL_FACE_AREA);
+
+ TopTools_ListOfShape face_list;
+ for (IfcSchema::IfcFace::list::it it = faces->begin(); it != faces->end(); ++it) {
+ bool success = false;
+ TopoDS_Face face;
+
+ try {
+ success = convert_face(*it, face);
+ } catch (const std::exception& e) {
+ Logger::Error(e);
+ } catch (const Standard_Failure& e) {
+ if (e.GetMessageString() && strlen(e.GetMessageString())) {
+ Logger::Error(e.GetMessageString());
+ } else {
+ Logger::Error("Unknown error creating face");
+ }
+ } catch (...) {
+ Logger::Error("Unknown error creating face");
+ }
+
+ if (!success) {
+ Logger::Message(Logger::LOG_WARNING, "Failed to convert face:", (*it));
+ continue;
+ }
+
+ if (face.ShapeType() == TopAbs_COMPOUND) {
+ TopoDS_Iterator face_it(face, false);
+ for (; face_it.More(); face_it.Next()) {
+ if (face_it.Value().ShapeType() == TopAbs_FACE) {
+ // This should really be the case. This is not asserted.
+ const TopoDS_Face& triangle = TopoDS::Face(face_it.Value());
+ if (face_area(triangle) > min_face_area) {
+ face_list.Append(triangle);
+ } else {
+ Logger::Message(Logger::LOG_WARNING, "Degenerate face:", (*it));
+ }
+ }
+ }
+ } else {
+ if (face_area(face) > min_face_area) {
+ face_list.Append(face);
+ } else {
+ Logger::Message(Logger::LOG_WARNING, "Degenerate face:", (*it));
+ }
+ }
+ }
+
+ if (face_list.Extent() == 0) {
+ return false;
+ }
+
+ if (face_list.Extent() > getValue(GV_MAX_FACES_TO_ORIENT) || !create_solid_from_faces(face_list, shape)) {
+ TopoDS_Compound compound;
+ BRep_Builder builder;
+ builder.MakeCompound(compound);
+
+ TopTools_ListIteratorOfListOfShape face_iterator;
+ for (face_iterator.Initialize(face_list); face_iterator.More(); face_iterator.Next()) {
+ builder.Add(compound, face_iterator.Value());
+ }
+ shape = compound;
+ }
+
+ return true;
+}
diff --git a/src/ifcgeom/IfcCraneRailAShapeProfileDef.cpp b/src/ifcgeom/IfcCraneRailAShapeProfileDef.cpp
new file mode 100644
index 0000000000..95d2306616
--- /dev/null
+++ b/src/ifcgeom/IfcCraneRailAShapeProfileDef.cpp
@@ -0,0 +1,65 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+#ifdef SCHEMA_HAS_IfcCraneRailAShapeProfileDef
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCraneRailAShapeProfileDef* l, TopoDS_Shape& face) {
+ double oh = l->OverallHeight() * getValue(GV_LENGTH_UNIT);
+ double bw2 = l->BaseWidth2() * getValue(GV_LENGTH_UNIT);
+ double hw = l->HeadWidth() * getValue(GV_LENGTH_UNIT);
+ double hd2 = l->HeadDepth2() * getValue(GV_LENGTH_UNIT);
+ double hd3 = l->HeadDepth3() * getValue(GV_LENGTH_UNIT);
+ double wt = l->WebThickness() * getValue(GV_LENGTH_UNIT);
+ double bw4 = l->BaseWidth4() * getValue(GV_LENGTH_UNIT);
+ double bd1 = l->BaseDepth1() * getValue(GV_LENGTH_UNIT);
+ double bd2 = l->BaseDepth2() * getValue(GV_LENGTH_UNIT);
+ double bd3 = l->BaseDepth3() * getValue(GV_LENGTH_UNIT);
+
+ gp_Trsf2d trsf2d;
+ bool has_position = true;
+#ifdef SCHEMA_IfcParameterizedProfileDef_Position_IS_OPTIONAL
+ has_position = l->Position() != nullptr;
+#endif
+ if (has_position) {
+ IfcGeom::Kernel::convert(l->Position(), trsf2d);
+ }
+
+ double coords[28] = {
+ -hw / 2., +oh / 2.,
+ -hw / 2., +oh / 2. - hd3,
+ -wt / 2., +oh / 2. - hd2,
+ -wt / 2., -oh / 2. + bd2,
+ -bw4 / 2., -oh / 2. + bd3,
+ -bw2 / 2., -oh / 2. + bd1,
+ -bw2 / 2., -oh / 2.,
+ +bw2 / 2., -oh / 2.,
+ +bw2 / 2., -oh / 2. + bd1,
+ +bw4 / 2., -oh / 2. + bd3,
+ +wt / 2., -oh / 2. + bd2,
+ +wt / 2., +oh / 2. - hd2,
+ +hw / 2., +oh / 2. - hd3,
+ +hw / 2., +oh / 2.
+ };
+
+ return profile_helper(14, coords, 0, 0, 0, trsf2d, face);
+}
+#endif
diff --git a/src/ifcgeom/IfcCsgSolid.cpp b/src/ifcgeom/IfcCsgSolid.cpp
new file mode 100644
index 0000000000..1a53a25509
--- /dev/null
+++ b/src/ifcgeom/IfcCsgSolid.cpp
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCsgSolid* l, TopoDS_Shape& shape) {
+ return convert_shape(l->TreeRootExpression(), shape);
+}
diff --git a/src/ifcgeom/IfcCurveBoundedPlane.cpp b/src/ifcgeom/IfcCurveBoundedPlane.cpp
new file mode 100644
index 0000000000..07f25981c6
--- /dev/null
+++ b/src/ifcgeom/IfcCurveBoundedPlane.cpp
@@ -0,0 +1,69 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCurveBoundedPlane* l, TopoDS_Shape& face) {
+ gp_Pln pln;
+ if (!IfcGeom::Kernel::convert(l->BasisSurface(), pln)) {
+ return false;
+ }
+
+ gp_Trsf trsf;
+ trsf.SetTransformation(pln.Position(), gp::XOY());
+
+ TopoDS_Wire outer;
+ if (!convert_wire(l->OuterBoundary(), outer)) {
+ return false;
+ }
+
+ BRepBuilderAPI_MakeFace mf(outer);
+
+ if (!mf.IsDone() || mf.Shape().IsNull()) {
+ Logger::Error("Invalid outer boundary:", l->OuterBoundary());
+ return false;
+ }
+
+ IfcSchema::IfcCurve::list::ptr boundaries = l->InnerBoundaries();
+
+ for (IfcSchema::IfcCurve::list::it it = boundaries->begin(); it != boundaries->end(); ++it) {
+ TopoDS_Wire inner;
+ if (convert_wire(*it, inner)) {
+ mf.Add(inner);
+ }
+ }
+
+ ShapeFix_Shape sfs(mf.Face());
+ sfs.Perform();
+
+ // `trsf` consitutes the placement of the plane and therefore has unit scale factor
+ face = TopoDS::Face(sfs.Shape()).Moved(trsf);
+
+ return true;
+}
diff --git a/src/ifcgeom/IfcCylindricalSurface.cpp b/src/ifcgeom/IfcCylindricalSurface.cpp
new file mode 100644
index 0000000000..a74fc41f1b
--- /dev/null
+++ b/src/ifcgeom/IfcCylindricalSurface.cpp
@@ -0,0 +1,39 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+#ifdef SCHEMA_HAS_IfcCylindricalSurface
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcCylindricalSurface* l, TopoDS_Shape& face) {
+ gp_Trsf trsf;
+ IfcGeom::Kernel::convert(l->Position(),trsf);
+
+ // IfcElementarySurface.Position has unit scale factor
+ face = BRepBuilderAPI_MakeFace(new Geom_CylindricalSurface(gp::XOY(), l->Radius() * getValue(GV_LENGTH_UNIT)), getValue(GV_PRECISION)).Face().Moved(trsf);
+ return true;
+}
+
+#endif
diff --git a/src/ifcgeom/IfcDerivedProfileDef.cpp b/src/ifcgeom/IfcDerivedProfileDef.cpp
new file mode 100644
index 0000000000..1bf666540a
--- /dev/null
+++ b/src/ifcgeom/IfcDerivedProfileDef.cpp
@@ -0,0 +1,38 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcDerivedProfileDef* l, TopoDS_Shape& face) {
+ TopoDS_Face f;
+ gp_Trsf2d trsf2d;
+ if (convert_face(l->ParentProfile(), f) && IfcGeom::Kernel::convert(l->Operator(), trsf2d)) {
+ gp_Trsf trsf = trsf2d;
+ face = BRepBuilderAPI_Transform(f, trsf).Shape();
+ return true;
+ } else {
+ return false;
+ }
+}
diff --git a/src/ifcgeom/IfcDirection.cpp b/src/ifcgeom/IfcDirection.cpp
new file mode 100644
index 0000000000..21e84df886
--- /dev/null
+++ b/src/ifcgeom/IfcDirection.cpp
@@ -0,0 +1,35 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcDirection* l, gp_Dir& dir) {
+ IN_CACHE(IfcDirection,l,gp_Dir,dir)
+ std::vector xyz = l->DirectionRatios();
+ dir = gp_Dir(
+ xyz.size() ? xyz[0] : 0.0f,
+ xyz.size() > 1 ? xyz[1] : 0.0f,
+ xyz.size() > 2 ? xyz[2] : 0.0f
+ );
+ CACHE(IfcDirection,l,dir)
+ return true;
+}
diff --git a/src/ifcgeom/IfcEdge.cpp b/src/ifcgeom/IfcEdge.cpp
new file mode 100644
index 0000000000..ea119ca2ad
--- /dev/null
+++ b/src/ifcgeom/IfcEdge.cpp
@@ -0,0 +1,54 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define _USE_MATH_DEFINES
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcEdge* l, TopoDS_Wire& result) {
+ if (!l->EdgeStart()->declaration().is(IfcSchema::IfcVertexPoint::Class()) || !l->EdgeEnd()->declaration().is(IfcSchema::IfcVertexPoint::Class())) {
+ Logger::Message(Logger::LOG_ERROR, "Only IfcVertexPoints are supported for EdgeStart and -End", l);
+ return false;
+ }
+
+ IfcSchema::IfcPoint* pnt1 = ((IfcSchema::IfcVertexPoint*) l->EdgeStart())->VertexGeometry();
+ IfcSchema::IfcPoint* pnt2 = ((IfcSchema::IfcVertexPoint*) l->EdgeEnd())->VertexGeometry();
+ if (!pnt1->declaration().is(IfcSchema::IfcCartesianPoint::Class()) || !pnt2->declaration().is(IfcSchema::IfcCartesianPoint::Class())) {
+ Logger::Message(Logger::LOG_ERROR, "Only IfcCartesianPoints are supported for VertexGeometry", l);
+ return false;
+ }
+
+ gp_Pnt p1, p2;
+ if (!convert(((IfcSchema::IfcCartesianPoint*)pnt1), p1) ||
+ !convert(((IfcSchema::IfcCartesianPoint*)pnt2), p2))
+ {
+ return false;
+ }
+
+ BRepBuilderAPI_MakeWire mw;
+ mw.Add(BRepBuilderAPI_MakeEdge(p1, p2));
+
+ result = mw.Wire();
+ return true;
+}
diff --git a/src/ifcgeom/IfcEdgeCurve.cpp b/src/ifcgeom/IfcEdgeCurve.cpp
new file mode 100644
index 0000000000..7cf5a34452
--- /dev/null
+++ b/src/ifcgeom/IfcEdgeCurve.cpp
@@ -0,0 +1,118 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#include "../ifcgeom_schema_agnostic/wire_builder.h"
+
+#define _USE_MATH_DEFINES
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcEdgeCurve* l, TopoDS_Wire& result) {
+ IfcSchema::IfcPoint* pnt1 = ((IfcSchema::IfcVertexPoint*) l->EdgeStart())->VertexGeometry();
+ IfcSchema::IfcPoint* pnt2 = ((IfcSchema::IfcVertexPoint*) l->EdgeEnd())->VertexGeometry();
+ if (!pnt1->declaration().is(IfcSchema::IfcCartesianPoint::Class()) || !pnt2->declaration().is(IfcSchema::IfcCartesianPoint::Class())) {
+ Logger::Message(Logger::LOG_ERROR, "Only IfcCartesianPoints are supported for VertexGeometry", l);
+ return false;
+ }
+
+ gp_Pnt p1, p2;
+ if (!IfcGeom::Kernel::convert(((IfcSchema::IfcCartesianPoint*)pnt1), p1) ||
+ !IfcGeom::Kernel::convert(((IfcSchema::IfcCartesianPoint*)pnt2), p2))
+ {
+ return false;
+ }
+
+ BRepBuilderAPI_MakeWire mw;
+ Handle_Geom_Curve crv;
+
+ // The lack of a clear separation between topological and geometrical entities
+ // is starting to get problematic. If the underlying curve is bounded it is
+ // assumed that a topological wire can be crafted from it. After which an
+ // attempt is made to reconstruct it from the individual curves and the vertices
+ // of the IfcEdgeCurve.
+ const bool is_bounded = l->EdgeGeometry()->declaration().is(IfcSchema::IfcBoundedCurve::Class());
+
+ if (!is_bounded && convert_curve(l->EdgeGeometry(), crv)) {
+ TopoDS_Edge e;
+ if (util::create_edge_over_curve_with_log_messages(crv, getValue(GV_PRECISION), p1, p2, e)) {
+ mw.Add(e);
+ result = mw;
+ return true;
+ } else {
+ return false;
+ }
+ } else if (is_bounded && convert_wire(l->EdgeGeometry(), result)) {
+ if (!l->SameSense()) {
+ result.Reverse();
+ }
+
+ bool first = true;
+ TopExp_Explorer exp(result, TopAbs_EDGE);
+
+ while (exp.More()) {
+ const TopoDS_Edge& ed = TopoDS::Edge(exp.Current());
+ Standard_Real u1, u2;
+ Handle(Geom_Curve) ecrv = BRep_Tool::Curve(ed, u1, u2);
+ exp.Next();
+ const bool last = !exp.More();
+
+ gp_Pnt a, b;
+
+ if (first && last) {
+ a = p1;
+ b = p2;
+ } else if (first) {
+ a = p1;
+ ecrv->D0(u2, b);
+ } else if (last) {
+ ecrv->D0(u1, a);
+ b = p2;
+ } else {
+ BRepBuilderAPI_MakeEdge me(ecrv, u1, u2);
+ if (!me.IsDone()) {
+ return false;
+ }
+ mw.Add(me.Edge());
+ first = false;
+ continue;
+ }
+
+ TopoDS_Edge e;
+ if (util::create_edge_over_curve_with_log_messages(ecrv, getValue(GV_PRECISION), a, b, e)) {
+ mw.Add(e);
+ } else {
+ return false;
+ }
+
+ first = false;
+ }
+ result = mw;
+ return true;
+ } else {
+ return false;
+ }
+}
diff --git a/src/ifcgeom/IfcEdgeLoop.cpp b/src/ifcgeom/IfcEdgeLoop.cpp
new file mode 100644
index 0000000000..b3c49d933a
--- /dev/null
+++ b/src/ifcgeom/IfcEdgeLoop.cpp
@@ -0,0 +1,42 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define _USE_MATH_DEFINES
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcEdgeLoop* l, TopoDS_Wire& result) {
+ IfcSchema::IfcOrientedEdge::list::ptr li = l->EdgeList();
+ BRepBuilderAPI_MakeWire mw;
+ for (IfcSchema::IfcOrientedEdge::list::it it = li->begin(); it != li->end(); ++it) {
+ TopoDS_Wire w;
+ if (convert_wire(*it, w)) {
+ mw.Add(TopoDS::Edge(TopoDS_Iterator(w).Value()));
+ }
+ }
+ if (!mw.IsDone()) {
+ return false;
+ }
+ result = mw.Wire();
+ return true;
+}
diff --git a/src/ifcgeom/IfcEllipse.cpp b/src/ifcgeom/IfcEllipse.cpp
new file mode 100644
index 0000000000..c76cc9d118
--- /dev/null
+++ b/src/ifcgeom/IfcEllipse.cpp
@@ -0,0 +1,58 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcEllipse* l, Handle(Geom_Curve)& curve) {
+ double x = l->SemiAxis1() * getValue(GV_LENGTH_UNIT);
+ double y = l->SemiAxis2() * getValue(GV_LENGTH_UNIT);
+ if (x < ALMOST_ZERO || y < ALMOST_ZERO) {
+ Logger::Message(Logger::LOG_ERROR, "Radius not greater than zero for:", l);
+ return false;
+ }
+ // Open Cascade does not allow ellipses of which the minor radius
+ // is greater than the major radius. Hence, in this case, the
+ // ellipse is rotated. Note that special care needs to be taken
+ // when creating a trimmed curve off of an ellipse like this.
+ const bool rotated = y > x;
+ gp_Trsf trsf;
+
+ IfcSchema::IfcAxis2Placement* placement = l->Position();
+ if (placement->as()) {
+ IfcGeom::Kernel::convert(placement->as(), trsf);
+ } else {
+ gp_Trsf2d trsf2d;
+ IfcGeom::Kernel::convert(placement->as(), trsf2d);
+ trsf = trsf2d;
+ }
+
+ gp_Ax2 ax = gp_Ax2();
+ if (rotated) {
+ ax.Rotate(ax.Axis(), M_PI / 2.);
+ std::swap(x, y);
+ }
+ ax.Transform(trsf);
+ curve = new Geom_Ellipse(ax, x, y);
+ return true;
+}
diff --git a/src/ifcgeom/IfcEllipseProfileDef.cpp b/src/ifcgeom/IfcEllipseProfileDef.cpp
new file mode 100644
index 0000000000..77de9babc5
--- /dev/null
+++ b/src/ifcgeom/IfcEllipseProfileDef.cpp
@@ -0,0 +1,66 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcEllipseProfileDef* l, TopoDS_Shape& face) {
+ double rx = l->SemiAxis1() * getValue(GV_LENGTH_UNIT);
+ double ry = l->SemiAxis2() * getValue(GV_LENGTH_UNIT);
+
+ if ( rx < ALMOST_ZERO || ry < ALMOST_ZERO ) {
+ Logger::Message(Logger::LOG_NOTICE,"Skipping zero sized profile:",l);
+ return false;
+ }
+
+ const bool rotated = ry > rx;
+
+ gp_Trsf2d trsf2d;
+ bool has_position = true;
+#ifdef SCHEMA_IfcParameterizedProfileDef_Position_IS_OPTIONAL
+ has_position = l->Position() != nullptr;
+#endif
+ if (has_position) {
+ IfcGeom::Kernel::convert(l->Position(), trsf2d);
+ }
+
+ gp_Ax2 ax = gp_Ax2();
+ if (rotated) {
+ ax.Rotate(ax.Axis(), M_PI / 2.);
+ std::swap(rx, ry);
+ }
+ ax.Transform(trsf2d);
+
+ BRepBuilderAPI_MakeWire w;
+ Handle(Geom_Ellipse) ellipse = new Geom_Ellipse(ax, rx, ry);
+ TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(ellipse);
+ w.Add(edge);
+
+ TopoDS_Face f;
+ bool success = convert_wire_to_face(w, f);
+ if (success) face = f;
+ return success;
+}
diff --git a/src/ifcgeom/IfcExtrudedAreaSolid.cpp b/src/ifcgeom/IfcExtrudedAreaSolid.cpp
new file mode 100644
index 0000000000..0f354d5de8
--- /dev/null
+++ b/src/ifcgeom/IfcExtrudedAreaSolid.cpp
@@ -0,0 +1,100 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+// uncomment if you'd like negative or close to zero extrusion depths to succeed
+// #define PERMISSIVE_EXTRUSION
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcExtrudedAreaSolid* l, TopoDS_Shape& shape) {
+ const double height = l->Depth() * getValue(GV_LENGTH_UNIT);
+ if (height < getValue(GV_PRECISION)) {
+ Logger::Message(Logger::LOG_ERROR, "Non-positive extrusion height encountered for:", l);
+#ifndef PERMISSIVE_EXTRUSION
+ return false;
+#endif
+ }
+
+ TopoDS_Shape face;
+ if ( !convert_face(l->SweptArea(),face) ) return false;
+
+#ifdef PERMISSIVE_EXTRUSION
+ if (abs(height) < getValue(GV_PRECISION)) {
+ shape = face;
+ return true;
+ }
+#endif
+
+ gp_Trsf trsf;
+ bool has_position = true;
+#ifdef SCHEMA_IfcSweptAreaSolid_Position_IS_OPTIONAL
+ has_position = l->Position() != nullptr;
+#endif
+ if (has_position) {
+ IfcGeom::Kernel::convert(l->Position(), trsf);
+ }
+
+ gp_Dir dir;
+ convert(l->ExtrudedDirection(),dir);
+
+ shape.Nullify();
+
+ /*
+ // @nb This logic was probably flawed always as this does not by itself result in a valid compsolid...
+ if (face.ShapeType() == TopAbs_COMPOUND) {
+
+ // For compounds (most likely the result of a IfcCompositeProfileDef)
+ // create a compound solid shape.
+
+ TopExp_Explorer exp(face, TopAbs_FACE);
+
+ TopoDS_CompSolid compound;
+ BRep_Builder builder;
+ builder.MakeCompSolid(compound);
+
+ int num_faces_extruded = 0;
+ for (; exp.More(); exp.Next(), ++num_faces_extruded) {
+ builder.Add(compound, BRepPrimAPI_MakePrism(exp.Current(), height*dir));
+ }
+
+ if (num_faces_extruded) {
+ shape = compound;
+ }
+
+ }
+ */
+
+ if (shape.IsNull()) {
+ shape = BRepPrimAPI_MakePrism(face, height*dir);
+ }
+
+ if (has_position && !shape.IsNull()) {
+ // IfcSweptAreaSolid.Position (trsf) is an IfcAxis2Placement3D
+ // and therefore has a unit scale factor
+ shape.Move(trsf);
+ }
+
+ return !shape.IsNull();
+}
diff --git a/src/ifcgeom/IfcExtrudedAreaSolidTapered.cpp b/src/ifcgeom/IfcExtrudedAreaSolidTapered.cpp
new file mode 100644
index 0000000000..bcf0dc6d5e
--- /dev/null
+++ b/src/ifcgeom/IfcExtrudedAreaSolidTapered.cpp
@@ -0,0 +1,140 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+#ifdef SCHEMA_HAS_IfcExtrudedAreaSolidTapered
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcExtrudedAreaSolidTapered* l, TopoDS_Shape& shape) {
+ const double height = l->Depth() * getValue(GV_LENGTH_UNIT);
+ if (height < getValue(GV_PRECISION)) {
+ Logger::Message(Logger::LOG_ERROR, "Non-positive extrusion height encountered for:", l);
+ return false;
+ }
+
+ TopoDS_Shape face1, face2;
+ if (!convert_face(l->SweptArea(), face1)) return false;
+ if (!convert_face(l->EndSweptArea(), face2)) return false;
+
+ gp_Trsf trsf;
+ bool has_position = true;
+#ifdef SCHEMA_IfcSweptAreaSolid_Position_IS_OPTIONAL
+ has_position = l->Position() != nullptr;
+#endif
+ if (has_position) {
+ IfcGeom::Kernel::convert(l->Position(), trsf);
+ }
+
+ gp_Dir dir;
+ convert(l->ExtrudedDirection(), dir);
+
+ gp_Trsf end_profile;
+ end_profile.SetTranslation(height * dir);
+
+ TopoDS_Edge spine_edge = BRepBuilderAPI_MakeEdge(gp_Pnt(), gp_Pnt((height * dir).XYZ())).Edge();
+ TopoDS_Wire wire = BRepBuilderAPI_MakeWire(spine_edge).Wire();
+
+ shape.Nullify();
+
+ TopExp_Explorer exp1(face1, TopAbs_WIRE);
+ TopExp_Explorer exp2(face2, TopAbs_WIRE);
+
+ TopoDS_Vertex v1, v2;
+ TopExp::Vertices(wire, v1, v2);
+
+ TopoDS_Shape shell;
+ TopoDS_Compound compound;
+ BRep_Builder compound_builder;
+
+ for (; exp1.More() && exp2.More(); exp1.Next(), exp2.Next()) {
+ const TopoDS_Wire& w1 = TopoDS::Wire(exp1.Current());
+ const TopoDS_Wire& w2 = TopoDS::Wire(exp2.Current());
+
+ BRepOffsetAPI_MakePipeShell builder(wire);
+ builder.Add(w1, v1);
+ builder.Add(w2.Moved(end_profile), v2);
+
+ TopoDS_Shape result = builder.Shape();
+
+ TopTools_ListOfShape li;
+ shape_to_face_list(result, li);
+ li.Append(BRepBuilderAPI_MakeFace(w1).Face().Reversed());
+ li.Append(BRepBuilderAPI_MakeFace(w2).Face().Moved(end_profile));
+
+ create_solid_from_faces(li, result, true);
+
+ // @todo ugly hack
+
+ // The reason for this distinction is that at this point of the loop we're not sure anymore
+ // whether this was constructed from an inner or outer bound. So rather than iterating over
+ // wires of `face1` and `face2` we should iterate over the faces and then properly check with
+ // BRepTools::OuterBound().
+ // Currently this distinction happens based on profile type which is not robust and probably
+ // not complete.
+ if (shell.IsNull()) {
+ shell = result;
+ } else if (l->SweptArea()->declaration().is(IfcSchema::IfcCircleHollowProfileDef::Class()) ||
+ l->SweptArea()->declaration().is(IfcSchema::IfcRectangleHollowProfileDef::Class()) ||
+ l->SweptArea()->declaration().is(IfcSchema::IfcArbitraryProfileDefWithVoids::Class()))
+ {
+ // @todo properly check for failure and all.
+ shell = BRepAlgoAPI_Cut(shell, result).Shape();
+ } else {
+ if (compound.IsNull()) {
+ compound_builder.MakeCompound(compound);
+ compound_builder.Add(compound, shell);
+ }
+ compound_builder.Add(compound, result);
+ }
+ }
+
+ if (!compound.IsNull()) {
+ shell = compound;
+ }
+
+ shape = shell;
+
+ if (exp1.More() != exp2.More()) {
+ Logger::Message(Logger::LOG_ERROR, "Inconsistent profiles encountered for:", l);
+ }
+
+ if (has_position && !shape.IsNull()) {
+ // IfcSweptAreaSolid.Position (trsf) is an IfcAxis2Placement3D
+ // and therefore has a unit scale factor
+ shape.Move(trsf);
+ }
+
+ return !shape.IsNull();
+}
+#endif
diff --git a/src/ifcgeom/IfcFace.cpp b/src/ifcgeom/IfcFace.cpp
new file mode 100644
index 0000000000..1c615d032d
--- /dev/null
+++ b/src/ifcgeom/IfcFace.cpp
@@ -0,0 +1,309 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../ifcgeom/IfcGeom.h"
+
+#include "../ifcgeom_schema_agnostic/face_definition.h"
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcFace* l, TopoDS_Shape& result) {
+ IfcSchema::IfcFaceBound::list::ptr bounds = l->Bounds();
+
+ util::face_definition fd;
+
+ const bool is_face_surface = l->declaration().is(IfcSchema::IfcFaceSurface::Class());
+
+ if (is_face_surface) {
+ IfcSchema::IfcFaceSurface* fs = (IfcSchema::IfcFaceSurface*) l;
+ fs->FaceSurface();
+ // FIXME: Surfaces are interpreted as a TopoDS_Shape
+ TopoDS_Shape surface_shape;
+ if (!convert_shape(fs->FaceSurface(), surface_shape)) return false;
+
+ // FIXME: Assert this obtaines the only face
+ TopExp_Explorer exp(surface_shape, TopAbs_FACE);
+ if (!exp.More()) return false;
+
+ TopoDS_Face surface = TopoDS::Face(exp.Current());
+ fd.surface() = BRep_Tool::Surface(surface);
+ }
+
+ const int num_bounds = bounds->size();
+ int num_outer_bounds = 0;
+
+ for (IfcSchema::IfcFaceBound::list::it it = bounds->begin(); it != bounds->end(); ++it) {
+ IfcSchema::IfcFaceBound* bound = *it;
+ if (bound->declaration().is(IfcSchema::IfcFaceOuterBound::Class())) num_outer_bounds ++;
+ }
+
+ // The number of outer bounds should be one according to the schema. Also Open Cascade
+ // expects this, but it is not strictly checked. Regardless, if the number is greater,
+ // the face will still be processed as long as there are no holes. A compound of faces
+ // is returned in that case.
+ if (num_bounds > 1 && num_outer_bounds > 1 && num_bounds != num_outer_bounds) {
+ Logger::Message(Logger::LOG_ERROR, "Invalid configuration of boundaries for:", l);
+ return false;
+ }
+
+ if (num_outer_bounds > 1) {
+ Logger::Message(Logger::LOG_WARNING, "Multiple outer boundaries for:", l);
+ fd.all_outer() = true;
+ }
+
+ TopTools_DataMapOfShapeInteger wire_senses;
+
+ for (int process_interior = 0; process_interior <= 1; ++process_interior) {
+ for (IfcSchema::IfcFaceBound::list::it it = bounds->begin(); it != bounds->end(); ++it) {
+ IfcSchema::IfcFaceBound* bound = *it;
+ IfcSchema::IfcLoop* loop = bound->Bound();
+
+ bool same_sense = bound->Orientation();
+ const bool is_interior =
+ !bound->declaration().is(IfcSchema::IfcFaceOuterBound::Class()) &&
+ (num_bounds > 1) &&
+ (num_outer_bounds < num_bounds);
+
+ // The exterior face boundary is processed first
+ if (is_interior == !process_interior) continue;
+
+ TopoDS_Wire wire;
+ if (faceset_helper_ && loop->as()) {
+ if (!faceset_helper_->wire(loop->as(), wire)) {
+ Logger::Message(Logger::LOG_WARNING, "Face boundary loop not included", loop);
+ continue;
+ }
+ } else if (!convert_wire(loop, wire)) {
+ Logger::Message(Logger::LOG_ERROR, "Failed to process face boundary loop", loop);
+ return false;
+ }
+
+ if (!same_sense) {
+ wire.Reverse();
+ }
+
+ wire_senses.Bind(wire.Oriented(TopAbs_FORWARD), same_sense ? TopAbs_FORWARD : TopAbs_REVERSED);
+
+ fd.wires().emplace_back(wire);
+ }
+ }
+
+ if (fd.wires().empty()) {
+ Logger::Warning("Face with no boundaries", l);
+ return false;
+ }
+
+ if (fd.surface().IsNull()) {
+ // Use the first wire to find a plane manually for polygonal wires
+ const TopoDS_Wire& wire = fd.wires().front();
+ if (util::is_polyhedron(wire)) {
+ TopExp_Explorer exp(wire, TopAbs_EDGE);
+ int count = 0;
+ TopoDS_Edge edges[2];
+ for (; exp.More(); exp.Next(), count++) {
+ if (count < 2) {
+ edges[count] = TopoDS::Edge(exp.Current());
+ }
+ }
+
+ if (count == 3) {
+ // Help Open Cascade by finding the plane more efficiently
+ double _, __;
+ Handle(Geom_Line) c1 = Handle(Geom_Line)::DownCast(BRep_Tool::Curve(edges[0], _, __));
+ Handle(Geom_Line) c2 = Handle(Geom_Line)::DownCast(BRep_Tool::Curve(edges[1], _, __));
+
+ const gp_Vec ab = c1->Position().Direction();
+ const gp_Vec ac = c2->Position().Direction();
+ const gp_Vec cross = ab.Crossed(ac);
+
+ if (cross.SquareMagnitude() > ALMOST_ZERO) {
+ const gp_Dir n = cross;
+ fd.surface() = new Geom_Plane(c1->Position().Location(), n);
+ }
+ } else {
+ gp_Pln pln;
+ if (approximate_plane_through_wire(wire, pln)) {
+ fd.surface() = new Geom_Plane(pln);
+ }
+ }
+ }
+ }
+
+ if (fd.surface().IsNull()) {
+ // BRepLib_FindSurface is used in case no surface is found or provided
+
+ const TopoDS_Wire& wire = fd.wires().front();
+
+ BRepLib_FindSurface fs(wire, getValue(GV_PRECISION), true, true);
+ if (fs.Found()) {
+ fd.surface() = fs.Surface();
+ ShapeFix_ShapeTolerance ftol;
+ ftol.SetTolerance(wire, fs.ToleranceReached(), TopAbs_WIRE);
+ }
+ }
+
+ TopTools_ListOfShape face_list;
+
+ if (fd.surface().IsNull()) {
+ // The set of wires is triangulated in case no surface can be found
+ Logger::Message(Logger::LOG_WARNING, "Triangulating face boundaries for face", l);
+
+ if (fd.all_outer()) {
+ for (const auto& w : fd.wires()) {
+ TopTools_ListOfShape fl;
+ triangulate_wire({ w }, fl);
+ face_list.Append(fl);
+ }
+ } else {
+ triangulate_wire(fd.wires(), face_list);
+ }
+ } else if (!fd.all_outer()) {
+ BRepBuilderAPI_MakeFace mf(fd.surface(), fd.outer_wire());
+ TopoDS_Face f = mf.Face();
+
+ if (mf.IsDone()) {
+ if (std::distance(fd.inner_wires().first, fd.inner_wires().second)) {
+ mf.Init(f);
+
+ for (auto it = fd.inner_wires().first; it != fd.inner_wires().second; ++it) {
+ mf.Add(*it);
+ }
+
+ face_list.Append(mf.Face());
+ } else {
+ face_list.Append(f);
+ }
+ }
+ } else {
+ for (const auto& w : fd.wires()) {
+ BRepBuilderAPI_MakeFace mf(fd.surface(), w);
+ if (mf.IsDone()) {
+ face_list.Append(mf.Face());
+ }
+ }
+ }
+
+ if (!fd.surface().IsNull()) {
+ // Some fixes for orientation and p-curves. If we have no surface, it
+ // means the face has been triangulated in which case none of these
+ // fixes are necessary.
+
+ if (fd.surface()->DynamicType() != STANDARD_TYPE(Geom_Plane)) {
+ // In case of (non-planar) face surface, p-curves need to be computed.
+ // For planar faces, Open Cascade generates p-curves on the fly.
+
+ for (TopTools_ListIteratorOfListOfShape it(face_list); it.More(); it.Next()) {
+ ShapeFix_Shape sfs(it.Value());
+
+ Handle(ShapeExtend_MsgRegistrator) msg;
+ msg = new ShapeExtend_MsgRegistrator;
+ sfs.SetMsgRegistrator(msg);
+
+ sfs.Perform();
+ it.Value() = sfs.Shape();
+
+ ShapeExtend_DataMapIteratorOfDataMapOfShapeListOfMsg jt(msg->MapShape());
+ for (; jt.More(); jt.Next()) {
+ Message_ListIteratorOfListOfMsg kt(jt.Value());
+ for (; kt.More(); kt.Next()) {
+ char* c = new char[kt.Value().Value().LengthOfCString() + 1];
+ kt.Value().Value().ToUTF8CString(c);
+ Logger::Notice(c, l);
+ delete[] c;
+ }
+ }
+ }
+ }
+
+ for (TopTools_ListIteratorOfListOfShape it(face_list); it.More(); it.Next()) {
+ const TopoDS_Face& face = TopoDS::Face(it.Value());
+
+ ShapeFix_Face sfs(TopoDS::Face(face));
+ TopTools_DataMapOfShapeListOfShape wire_map;
+ sfs.FixOrientation(wire_map);
+
+ TopoDS_Iterator jt(face, false);
+ for (; jt.More(); jt.Next()) {
+ const TopoDS_Wire& w = TopoDS::Wire(jt.Value());
+ // tfk: @todo if wire_map contains w, I would assume wire_senses also contains w,
+ // this is not the case in github issue #405.
+ if (wire_map.IsBound(w) && wire_senses.IsBound(w)) {
+ const TopTools_ListOfShape& shapes = wire_map.Find(w);
+ TopTools_ListIteratorOfListOfShape kt(shapes);
+ for (; kt.More(); kt.Next()) {
+ // Apparently the wire got reversed, so register it with opposite orientation in the map
+ wire_senses.Bind(kt.Value(), wire_senses.Find(w) == TopAbs_FORWARD ? TopAbs_REVERSED : TopAbs_FORWARD);
+ }
+ }
+ }
+
+ it.Value() = sfs.Face();
+ }
+
+ for (TopTools_ListIteratorOfListOfShape it(face_list); it.More(); it.Next()) {
+ TopoDS_Face& face = TopoDS::Face(it.Value());
+
+ bool all_reversed = true;
+ TopoDS_Iterator jt(face, false);
+ for (; jt.More(); jt.Next()) {
+ const TopoDS_Wire& w = TopoDS::Wire(jt.Value());
+ if (!wire_senses.IsBound(w.Oriented(TopAbs_FORWARD)) || (w.Orientation() == wire_senses.Find(w.Oriented(TopAbs_FORWARD)))) {
+ all_reversed = false;
+ }
+ }
+
+ if (all_reversed) {
+ face.Reverse();
+ }
+ }
+ }
+
+ if (face_list.Extent() > 1) {
+ TopoDS_Compound compound;
+ BRep_Builder builder;
+ builder.MakeCompound(compound);
+ for (TopTools_ListIteratorOfListOfShape it(face_list); it.More(); it.Next()) {
+ TopoDS_Face& face = TopoDS::Face(it.Value());
+ builder.Add(compound, face);
+ }
+ result = compound;
+ } else {
+ result = face_list.First();
+ }
+
+ return true;
+}
diff --git a/src/ifcgeom/IfcFaceBasedSurfaceModel.cpp b/src/ifcgeom/IfcFaceBasedSurfaceModel.cpp
new file mode 100644
index 0000000000..760a860d6f
--- /dev/null
+++ b/src/ifcgeom/IfcFaceBasedSurfaceModel.cpp
@@ -0,0 +1,38 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include "../ifcgeom/IfcGeom.h"
+#include
+
+#define Kernel MAKE_TYPE_NAME(Kernel)
+
+bool IfcGeom::Kernel::convert(const IfcSchema::IfcFaceBasedSurfaceModel* l, IfcRepresentationShapeItems& shapes) {
+ bool part_success = false;
+ IfcSchema::IfcConnectedFaceSet::list::ptr facesets = l->FbsmFaces();
+ auto collective_style = get_style(l);
+ for( IfcSchema::IfcConnectedFaceSet::list::it it = facesets->begin(); it != facesets->end(); ++ it ) {
+ TopoDS_Shape s;
+ auto shell_style = get_style(*it);
+ if (convert_shape(*it,s)) {
+ shapes.push_back(IfcRepresentationShapeItem(l->data().id(), s, shell_style ? shell_style : collective_style));
+ part_success |= true;
+ }
+ }
+ return part_success;
+}
diff --git a/src/ifcgeom/IfcGeomFunctions.cpp b/src/ifcgeom/IfcGeom.cpp
similarity index 85%
rename from src/ifcgeom/IfcGeomFunctions.cpp
rename to src/ifcgeom/IfcGeom.cpp
index f156692b0d..d2959f2ff7 100644
--- a/src/ifcgeom/IfcGeomFunctions.cpp
+++ b/src/ifcgeom/IfcGeom.cpp
@@ -24,7 +24,6 @@
********************************************************************************/
#include
-#include
#include
#include
@@ -34,32 +33,19 @@
#include
#include
#include
-#include
-#include
-#include
-#include