mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 06:21:40 +00:00
Merge remote-tracking branch 'origin/v0.8.0' into test-ci
This commit is contained in:
+18
-14
@@ -50,6 +50,7 @@ jobs:
|
|||||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
|
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
|
||||||
pip install src/bcf --no-deps
|
pip install src/bcf --no-deps
|
||||||
pip install git+https://github.com/zdhoward/aud
|
pip install git+https://github.com/zdhoward/aud
|
||||||
|
pip install pytest-xdist==3.8.0
|
||||||
|
|
||||||
- name: Install C++ dependencies
|
- name: Install C++ dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -120,20 +121,23 @@ jobs:
|
|||||||
sudo make -j $(nproc)
|
sudo make -j $(nproc)
|
||||||
sudo make install
|
sudo make install
|
||||||
|
|
||||||
- name: Run IfcConvert on Sample files
|
# - name: Run IfcConvert on Sample files
|
||||||
run: |
|
# run: |
|
||||||
(find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
|
# (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
|
||||||
echo $i | tee -a log; \
|
# echo $i | tee -a log; \
|
||||||
timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
|
# timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
|
||||||
echo $i $? >> statuses; \
|
# echo $i $? >> statuses; \
|
||||||
done) || true
|
# done) || true
|
||||||
echo Failed
|
# echo Failed
|
||||||
grep -v 0$ statuses
|
# grep -v 0$ statuses
|
||||||
grep -v 0$ statuses | wc -l
|
# grep -v 0$ statuses | wc -l
|
||||||
echo Succeeded
|
# echo Succeeded
|
||||||
grep 0$ statuses
|
# grep 0$ statuses
|
||||||
grep 0$ statuses | wc -l
|
# grep 0$ statuses | wc -l
|
||||||
|
|
||||||
|
- name: Run IfcConvert on Sample file
|
||||||
|
run: |
|
||||||
|
IfcConvert test/input/acad2010_walls.ifc test/input/acad2010_walls.obj
|
||||||
|
|
||||||
- name: Test ifcopenshell-python
|
- name: Test ifcopenshell-python
|
||||||
run: |
|
run: |
|
||||||
@@ -143,7 +147,7 @@ jobs:
|
|||||||
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
||||||
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
|
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
|
||||||
ERROR=0
|
ERROR=0
|
||||||
make test || ERROR=1
|
make test-parallel || ERROR=1
|
||||||
cd ../bcf && make test || ERROR=1
|
cd ../bcf && make test || ERROR=1
|
||||||
pip install requests
|
pip install requests
|
||||||
cd ../bsdd && make test || ERROR=1
|
cd ../bsdd && make test || ERROR=1
|
||||||
|
|||||||
+4
-1
@@ -109,4 +109,7 @@ src/bonsai/bonsai/bim/schema/Brick.ttl
|
|||||||
bonsaiDecoratorForLoads.code-workspace
|
bonsaiDecoratorForLoads.code-workspace
|
||||||
dev_environment.bat
|
dev_environment.bat
|
||||||
|
|
||||||
.pixi/
|
.pixi/
|
||||||
|
|
||||||
|
src/ifcopenshell-python/ifcopenshell/express/*.exp
|
||||||
|
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||||
|
|||||||
@@ -37,25 +37,25 @@ Contents
|
|||||||
|
|
||||||
| Name | Description | License | Service |
|
| Name | Description | License | Service |
|
||||||
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
|
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
|
||||||
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
|
| [bcf](https://docs.ifcopenshell.org/bcf.html) | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
|
||||||
| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
| [bonsai](https://docs.ifcopenshell.org/bonsai.html) | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||||
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
|
| [bsdd](https://docs.ifcopenshell.org/bsdd.html) | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
|
||||||
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
|
| [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
|
||||||
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
|
| [ifc4d](https://docs.ifcopenshell.org/ifc4d.html) | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
|
||||||
| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
|
| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
|
||||||
| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
|
| [ifcbimtester](https://docs.ifcopenshell.org/bimtester.html) | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
|
||||||
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
|
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
|
||||||
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
|
| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
|
||||||
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
|
| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
|
||||||
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
|
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
|
||||||
| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
|
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
|
||||||
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||||
| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
||||||
| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
||||||
| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||||
| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||||
| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||||
| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||||
|
|
||||||
The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
||||||
|
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ if(WASM_BUILD)
|
|||||||
else()
|
else()
|
||||||
# @todo review this, shouldn't this be all possible header-only now?
|
# @todo review this, shouldn't this be all possible header-only now?
|
||||||
# ... or rewritten using C++17 features?
|
# ... or rewritten using C++17 features?
|
||||||
set(BOOST_COMPONENTS system program_options regex thread date_time)
|
set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(USE_MMAP)
|
if(USE_MMAP)
|
||||||
@@ -1044,11 +1044,7 @@ if(BUILD_IFCGEOM)
|
|||||||
foreach(schema ${SCHEMA_VERSIONS})
|
foreach(schema ${SCHEMA_VERSIONS})
|
||||||
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema})
|
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema})
|
||||||
endforeach()
|
endforeach()
|
||||||
if(WASM_BUILD)
|
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
|
||||||
else()
|
|
||||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
|
||||||
endif()
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||||
@@ -1177,7 +1173,10 @@ if(BUILD_IFCGEOM)
|
|||||||
|
|
||||||
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
|
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
|
||||||
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||||
target_link_libraries(geometry_mapping_ifc${schema} IfcParse IfcGeom)
|
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
|
||||||
|
if (NOT BUILD_SHARED_LIBS)
|
||||||
|
target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
|
||||||
|
endif()
|
||||||
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
|
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -154,7 +154,7 @@ SWIG_VERSION = "4.1.0"
|
|||||||
OPENCOLLADA_VERSION = "v1.6.68"
|
OPENCOLLADA_VERSION = "v1.6.68"
|
||||||
HDF5_VERSION = "1.13.1"
|
HDF5_VERSION = "1.13.1"
|
||||||
|
|
||||||
GMP_VERSION = "6.2.1"
|
GMP_VERSION = "6.3.0"
|
||||||
MPFR_VERSION = "3.1.6" # latest is 4.1.0
|
MPFR_VERSION = "3.1.6" # latest is 4.1.0
|
||||||
CGAL_VERSION = "v5.6.3"
|
CGAL_VERSION = "v5.6.3"
|
||||||
USD_VERSION = "23.05"
|
USD_VERSION = "23.05"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 215 B |
Binary file not shown.
|
After Width: | Height: | Size: 217 B |
@@ -13,10 +13,10 @@ DATA;
|
|||||||
#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','Whether or not this drawing can be referenced in other drawings.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||||
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||||
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||||
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||||
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||||
#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||||
#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from bonsai.bim.ifc import IfcStore
|
|||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
|
from math import radians
|
||||||
|
|
||||||
|
|
||||||
class IfcExporter:
|
class IfcExporter:
|
||||||
@@ -104,7 +105,16 @@ class IfcExporter:
|
|||||||
|
|
||||||
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||||
if tool.Geometry.is_scaled(obj):
|
# Handle camera scales specially
|
||||||
|
if obj.type == "CAMERA":
|
||||||
|
# Check if this is a reflected ceiling plan camera
|
||||||
|
camera = tool.Ifc.get_entity(obj)
|
||||||
|
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
|
||||||
|
# Ensure reflected ceiling cameras have the correct scale
|
||||||
|
if obj.scale != (-1, -1, -1):
|
||||||
|
obj.scale = (-1, -1, -1)
|
||||||
|
# Skip all other scale handling for cameras
|
||||||
|
elif tool.Geometry.is_scaled(obj):
|
||||||
bpy.ops.bim.update_representation(obj=obj.name)
|
bpy.ops.bim.update_representation(obj=obj.name)
|
||||||
# update_representation might not apply scale if the object has openings
|
# update_representation might not apply scale if the object has openings
|
||||||
# reset it, so let user know that the scale wasn't saved.
|
# reset it, so let user know that the scale wasn't saved.
|
||||||
|
|||||||
@@ -222,8 +222,6 @@ def refresh_ui_data():
|
|||||||
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
|
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
|
||||||
ifc_file.clear_cache()
|
ifc_file.clear_cache()
|
||||||
|
|
||||||
props = tool.Drawing.get_document_props()
|
|
||||||
props.should_draw_decorations = props.should_draw_decorations
|
|
||||||
if tool.Web.get_web_props().is_connected:
|
if tool.Web.get_web_props().is_connected:
|
||||||
tool.Web.send_webui_data()
|
tool.Web.send_webui_data()
|
||||||
|
|
||||||
|
|||||||
@@ -952,6 +952,9 @@ class IfcImporter:
|
|||||||
if not props.ifc_file:
|
if not props.ifc_file:
|
||||||
props.ifc_file = self.ifc_import_settings.input_file
|
props.ifc_file = self.ifc_import_settings.input_file
|
||||||
self.file = tool.Ifc.get()
|
self.file = tool.Ifc.get()
|
||||||
|
# IFC4 Reference View shall have no booleans https://github.com/BuildingSMART/IFC4-CV/issues/14
|
||||||
|
if self.file.schema == "IFC4" and "ReferenceView" in str(self.file.header.file_description.description):
|
||||||
|
self.ifc_import_settings.void_limit = 0
|
||||||
|
|
||||||
def calculate_unit_scale(self):
|
def calculate_unit_scale(self):
|
||||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
|
aggregates_to_check = set()
|
||||||
|
|
||||||
|
# First pass: unassign all parts and track their aggregates
|
||||||
for obj in tool.Blender.get_selected_objects():
|
for obj in tool.Blender.get_selected_objects():
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if not element:
|
if not element:
|
||||||
@@ -101,6 +104,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||||
if not aggregate:
|
if not aggregate:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Track this aggregate for later checking
|
||||||
|
aggregates_to_check.add(aggregate)
|
||||||
|
|
||||||
core.unassign_object(
|
core.unassign_object(
|
||||||
tool.Ifc,
|
tool.Ifc,
|
||||||
tool.Aggregate,
|
tool.Aggregate,
|
||||||
@@ -116,6 +123,29 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
pset = tool.Ifc.get().by_id(pset["id"])
|
pset = tool.Ifc.get().by_id(pset["id"])
|
||||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||||
|
|
||||||
|
# Second pass: delete aggregates that now have no parts
|
||||||
|
deleted_aggregates = []
|
||||||
|
for aggregate in aggregates_to_check:
|
||||||
|
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||||
|
if len(related_objects) == 0:
|
||||||
|
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||||
|
deleted_aggregates.append(aggregate_name)
|
||||||
|
|
||||||
|
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||||
|
if aggregate_obj:
|
||||||
|
ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate)
|
||||||
|
bpy.data.objects.remove(aggregate_obj, do_unlink=True)
|
||||||
|
|
||||||
|
# Show info message if aggregates were deleted
|
||||||
|
if deleted_aggregates:
|
||||||
|
if len(deleted_aggregates) == 1:
|
||||||
|
self.report(
|
||||||
|
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||||
|
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||||
|
|
||||||
|
|
||||||
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
|
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
|
||||||
"""Enable editing aggregation relationship"""
|
"""Enable editing aggregation relationship"""
|
||||||
|
|||||||
@@ -282,7 +282,11 @@ class BIM_PT_classification_references(Panel, ReferenceUI):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return bool((obj := context.active_object) and tool.Ifc.get_entity(obj))
|
return (
|
||||||
|
(obj := tool.Blender.get_active_object())
|
||||||
|
and (element := tool.Ifc.get_entity(obj))
|
||||||
|
and element.is_a("IfcObjectDefinition")
|
||||||
|
)
|
||||||
|
|
||||||
def get_object_name(self, context: bpy.types.Context) -> str:
|
def get_object_name(self, context: bpy.types.Context) -> str:
|
||||||
assert (obj := context.active_object)
|
assert (obj := context.active_object)
|
||||||
|
|||||||
@@ -818,7 +818,12 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.merge_identical_objects"
|
bl_idname = "bim.merge_identical_objects"
|
||||||
bl_label = "Merge Identical Objects"
|
bl_label = "Merge Identical Objects"
|
||||||
bl_description = "For materials currently only IfcMaterials are supported"
|
bl_description = (
|
||||||
|
"Merge identical IFC objects (that match all attributes).\n"
|
||||||
|
"\n"
|
||||||
|
"SHIFT + CLICK to merge by name/identification attribute only.\n"
|
||||||
|
"Merges names with number suffix, as well (ex: foo, foo.001, foo.002)\n"
|
||||||
|
)
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||||
@@ -826,18 +831,36 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
by_name_or_identification_only: bpy.props.BoolProperty(
|
||||||
|
name="By Name/Identification Only",
|
||||||
|
description="Merge based only on Name or Identification attribute, ignoring other properties",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
object_type: tool.Debug.PurgeMergeObjectType
|
object_type: tool.Debug.PurgeMergeObjectType
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# Check if shift key is pressed
|
||||||
|
if event.shift:
|
||||||
|
self.by_name_or_identification_only = True
|
||||||
|
else:
|
||||||
|
self.by_name_or_identification_only = False
|
||||||
|
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
object_type: str = self.object_type
|
object_type: str = self.object_type
|
||||||
if object_type in ("PROFILE", "TYPE"):
|
if object_type in ("PROFILE", "TYPE"):
|
||||||
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
|
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
merged_data = tool.Debug.merge_identical_objects(object_type)
|
merged_data = tool.Debug.merge_identical_objects(
|
||||||
|
object_type, by_name_or_identification_only=self.by_name_or_identification_only
|
||||||
|
)
|
||||||
plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
|
plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
|
||||||
if merged_data:
|
if merged_data:
|
||||||
|
merge_mode = " by name/identification" if self.by_name_or_identification_only else ""
|
||||||
for element_type, element_names in merged_data.items():
|
for element_type, element_names in merged_data.items():
|
||||||
print(f"- {element_type}:")
|
print(f"- {element_type}:")
|
||||||
for name in element_names:
|
for name in element_names:
|
||||||
@@ -846,7 +869,8 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
merged = sum(len(v) for v in merged_data.values())
|
merged = sum(len(v) for v in merged_data.values())
|
||||||
|
|
||||||
msg = " See system console for details." if merged else ""
|
msg = " See system console for details." if merged else ""
|
||||||
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}")
|
merge_mode = " (by name/identification)" if self.by_name_or_identification_only else ""
|
||||||
|
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged{merge_mode}.{msg}")
|
||||||
|
|
||||||
if merged == 0:
|
if merged == 0:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ classes = (
|
|||||||
operator.EnableEditingAssignedProduct,
|
operator.EnableEditingAssignedProduct,
|
||||||
operator.EnableEditingElementFilter,
|
operator.EnableEditingElementFilter,
|
||||||
operator.EnableEditingText,
|
operator.EnableEditingText,
|
||||||
|
operator.ExcludeAnnotation,
|
||||||
operator.ExpandSheet,
|
operator.ExpandSheet,
|
||||||
operator.LoadDrawings,
|
operator.LoadDrawings,
|
||||||
operator.LoadReferences,
|
operator.LoadReferences,
|
||||||
|
|||||||
@@ -235,13 +235,18 @@ class DecoratorData:
|
|||||||
cut_cache = {}
|
cut_cache = {}
|
||||||
slice_cache = {}
|
slice_cache = {}
|
||||||
fill_cache = {}
|
fill_cache = {}
|
||||||
|
camera_location_checksum = None
|
||||||
|
camera_rotation_checksum = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear_cache(cls):
|
||||||
|
cls.cut_cache = {}
|
||||||
|
cls.layerset_cache = {}
|
||||||
|
cls.fill_cache = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, handler):
|
def load(cls, handler):
|
||||||
cls.is_loaded = True
|
cls.is_loaded = True
|
||||||
cls.cut_cache = {}
|
|
||||||
cls.layerset_cache = {}
|
|
||||||
cls.fill_cache = {}
|
|
||||||
|
|
||||||
text = {}
|
text = {}
|
||||||
dimension = {}
|
dimension = {}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import blf
|
|||||||
import math
|
import math
|
||||||
import bmesh
|
import bmesh
|
||||||
import shapely
|
import shapely
|
||||||
|
import numpy as np
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
import ifcopenshell.util.representation
|
import ifcopenshell.util.representation
|
||||||
@@ -1625,6 +1626,7 @@ class CutDecorator:
|
|||||||
if cls.installed:
|
if cls.installed:
|
||||||
cls.uninstall()
|
cls.uninstall()
|
||||||
handler = cls()
|
handler = cls()
|
||||||
|
handler.cache_camera_matrix()
|
||||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1727,6 +1729,30 @@ class CutDecorator:
|
|||||||
shader.uniform_float("color", color)
|
shader.uniform_float("color", color)
|
||||||
batch.draw(shader)
|
batch.draw(shader)
|
||||||
|
|
||||||
|
def cache_camera_matrix(self):
|
||||||
|
obj = bpy.context.scene.camera
|
||||||
|
DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
|
||||||
|
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
|
||||||
|
|
||||||
|
def is_camera_moved(self):
|
||||||
|
if not DecoratorData.camera_location_checksum:
|
||||||
|
self.cache_camera_matrix()
|
||||||
|
return True # Let's be conservative
|
||||||
|
obj = bpy.context.scene.camera
|
||||||
|
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
|
||||||
|
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||||
|
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||||
|
self.cache_camera_matrix()
|
||||||
|
return True
|
||||||
|
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
|
||||||
|
rot_real = np.array(obj.matrix_world.to_3x3())
|
||||||
|
rot_dot = np.dot(rot_check, rot_real.T)
|
||||||
|
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||||
|
if angle_rad > 0.0017453292519943296: # 0.1 degrees
|
||||||
|
self.cache_camera_matrix()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||||
has_cut_cache = element.id() in DecoratorData.cut_cache
|
has_cut_cache = element.id() in DecoratorData.cut_cache
|
||||||
has_fill_cache = element.id() in DecoratorData.fill_cache
|
has_fill_cache = element.id() in DecoratorData.fill_cache
|
||||||
@@ -1734,9 +1760,9 @@ class CutDecorator:
|
|||||||
# Currently selected objects must be recalculated as they may be being moved / edited.
|
# Currently selected objects must be recalculated as they may be being moved / edited.
|
||||||
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
||||||
|
|
||||||
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
|
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
|
||||||
self.recalculate_cut(context, obj, element)
|
self.recalculate_cut(context, obj, element)
|
||||||
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
|
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
|
||||||
self.recalculate_fill(context, obj, element)
|
self.recalculate_fill(context, obj, element)
|
||||||
|
|
||||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||||
@@ -1933,8 +1959,7 @@ class DecorationsHandler:
|
|||||||
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
|
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
|
||||||
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
||||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
||||||
if not DecoratorData.is_loaded:
|
DecoratorData.clear_cache()
|
||||||
DecoratorData.load(handler)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def uninstall(cls):
|
def uninstall(cls):
|
||||||
@@ -1961,5 +1986,8 @@ class DecorationsHandler:
|
|||||||
if not DrawingsData.is_loaded:
|
if not DrawingsData.is_loaded:
|
||||||
DrawingsData.load()
|
DrawingsData.load()
|
||||||
|
|
||||||
|
if not DecoratorData.is_loaded:
|
||||||
|
DecoratorData.load(self)
|
||||||
|
|
||||||
for obj, decorator in DecoratorData.data["object_decorators"]:
|
for obj, decorator in DecoratorData.data["object_decorators"]:
|
||||||
decorator.decorate(context, obj)
|
decorator.decorate(context, obj)
|
||||||
|
|||||||
@@ -210,12 +210,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
should_duplicate_annotations=self.should_duplicate_annotations,
|
should_duplicate_annotations=self.should_duplicate_annotations,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Why need to resync active drawing, if it wasn't changed.
|
|
||||||
drawing = props.get_active_drawing()
|
|
||||||
if drawing is None:
|
|
||||||
return
|
|
||||||
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateDrawing(bpy.types.Operator):
|
class CreateDrawing(bpy.types.Operator):
|
||||||
"""Creates/refreshes a .svg drawing
|
"""Creates/refreshes a .svg drawing
|
||||||
@@ -442,6 +436,8 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
|
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
|
||||||
assert (drawing_style := self.cprops.get_active_drawing_style())
|
assert (drawing_style := self.cprops.get_active_drawing_style())
|
||||||
|
|
||||||
|
tool.Blender.sync_render_visibility()
|
||||||
|
|
||||||
if drawing_style.render_type == "DEFAULT":
|
if drawing_style.render_type == "DEFAULT":
|
||||||
bpy.ops.render.render(write_still=True)
|
bpy.ops.render.render(write_still=True)
|
||||||
else:
|
else:
|
||||||
@@ -625,7 +621,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
path.attrib["d"] = d
|
path.attrib["d"] = d
|
||||||
group.append(g)
|
group.append(g)
|
||||||
|
|
||||||
def generate_wall_layers(self, context: bpy.types.Context, root) -> None:
|
def generate_material_layers(self, context: bpy.types.Context, root) -> None:
|
||||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||||
if "projection" in el.get("class", "").split():
|
if "projection" in el.get("class", "").split():
|
||||||
continue
|
continue
|
||||||
@@ -794,8 +790,9 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
edge_bm.to_mesh(edge_mesh)
|
edge_bm.to_mesh(edge_mesh)
|
||||||
edge_bm.free()
|
edge_bm.free()
|
||||||
|
|
||||||
actual_path = svg_path[0:-4] + "0001.svg"
|
freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter")
|
||||||
context.scene.render.filepath = svg_path[0:-4]
|
context.scene.render.filepath = svg_path[0:-4]
|
||||||
|
actual_path = freestyle_svg_exporter.create_path(bpy.context.scene)
|
||||||
bpy.ops.render.render(write_still=False)
|
bpy.ops.render.render(write_still=False)
|
||||||
|
|
||||||
os.replace(actual_path, svg_path)
|
os.replace(actual_path, svg_path)
|
||||||
@@ -839,6 +836,8 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
if tool.Drawing.is_camera_orthographic():
|
if tool.Drawing.is_camera_orthographic():
|
||||||
self.generate_bisect_linework(context, root)
|
self.generate_bisect_linework(context, root)
|
||||||
|
if self.cprops.generate_material_layers:
|
||||||
|
self.generate_material_layers(context, root)
|
||||||
self.merge_linework_and_add_metadata(root)
|
self.merge_linework_and_add_metadata(root)
|
||||||
self.move_elements_to_top(root)
|
self.move_elements_to_top(root)
|
||||||
|
|
||||||
@@ -936,12 +935,14 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
if self.cprops.cut_mode == "BISECT":
|
if self.cprops.cut_mode == "BISECT":
|
||||||
self.remove_cut_linework(root)
|
self.remove_cut_linework(root)
|
||||||
self.generate_bisect_linework(context, root)
|
self.generate_bisect_linework(context, root)
|
||||||
self.generate_wall_layers(context, root)
|
if self.cprops.generate_material_layers:
|
||||||
|
self.generate_material_layers(context, root)
|
||||||
self.merge_linework_and_add_metadata(root)
|
self.merge_linework_and_add_metadata(root)
|
||||||
self.move_elements_to_top(root)
|
self.move_elements_to_top(root)
|
||||||
elif self.cprops.cut_mode == "OPENCASCADE":
|
elif self.cprops.cut_mode == "OPENCASCADE":
|
||||||
self.move_projection_to_bottom(root)
|
self.move_projection_to_bottom(root)
|
||||||
self.generate_wall_layers(context, root)
|
if self.cprops.generate_material_layers:
|
||||||
|
self.generate_material_layers(context, root)
|
||||||
self.merge_linework_and_add_metadata(root)
|
self.merge_linework_and_add_metadata(root)
|
||||||
self.move_elements_to_top(root)
|
self.move_elements_to_top(root)
|
||||||
|
|
||||||
@@ -1251,6 +1252,8 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
def get_svg_classes(self, element, layer=None):
|
def get_svg_classes(self, element, layer=None):
|
||||||
classes = [element.is_a()]
|
classes = [element.is_a()]
|
||||||
|
|
||||||
|
# ─── Material ──────────────────────────────────────────────
|
||||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||||
material_name = ""
|
material_name = ""
|
||||||
if material:
|
if material:
|
||||||
@@ -1263,6 +1266,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
else:
|
else:
|
||||||
classes.append("material-null")
|
classes.append("material-null")
|
||||||
|
|
||||||
|
# ─── Layer ─────────────────────────────────────────────────
|
||||||
if layer:
|
if layer:
|
||||||
classes.append(layer.is_a())
|
classes.append(layer.is_a())
|
||||||
layer_material = layer.Material
|
layer_material = layer.Material
|
||||||
@@ -1275,12 +1279,19 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
|
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
|
||||||
classes.append(f"layer-material-category-{layer_material_category}")
|
classes.append(f"layer-material-category-{layer_material_category}")
|
||||||
|
|
||||||
|
# ─── Metadata ──────────────────────────────────────────────
|
||||||
for key in self.metadata:
|
for key in self.metadata:
|
||||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||||
if value:
|
if value:
|
||||||
classes.append(
|
classes.append(
|
||||||
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ─── Target View ───────────────────────────────────────────
|
||||||
|
if getattr(self.cprops, "target_view", None):
|
||||||
|
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
|
||||||
|
classes.append(f"target-view-{target_view_class}")
|
||||||
|
|
||||||
return classes
|
return classes
|
||||||
|
|
||||||
def is_manifold(self, obj) -> bool:
|
def is_manifold(self, obj) -> bool:
|
||||||
@@ -1347,7 +1358,13 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
join_criteria = join_criteria.split(",")
|
join_criteria = join_criteria.split(",")
|
||||||
else:
|
else:
|
||||||
# Drawing convention states that same objects classes with the same material are merged when cut.
|
# Drawing convention states that same objects classes with the same material are merged when cut.
|
||||||
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
|
join_criteria = [
|
||||||
|
"class",
|
||||||
|
"material.Name",
|
||||||
|
"/Pset_.*Common/.Status",
|
||||||
|
"EPset_Status.Status",
|
||||||
|
"EPset_Status.UserDefinedStatus",
|
||||||
|
]
|
||||||
|
|
||||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||||
joined_paths = {}
|
joined_paths = {}
|
||||||
@@ -1476,11 +1493,10 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
joined_paths.setdefault(hash_keys, []).append(el)
|
joined_paths.setdefault(hash_keys, []).append(el)
|
||||||
|
|
||||||
for key, els in joined_paths.items():
|
for key, els in joined_paths.items():
|
||||||
polygons = []
|
queue = []
|
||||||
classes = set()
|
|
||||||
|
|
||||||
for el in els:
|
for el in els:
|
||||||
classes.update(el.attrib["class"].split())
|
classes = set(el.attrib["class"].split())
|
||||||
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
|
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
|
||||||
is_closed_polygon = False
|
is_closed_polygon = False
|
||||||
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
||||||
@@ -1495,31 +1511,30 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
coords.append(coords[0])
|
coords.append(coords[0])
|
||||||
if len(coords) > 2 and coords[0] == coords[-1]:
|
if len(coords) > 2 and coords[0] == coords[-1]:
|
||||||
is_closed_polygon = True
|
is_closed_polygon = True
|
||||||
polygons.append(shapely.Polygon(coords))
|
queue.append((shapely.Polygon(coords), classes))
|
||||||
if is_closed_polygon:
|
if is_closed_polygon:
|
||||||
el.getparent().remove(el)
|
el.getparent().remove(el)
|
||||||
|
|
||||||
try:
|
while queue:
|
||||||
merged_polygons = shapely.ops.unary_union(polygons)
|
polygon, polygon_classes = queue.pop()
|
||||||
except:
|
for polygon2, polygon2_classes in queue[:]:
|
||||||
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
|
try:
|
||||||
merged_polygons = polygons
|
merged_polygon = shapely.union(polygon, polygon2)
|
||||||
|
except:
|
||||||
|
print("Warning. Portions of the merge failed. Please report a bug!", polygon, polygon2)
|
||||||
|
continue
|
||||||
|
if type(merged_polygon) == shapely.Polygon:
|
||||||
|
polygon = merged_polygon
|
||||||
|
polygon_classes.update(polygon2_classes)
|
||||||
|
queue.remove((polygon2, polygon2_classes))
|
||||||
|
|
||||||
if type(merged_polygons) == shapely.MultiPolygon:
|
|
||||||
merged_polygons = merged_polygons.geoms
|
|
||||||
elif type(merged_polygons) == shapely.Polygon:
|
|
||||||
merged_polygons = [merged_polygons]
|
|
||||||
else:
|
|
||||||
merged_polygons = []
|
|
||||||
|
|
||||||
for polygon in merged_polygons:
|
|
||||||
g = etree.Element("g")
|
g = etree.Element("g")
|
||||||
path = etree.SubElement(g, "path")
|
path = etree.SubElement(g, "path")
|
||||||
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
|
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
|
||||||
for interior in polygon.interiors:
|
for interior in polygon.interiors:
|
||||||
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
|
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
|
||||||
path.attrib["d"] = d
|
path.attrib["d"] = d
|
||||||
g.set("class", " ".join(list(classes)))
|
g.set("class", " ".join(list(polygon_classes)))
|
||||||
group.append(g)
|
group.append(g)
|
||||||
|
|
||||||
def drawing_to_model_co(self, x: float, y: float) -> Vector:
|
def drawing_to_model_co(self, x: float, y: float) -> Vector:
|
||||||
@@ -1861,13 +1876,15 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
props = tool.Drawing.get_document_props()
|
props = tool.Drawing.get_document_props()
|
||||||
# Won't be visible in UI anyway.
|
|
||||||
prefs = tool.Blender.get_addon_preferences()
|
|
||||||
if not props.sheets or not prefs.data_dir:
|
|
||||||
return False
|
|
||||||
if not tool.Drawing.get_active_drawing_item():
|
if not tool.Drawing.get_active_drawing_item():
|
||||||
cls.poll_message_set("No drawing selected.")
|
cls.poll_message_set("No drawing selected.")
|
||||||
return False
|
return False
|
||||||
|
if not props.sheets:
|
||||||
|
cls.poll_message_set("No sheets available.")
|
||||||
|
return False
|
||||||
|
if not tool.Blender.get_user_data_dir():
|
||||||
|
cls.poll_message_set("BIM data directory not set.")
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
@@ -2221,6 +2238,7 @@ class ActivateModel(bpy.types.Operator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tool.Blender.reset_object_visibility()
|
tool.Blender.reset_object_visibility()
|
||||||
|
tool.Drawing.hide_all_drawing_collections()
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
bonsai.bim.handler.refresh_ui_data()
|
bonsai.bim.handler.refresh_ui_data()
|
||||||
|
|
||||||
@@ -2317,9 +2335,8 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
|||||||
|
|
||||||
dprops.active_drawing_id = self.drawing
|
dprops.active_drawing_id = self.drawing
|
||||||
dprops.drawing_styles.clear()
|
dprops.drawing_styles.clear()
|
||||||
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
|
bpy.ops.bim.reload_drawing_styles()
|
||||||
bpy.ops.bim.reload_drawing_styles()
|
bpy.ops.bim.activate_drawing_style()
|
||||||
bpy.ops.bim.activate_drawing_style()
|
|
||||||
|
|
||||||
if tool.Drawing.is_camera_orthographic():
|
if tool.Drawing.is_camera_orthographic():
|
||||||
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
|
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
|
||||||
@@ -2332,10 +2349,22 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
|||||||
camera = context.scene.camera
|
camera = context.scene.camera
|
||||||
assert camera
|
assert camera
|
||||||
camera_props = tool.Drawing.get_camera_props(camera)
|
camera_props = tool.Drawing.get_camera_props(camera)
|
||||||
|
# Check if this is a reflected ceiling camera and preserve its scale
|
||||||
|
camera_element = tool.Ifc.get_entity(camera)
|
||||||
|
is_reflected = False
|
||||||
|
if camera_element:
|
||||||
|
is_reflected = (
|
||||||
|
ifcopenshell.util.element.get_pset(camera_element, "EPset_Drawing", "TargetView")
|
||||||
|
== "REFLECTED_PLAN_VIEW"
|
||||||
|
)
|
||||||
|
if is_reflected and camera.scale != (-1, -1, -1):
|
||||||
|
camera.scale = (-1, -1, -1)
|
||||||
|
|
||||||
if camera_props.update_representation(camera.matrix_world):
|
if camera_props.update_representation(camera.matrix_world):
|
||||||
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
|
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
|
||||||
# See 6452 and 6478.
|
# Restore the scale after update if needed
|
||||||
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
if is_reflected:
|
||||||
|
camera.scale = (-1, -1, -1)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -2811,8 +2840,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if not props.schedules:
|
if not props.schedules:
|
||||||
cls.poll_message_set("No schedule selected.")
|
cls.poll_message_set("No schedule selected.")
|
||||||
return False
|
return False
|
||||||
prefs = tool.Blender.get_addon_preferences()
|
if not props.sheets:
|
||||||
return props.schedules and props.sheets and prefs.data_dir
|
cls.poll_message_set("No sheets available.")
|
||||||
|
return False
|
||||||
|
if not tool.Blender.get_user_data_dir():
|
||||||
|
cls.poll_message_set("BIM data directory not set.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
props = tool.Drawing.get_document_props()
|
props = tool.Drawing.get_document_props()
|
||||||
@@ -2879,8 +2913,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if not props.references:
|
if not props.references:
|
||||||
cls.poll_message_set("No reference selected.")
|
cls.poll_message_set("No reference selected.")
|
||||||
return False
|
return False
|
||||||
bim_props = tool.Blender.get_bim_props()
|
if not props.sheets:
|
||||||
return props.references and props.sheets and bim_props.data_dir
|
cls.poll_message_set("No sheets available.")
|
||||||
|
return False
|
||||||
|
if not tool.Blender.get_user_data_dir():
|
||||||
|
cls.poll_message_set("BIM data directory not set.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
props = tool.Drawing.get_document_props()
|
props = tool.Drawing.get_document_props()
|
||||||
@@ -3729,3 +3768,19 @@ class OpenDocumentationWebUi(bpy.types.Operator):
|
|||||||
else:
|
else:
|
||||||
bpy.ops.bim.open_web_browser(page="documentation")
|
bpy.ops.bim.open_web_browser(page="documentation")
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
bl_idname = "bim.exclude_annotation"
|
||||||
|
bl_label = "Exclude Annotation"
|
||||||
|
bl_description = "Excludes the automatic annotation reference from the drawing"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
def _execute(self, context):
|
||||||
|
if not (obj := bpy.context.scene.camera) or not (drawing := tool.Ifc.get_entity(obj)):
|
||||||
|
return
|
||||||
|
for obj in tool.Blender.get_selected_objects(include_active=False):
|
||||||
|
if (element := tool.Ifc.get_entity(obj)) and tool.Drawing.is_auto_annotation(element):
|
||||||
|
if referenced_element := tool.Drawing.get_annotation_element(element):
|
||||||
|
tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing)
|
||||||
|
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
|
||||||
|
|||||||
@@ -498,6 +498,9 @@ class BIMCameraProperties(PropertyGroup):
|
|||||||
name="Linework Mode",
|
name="Linework Mode",
|
||||||
update=get_update_layer_callback("linework_mode", "LineworkMode"),
|
update=get_update_layer_callback("linework_mode", "LineworkMode"),
|
||||||
)
|
)
|
||||||
|
generate_material_layers: bpy.props.BoolProperty(
|
||||||
|
name="Generate Material Layers", description="Generate material layer linework in drawings", default=True
|
||||||
|
)
|
||||||
fill_mode: EnumProperty(
|
fill_mode: EnumProperty(
|
||||||
items=[
|
items=[
|
||||||
("NONE", "None", "Disable filling areas seen in projection"),
|
("NONE", "None", "Disable filling areas seen in projection"),
|
||||||
@@ -859,6 +862,19 @@ class BIMAnnotationProperties(PropertyGroup):
|
|||||||
)
|
)
|
||||||
is_adding_type: bpy.props.BoolProperty(default=False)
|
is_adding_type: bpy.props.BoolProperty(default=False)
|
||||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||||
|
tag_rotation_mode: bpy.props.EnumProperty(
|
||||||
|
name="Tag Rotation Mode",
|
||||||
|
description="How to orient the tag relative to the tagged object",
|
||||||
|
items=[
|
||||||
|
("NONE", "No Rotation", "Keep tag in default orientation"),
|
||||||
|
("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"),
|
||||||
|
("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"),
|
||||||
|
("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"),
|
||||||
|
("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"),
|
||||||
|
("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"),
|
||||||
|
],
|
||||||
|
default="NONE",
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
object_type: str
|
object_type: str
|
||||||
|
|||||||
@@ -99,12 +99,13 @@ class BIM_PT_camera(Panel):
|
|||||||
|
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "linework_mode")
|
row.prop(props, "linework_mode")
|
||||||
|
row = self.layout.row()
|
||||||
|
row.prop(props, "generate_material_layers")
|
||||||
if props.linework_mode == "OPENCASCADE":
|
if props.linework_mode == "OPENCASCADE":
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "fill_mode")
|
row.prop(props, "fill_mode")
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "cut_mode")
|
row.prop(props, "cut_mode")
|
||||||
|
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "width")
|
row.prop(props, "width")
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
@@ -193,6 +194,7 @@ class BIM_PT_element_filters(Panel):
|
|||||||
text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
|
text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
|
||||||
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
|
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
|
||||||
row.label(text=text, icon="FILTER")
|
row.label(text=text, icon="FILTER")
|
||||||
|
row.operator("bim.exclude_annotation", icon="REMOVE", text="")
|
||||||
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
|
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,6 @@ class AnnotationToolUI:
|
|||||||
def draw_type_selection_interface(cls):
|
def draw_type_selection_interface(cls):
|
||||||
# shared by both sidebar and header
|
# shared by both sidebar and header
|
||||||
object_type = cls.props.object_type
|
object_type = cls.props.object_type
|
||||||
|
|
||||||
row = cls.layout.row(align=True)
|
row = cls.layout.row(align=True)
|
||||||
row.label(text="", icon="FILE_VOLUME")
|
row.label(text="", icon="FILE_VOLUME")
|
||||||
prop_with_search(row, cls.props, "object_type", text="")
|
prop_with_search(row, cls.props, "object_type", text="")
|
||||||
@@ -248,6 +247,9 @@ class AnnotationToolUI:
|
|||||||
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
|
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
|
||||||
|
|
||||||
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
|
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
|
||||||
|
row = cls.layout.row(align=True)
|
||||||
|
row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")
|
||||||
|
row.prop(cls.props, "tag_rotation_mode", text="")
|
||||||
add_layout_hotkey_operator(
|
add_layout_hotkey_operator(
|
||||||
cls.layout,
|
cls.layout,
|
||||||
"Bulk Tag",
|
"Bulk Tag",
|
||||||
@@ -299,6 +301,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
return
|
return
|
||||||
|
|
||||||
related_objects = bpy.context.selected_objects
|
related_objects = bpy.context.selected_objects
|
||||||
|
created_objects = []
|
||||||
|
|
||||||
for related_object in related_objects:
|
for related_object in related_objects:
|
||||||
obj = core.add_annotation(
|
obj = core.add_annotation(
|
||||||
tool.Ifc,
|
tool.Ifc,
|
||||||
@@ -311,7 +315,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
),
|
),
|
||||||
enable_editing=False,
|
enable_editing=False,
|
||||||
)
|
)
|
||||||
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
|
tool.Drawing.setup_annotation_object(obj, object_type, related_object, props.tag_rotation_mode)
|
||||||
|
created_objects.append(obj)
|
||||||
|
|
||||||
|
# Select the created annotation objects
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
for obj in created_objects:
|
||||||
|
obj.select_set(True)
|
||||||
|
if created_objects:
|
||||||
|
bpy.context.view_layer.objects.active = created_objects[-1]
|
||||||
|
|
||||||
def hotkey_S_A(self):
|
def hotkey_S_A(self):
|
||||||
if bpy.ops.bim.add_annotation.poll():
|
if bpy.ops.bim.add_annotation.poll():
|
||||||
@@ -341,4 +353,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
continue
|
continue
|
||||||
related_object = tool.Ifc.get_object(related_product)
|
related_object = tool.Ifc.get_object(related_product)
|
||||||
|
|
||||||
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
|
rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode
|
||||||
|
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode)
|
||||||
|
|||||||
@@ -109,8 +109,9 @@ def block_scale(scene: bpy.types.Scene) -> None:
|
|||||||
if obj.type == "CAMERA":
|
if obj.type == "CAMERA":
|
||||||
camera = tool.Ifc.get_entity(obj)
|
camera = tool.Ifc.get_entity(obj)
|
||||||
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
|
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
|
||||||
obj.scale = (-1, -1, -1)
|
# Only update if scale isn't already (-1, -1, -1)
|
||||||
obj.rotation_euler = (0.0, 0.0, math.radians(180))
|
if obj.scale != (-1, -1, -1):
|
||||||
|
obj.scale = (-1, -1, -1)
|
||||||
else:
|
else:
|
||||||
if obj.scale != (1, 1, 1):
|
if obj.scale != (1, 1, 1):
|
||||||
obj.scale = (1, 1, 1)
|
obj.scale = (1, 1, 1)
|
||||||
|
|||||||
@@ -858,6 +858,10 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
objects_to_remove = context.selected_objects
|
objects_to_remove = context.selected_objects
|
||||||
|
|
||||||
self.process_arrays(context)
|
self.process_arrays(context)
|
||||||
|
|
||||||
|
# Track aggregates before deleting their parts
|
||||||
|
aggregates_to_check = self.track_aggregates(objects_to_remove)
|
||||||
|
|
||||||
clear_active_object = True
|
clear_active_object = True
|
||||||
|
|
||||||
for i, obj in enumerate(objects_to_remove, 1):
|
for i, obj in enumerate(objects_to_remove, 1):
|
||||||
@@ -882,13 +886,28 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
continue
|
continue
|
||||||
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
|
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
|
||||||
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
|
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
|
||||||
return {"FINISHED"}
|
continue
|
||||||
|
if element.is_a("IfcGridAxis"):
|
||||||
|
# Deleting the last W axis is OK
|
||||||
|
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
||||||
|
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
|
||||||
|
):
|
||||||
|
self.report(
|
||||||
|
{"INFO"}, "The last grid axis of a grid cannot be deleted. Delete the grid instead."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if tool.Drawing.is_auto_annotation(element):
|
||||||
|
self.report({"INFO"}, "References cannot be deleted. Exclude the referenced element instead.")
|
||||||
|
continue
|
||||||
tool.Geometry.delete_ifc_object(obj)
|
tool.Geometry.delete_ifc_object(obj)
|
||||||
elif tool.Geometry.is_representation_item(obj):
|
elif tool.Geometry.is_representation_item(obj):
|
||||||
tool.Geometry.delete_ifc_item(obj)
|
tool.Geometry.delete_ifc_item(obj)
|
||||||
else:
|
else:
|
||||||
bpy.data.objects.remove(obj)
|
bpy.data.objects.remove(obj)
|
||||||
|
|
||||||
|
# Delete empty aggregates after deleting their parts
|
||||||
|
self.delete_empty_aggregates(aggregates_to_check)
|
||||||
|
|
||||||
for opening in tool.Model.get_model_props().openings:
|
for opening in tool.Model.get_model_props().openings:
|
||||||
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
|
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
|
||||||
bpy.data.objects.remove(opening.obj)
|
bpy.data.objects.remove(opening.obj)
|
||||||
@@ -921,6 +940,49 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
data["old_file"].redo()
|
data["old_file"].redo()
|
||||||
tool.Ifc.set(data["new_file"])
|
tool.Ifc.set(data["new_file"])
|
||||||
|
|
||||||
|
def track_aggregates(self, objects_to_remove):
|
||||||
|
"""Track aggregates that contain objects being deleted"""
|
||||||
|
aggregates_to_check = set()
|
||||||
|
for obj in objects_to_remove:
|
||||||
|
if not tool.Blender.is_valid_data_block(obj):
|
||||||
|
continue
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element:
|
||||||
|
continue
|
||||||
|
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||||
|
if aggregate:
|
||||||
|
aggregates_to_check.add(aggregate)
|
||||||
|
return aggregates_to_check
|
||||||
|
|
||||||
|
def delete_empty_aggregates(self, aggregates_to_check):
|
||||||
|
"""Delete aggregates that now have no parts"""
|
||||||
|
deleted_aggregates = []
|
||||||
|
for aggregate in aggregates_to_check:
|
||||||
|
# Check if aggregate still exists (might have been deleted already)
|
||||||
|
try:
|
||||||
|
aggregate.id()
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||||
|
if len(related_objects) == 0:
|
||||||
|
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||||
|
deleted_aggregates.append(aggregate_name)
|
||||||
|
|
||||||
|
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||||
|
if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj):
|
||||||
|
tool.Geometry.delete_ifc_object(aggregate_obj)
|
||||||
|
|
||||||
|
# Show info message if aggregates were deleted
|
||||||
|
if deleted_aggregates:
|
||||||
|
if len(deleted_aggregates) == 1:
|
||||||
|
self.report(
|
||||||
|
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||||
|
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||||
|
|
||||||
def process_arrays(self, context: bpy.types.Context) -> None:
|
def process_arrays(self, context: bpy.types.Context) -> None:
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
selected_objects = set(context.selected_objects)
|
selected_objects = set(context.selected_objects)
|
||||||
@@ -2973,6 +3035,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
item_mesh = bpy.data.meshes.new("tmp")
|
item_mesh = bpy.data.meshes.new("tmp")
|
||||||
tool.Ifc.link(item, item_mesh)
|
tool.Ifc.link(item, item_mesh)
|
||||||
item_obj = bpy.data.objects.new("tmp", item_mesh)
|
item_obj = bpy.data.objects.new("tmp", item_mesh)
|
||||||
|
tool.Geometry.lock_scale(item_obj)
|
||||||
tool.Geometry.name_item_object(item_obj, item)
|
tool.Geometry.name_item_object(item_obj, item)
|
||||||
item_obj.matrix_world = obj.matrix_world
|
item_obj.matrix_world = obj.matrix_world
|
||||||
bpy.context.collection.objects.link(item_obj)
|
bpy.context.collection.objects.link(item_obj)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class SolarData:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sun_position(cls):
|
def sun_position(cls):
|
||||||
return tool.Blender.get_sun_position_addon()
|
return tool.Blender.get_addon("sun_position")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sites(cls):
|
def sites(cls):
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class RadianceRender(bpy.types.Operator):
|
|||||||
print(f"Camera position: {camera_position}")
|
print(f"Camera position: {camera_position}")
|
||||||
print(f"Camera direction: {camera_direction}")
|
print(f"Camera direction: {camera_direction}")
|
||||||
|
|
||||||
# sun_position = tool.Blender.get_sun_position_addon()
|
# sun_position = tool.Blender.get_addon("sun_position")
|
||||||
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
|
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
|
||||||
# sun_pos_props.time,
|
# sun_pos_props.time,
|
||||||
# sun_pos_props.latitude,
|
# sun_pos_props.latitude,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from bpy.types import PropertyGroup
|
|||||||
from bonsai.bim.module.light.data import SolarData
|
from bonsai.bim.module.light.data import SolarData
|
||||||
from bonsai.bim.module.light.decorator import SolarDecorator
|
from bonsai.bim.module.light.decorator import SolarDecorator
|
||||||
|
|
||||||
sun_position = tool.Blender.get_sun_position_addon()
|
sun_position = tool.Blender.get_addon("sun_position")
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||||
|
|||||||
@@ -373,7 +373,12 @@ class BIMStairProperties(PropertyGroup):
|
|||||||
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
||||||
self["nosing_length"] = 0
|
self["nosing_length"] = 0
|
||||||
|
|
||||||
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
|
def update_custom_tread_lock(self, context: bpy.types.Context) -> None:
|
||||||
|
"""When lock is enabled, sync custom treads with tread_run"""
|
||||||
|
if self.custom_tread_lock:
|
||||||
|
self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||||
|
|
||||||
|
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock")
|
||||||
|
|
||||||
is_editing: bpy.props.BoolProperty(default=False)
|
is_editing: bpy.props.BoolProperty(default=False)
|
||||||
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
|
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
|
||||||
@@ -407,6 +412,12 @@ class BIMStairProperties(PropertyGroup):
|
|||||||
default="CONCRETE",
|
default="CONCRETE",
|
||||||
update=validate_nosing_value,
|
update=validate_nosing_value,
|
||||||
)
|
)
|
||||||
|
custom_tread_lock: bpy.props.BoolProperty(
|
||||||
|
name="Lock First/Last Treads to Tread Run",
|
||||||
|
description="When enabled, first and last treads automatically use the Tread Run value",
|
||||||
|
default=True,
|
||||||
|
update=update_custom_tread_lock,
|
||||||
|
)
|
||||||
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
|
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
|
||||||
name="Custom First / Last Treads Widths",
|
name="Custom First / Last Treads Widths",
|
||||||
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
|
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
|
||||||
@@ -442,6 +453,7 @@ class BIMStairProperties(PropertyGroup):
|
|||||||
top_slab_depth: float
|
top_slab_depth: float
|
||||||
has_top_nib: bool
|
has_top_nib: bool
|
||||||
stair_type: str
|
stair_type: str
|
||||||
|
custom_tread_lock: bool
|
||||||
custom_first_last_tread_run: tuple[float, float]
|
custom_first_last_tread_run: tuple[float, float]
|
||||||
nosing_length: float
|
nosing_length: float
|
||||||
nosing_depth: float
|
nosing_depth: float
|
||||||
@@ -480,8 +492,11 @@ class BIMStairProperties(PropertyGroup):
|
|||||||
}
|
}
|
||||||
stair_kwargs.update(generic_props)
|
stair_kwargs.update(generic_props)
|
||||||
|
|
||||||
# defined here to appear last in UI
|
# If locked, use tread_run for both first and last treads
|
||||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
if self.custom_tread_lock:
|
||||||
|
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||||
|
else:
|
||||||
|
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||||
|
|
||||||
if not convert_to_project_units:
|
if not convert_to_project_units:
|
||||||
return stair_kwargs
|
return stair_kwargs
|
||||||
@@ -489,8 +504,24 @@ class BIMStairProperties(PropertyGroup):
|
|||||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
|
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
|
||||||
return stair_kwargs
|
return stair_kwargs
|
||||||
|
|
||||||
|
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
|
||||||
|
"""Get props including custom_tread_lock for saving to IFC"""
|
||||||
|
stair_kwargs = self.get_props_kwargs(convert_to_project_units, stair_type)
|
||||||
|
# Add the lock state for IFC storage (after getting base kwargs to avoid passing to generate function)
|
||||||
|
stair_kwargs["custom_tread_lock"] = self.custom_tread_lock
|
||||||
|
return stair_kwargs
|
||||||
|
|
||||||
def set_props_kwargs_from_ifc_data(self, kwargs):
|
def set_props_kwargs_from_ifc_data(self, kwargs):
|
||||||
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
|
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
|
||||||
|
|
||||||
|
# Determine lock state based on whether custom treads match tread_run
|
||||||
|
# If custom_tread_lock wasn't saved (old files), infer it from the data
|
||||||
|
if "custom_tread_lock" not in kwargs:
|
||||||
|
custom_treads = kwargs.get("custom_first_last_tread_run", (0.0, 0.0))
|
||||||
|
tread_run = kwargs.get("tread_run", 0.3)
|
||||||
|
# Lock is off if either custom tread differs from tread_run and is not 0
|
||||||
|
kwargs["custom_tread_lock"] = not any(ct != 0.0 and ct != tread_run for ct in custom_treads)
|
||||||
|
|
||||||
for prop_name in kwargs:
|
for prop_name in kwargs:
|
||||||
setattr(self, prop_name, kwargs[prop_name])
|
setattr(self, prop_name, kwargs[prop_name])
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
props = tool.Model.get_stair_props(obj)
|
props = tool.Model.get_stair_props(obj)
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
|
|
||||||
stair_data = props.get_props_kwargs(convert_to_project_units=True)
|
# Use the special method that includes custom_tread_lock for IFC storage
|
||||||
|
stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
|
||||||
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
|
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
|
||||||
if not pset:
|
if not pset:
|
||||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair")
|
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair")
|
||||||
@@ -241,7 +242,8 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
assert element
|
assert element
|
||||||
props = tool.Model.get_stair_props(obj)
|
props = tool.Model.get_stair_props(obj)
|
||||||
|
|
||||||
data = props.get_props_kwargs(convert_to_project_units=True)
|
# Use the special method that includes custom_tread_lock for IFC storage
|
||||||
|
data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
|
||||||
props.is_editing = False
|
props.is_editing = False
|
||||||
regenerate_stair_mesh(obj)
|
regenerate_stair_mesh(obj)
|
||||||
tool.Model.add_body_representation(obj)
|
tool.Model.add_body_representation(obj)
|
||||||
|
|||||||
@@ -302,13 +302,38 @@ class BIM_PT_stair(bpy.types.Panel):
|
|||||||
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
|
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
for prop_name in props.get_props_kwargs():
|
for prop_name in props.get_props_kwargs():
|
||||||
|
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
|
||||||
|
if prop_name == "custom_tread_lock":
|
||||||
|
continue
|
||||||
|
|
||||||
prop_value = getattr(props, prop_name)
|
prop_value = getattr(props, prop_name)
|
||||||
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
|
||||||
|
# Special handling for custom_first_last_tread_run
|
||||||
|
if prop_name == "custom_first_last_tread_run":
|
||||||
|
# Draw the lock toggle
|
||||||
|
row_lock = self.layout.row(align=True)
|
||||||
|
lock_text = (
|
||||||
|
"Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
|
||||||
|
)
|
||||||
|
row_lock.prop(
|
||||||
|
props,
|
||||||
|
"custom_tread_lock",
|
||||||
|
text=lock_text,
|
||||||
|
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only show the custom values input if unlocked
|
||||||
|
if not props.custom_tread_lock:
|
||||||
|
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||||
|
self.layout.label(text=f"{prop_readable_name}:")
|
||||||
|
self.layout.prop(props, prop_name, text="")
|
||||||
|
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||||
self.layout.label(text=f"{prop_readable_name}:")
|
self.layout.label(text=f"{prop_readable_name}:")
|
||||||
self.layout.prop(props, prop_name, text="")
|
self.layout.prop(props, prop_name, text="")
|
||||||
else:
|
else:
|
||||||
self.layout.prop(props, prop_name)
|
self.layout.prop(props, prop_name)
|
||||||
|
|
||||||
if prop_name == "height": # Weak but we just want to insert this inside props drawing
|
if prop_name == "height": # Weak but we just want to insert this inside props drawing
|
||||||
row_length = self.layout.row(align=True)
|
row_length = self.layout.row(align=True)
|
||||||
row_length.prop(props, "total_length_target")
|
row_length.prop(props, "total_length_target")
|
||||||
|
|||||||
@@ -2522,6 +2522,7 @@ class FlipClippingPlane(bpy.types.Operator):
|
|||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
if obj in tool.Project.get_project_props().clipping_planes_objs:
|
if obj in tool.Project.get_project_props().clipping_planes_objs:
|
||||||
obj.rotation_euler[0] += radians(180)
|
obj.rotation_euler[0] += radians(180)
|
||||||
|
obj.rotation_euler[0] %= radians(360)
|
||||||
context.view_layer.update()
|
context.view_layer.update()
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
@@ -450,9 +450,9 @@ class BIMProjectProperties(PropertyGroup):
|
|||||||
self.mvd = header_data.mvd
|
self.mvd = header_data.mvd
|
||||||
self.author_name = header_data.author_name
|
self.author_name = header_data.author_name
|
||||||
self.author_email = header_data.author_email
|
self.author_email = header_data.author_email
|
||||||
self.organisation_name = header_data.organization_name
|
self.organisation_name = header_data.organisation_name
|
||||||
self.organisation_email = header_data.organization_email
|
self.organisation_email = header_data.organisation_email
|
||||||
self.authorisation = header_data.authorization
|
self.authorisation = header_data.authorisation
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
is_editing: bool
|
is_editing: bool
|
||||||
|
|||||||
@@ -653,4 +653,5 @@ class BIM_PT_purge(Panel):
|
|||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
|
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
|
||||||
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
|
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
|
||||||
row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = object_type
|
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
|
||||||
|
merge_op.object_type = object_type
|
||||||
|
|||||||
@@ -242,14 +242,11 @@ class BIM_PT_object_psets(Panel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if not (obj := context.active_object):
|
return (
|
||||||
return False
|
(obj := tool.Blender.get_active_object())
|
||||||
ifc_id = tool.Blender.get_ifc_definition_id(obj)
|
and (element := tool.Ifc.get_entity(obj))
|
||||||
if not ifc_id:
|
and element.is_a("IfcObjectDefinition")
|
||||||
return False
|
)
|
||||||
if not tool.Ifc.get_object_by_identifier(ifc_id):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
if not ObjectPsetsData.is_loaded:
|
if not ObjectPsetsData.is_loaded:
|
||||||
@@ -388,14 +385,11 @@ class BIM_PT_object_qtos(Panel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if not (obj := context.active_object):
|
return (
|
||||||
return False
|
(obj := tool.Blender.get_active_object())
|
||||||
ifc_id = tool.Blender.get_ifc_definition_id(obj)
|
and (element := tool.Ifc.get_entity(obj))
|
||||||
if not ifc_id:
|
and element.is_a("IfcObjectDefinition")
|
||||||
return False
|
)
|
||||||
if not tool.Ifc.get_object_by_identifier(ifc_id):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
if not ObjectQtosData.is_loaded:
|
if not ObjectQtosData.is_loaded:
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
|
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
|
||||||
element = core.assign_class(
|
element = core.assign_class(
|
||||||
tool.Ifc,
|
tool.Ifc,
|
||||||
tool.Collector,
|
tool.Collector,
|
||||||
@@ -326,13 +327,9 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
predefined_type=predefined_type,
|
predefined_type=predefined_type,
|
||||||
should_add_representation=False,
|
should_add_representation=False,
|
||||||
)
|
)
|
||||||
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
|
|
||||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||||
bonsai.core.geometry.switch_representation(
|
bonsai.core.geometry.switch_representation(
|
||||||
tool.Ifc,
|
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||||
tool.Geometry,
|
|
||||||
obj=obj,
|
|
||||||
representation=representation,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class ColourByPropertyData:
|
|||||||
elif pset.endswith("BaseQuantities"):
|
elif pset.endswith("BaseQuantities"):
|
||||||
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
|
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
|
||||||
else:
|
else:
|
||||||
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
|
keys.extend([f'"{pset}"."{name}"' for name in properties.keys() if name != "id"])
|
||||||
results = [(k, k, "") for k in keys]
|
results = [(k, k, "") for k in keys]
|
||||||
return default + results
|
return default + results
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ classes = (
|
|||||||
operator.RemoveWorkTime,
|
operator.RemoveWorkTime,
|
||||||
operator.ReorderTask,
|
operator.ReorderTask,
|
||||||
operator.SaveAnimationColorScheme,
|
operator.SaveAnimationColorScheme,
|
||||||
|
operator.SelectTaskElements,
|
||||||
operator.SelectTaskRelatedInputs,
|
operator.SelectTaskRelatedInputs,
|
||||||
operator.SelectTaskRelatedProducts,
|
operator.SelectTaskRelatedProducts,
|
||||||
operator.SelectUnassignedWorkScheduleProducts,
|
operator.SelectUnassignedWorkScheduleProducts,
|
||||||
|
|||||||
@@ -243,6 +243,9 @@ class SequenceData:
|
|||||||
data["NestingIndex"] = None
|
data["NestingIndex"] = None
|
||||||
for rel in task.Nests or []:
|
for rel in task.Nests or []:
|
||||||
data["NestingIndex"] = rel.RelatedObjects.index(task)
|
data["NestingIndex"] = rel.RelatedObjects.index(task)
|
||||||
|
data["TotalInputs"] = len(tool.Sequence.get_task_inputs(task))
|
||||||
|
data["TotalOutputs"] = len(tool.Sequence.get_task_outputs(task))
|
||||||
|
data["TotalElements"] = data["TotalInputs"] + data["TotalOutputs"]
|
||||||
cls.data["tasks"][task.id()] = data
|
cls.data["tasks"][task.id()] = data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1281,6 +1281,17 @@ class DisableEditingSequence(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class SelectTaskElements(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
bl_idname = "bim.select_task_elements"
|
||||||
|
bl_label = "Select All Task Elements"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
task: bpy.props.IntProperty()
|
||||||
|
|
||||||
|
def _execute(self, context):
|
||||||
|
core.select_task_inputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
|
||||||
|
core.select_task_outputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task))
|
||||||
|
|
||||||
|
|
||||||
class SelectTaskRelatedProducts(bpy.types.Operator, tool.Ifc.Operator):
|
class SelectTaskRelatedProducts(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.select_task_related_products"
|
bl_idname = "bim.select_task_related_products"
|
||||||
bl_label = "Select All Output Products"
|
bl_label = "Select All Output Products"
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ def update_active_task_index(self, context):
|
|||||||
bonsai.bim.module.pset.data.refresh()
|
bonsai.bim.module.pset.data.refresh()
|
||||||
if self.editing_task_type == "SEQUENCE":
|
if self.editing_task_type == "SEQUENCE":
|
||||||
tool.Sequence.load_task_properties()
|
tool.Sequence.load_task_properties()
|
||||||
|
if self.should_auto_select:
|
||||||
|
tool.Sequence.select_active_task_elements(task)
|
||||||
|
|
||||||
|
|
||||||
def update_active_task_outputs(self, context):
|
def update_active_task_outputs(self, context):
|
||||||
@@ -485,6 +487,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
|||||||
other_columns: EnumProperty(
|
other_columns: EnumProperty(
|
||||||
items=[
|
items=[
|
||||||
("Controls.Calendar", "Calendar", ""),
|
("Controls.Calendar", "Calendar", ""),
|
||||||
|
("Controls.TotalElements", "Total Elements", "The total number of assigned elements to this task"),
|
||||||
|
("Controls.TotalInputs", "Total Inputs", "The total number of input elements to this task"),
|
||||||
|
("Controls.TotalOutputs", "Total Outputs", "The total number of input elements to this task"),
|
||||||
],
|
],
|
||||||
name="Special Columns",
|
name="Special Columns",
|
||||||
)
|
)
|
||||||
@@ -518,9 +523,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
|||||||
active_task_input_index: IntProperty(name="Active Task Input Index")
|
active_task_input_index: IntProperty(name="Active Task Input Index")
|
||||||
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
|
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
|
||||||
active_task_output_index: IntProperty(name="Active Task Output Index")
|
active_task_output_index: IntProperty(name="Active Task Output Index")
|
||||||
show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs)
|
show_nested_outputs: BoolProperty(
|
||||||
show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources)
|
name="Show Nested Task Elements", default=False, update=update_active_task_outputs
|
||||||
show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs)
|
)
|
||||||
|
show_nested_resources: BoolProperty(
|
||||||
|
name="Show Nested Task Elements", default=False, update=update_active_task_resources
|
||||||
|
)
|
||||||
|
show_nested_inputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_inputs)
|
||||||
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
|
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
|
||||||
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
|
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
|
||||||
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
|
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
|
||||||
@@ -531,6 +540,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
|||||||
filter_by_active_schedule: BoolProperty(
|
filter_by_active_schedule: BoolProperty(
|
||||||
name="Filter By Active Schedule", default=False, update=update_filter_by_active_schedule
|
name="Filter By Active Schedule", default=False, update=update_filter_by_active_schedule
|
||||||
)
|
)
|
||||||
|
should_auto_select: BoolProperty(
|
||||||
|
name="Auto Select", description="Auto select elements when task is selected", default=False
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
work_schedule_predefined_types: str
|
work_schedule_predefined_types: str
|
||||||
|
|||||||
@@ -290,13 +290,21 @@ class BIM_PT_work_schedules(Panel):
|
|||||||
row2 = self.layout.row(align=True)
|
row2 = self.layout.row(align=True)
|
||||||
row2.alignment = "RIGHT"
|
row2.alignment = "RIGHT"
|
||||||
|
|
||||||
row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
|
col = row2.column()
|
||||||
row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
|
row_ = col.row(align=True)
|
||||||
row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
|
op = row_.operator("bim.select_task_elements", icon="RESTRICT_SELECT_OFF", text="Select Elements")
|
||||||
row2.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
|
op.task = task.ifc_definition_id
|
||||||
|
row_.prop(self.props, "should_auto_select", icon="OBJECT_HIDDEN", text="")
|
||||||
|
|
||||||
|
col = row2.column()
|
||||||
|
row_ = col.row(align=True)
|
||||||
|
row_.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
|
||||||
|
row_.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING")
|
||||||
|
row_.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
|
||||||
|
row_.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = (
|
||||||
ifc_definition_id
|
ifc_definition_id
|
||||||
)
|
)
|
||||||
row2.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
|
row_.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = (
|
||||||
ifc_definition_id
|
ifc_definition_id
|
||||||
)
|
)
|
||||||
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
|
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
|
||||||
@@ -716,11 +724,10 @@ class BIM_PT_task_icom(Panel):
|
|||||||
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
|
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
|
||||||
op.related_object = input_id
|
op.related_object = input_id
|
||||||
|
|
||||||
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
|
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
|
||||||
op.task = task.ifc_definition_id
|
op.task = task.ifc_definition_id
|
||||||
|
row2.prop(self.props, "show_nested_inputs", icon="OUTLINER", text="")
|
||||||
|
|
||||||
row2 = col.row()
|
|
||||||
row2.prop(self.props, "show_nested_inputs", text="Show Nested")
|
|
||||||
row2 = col.row()
|
row2 = col.row()
|
||||||
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
|
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index")
|
||||||
|
|
||||||
@@ -744,8 +751,7 @@ class BIM_PT_task_icom(Panel):
|
|||||||
op.related_object_type = "RESOURCE"
|
op.related_object_type = "RESOURCE"
|
||||||
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
|
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id
|
||||||
|
|
||||||
row2 = col.row()
|
row2.prop(self.props, "show_nested_resources", icon="OUTLINER", text="")
|
||||||
row2.prop(self.props, "show_nested_resources", text="Show Nested")
|
|
||||||
|
|
||||||
row2 = col.row()
|
row2 = col.row()
|
||||||
row2.template_list(
|
row2.template_list(
|
||||||
@@ -773,10 +779,10 @@ class BIM_PT_task_icom(Panel):
|
|||||||
output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
|
output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
|
||||||
op.relating_product = output_id
|
op.relating_product = output_id
|
||||||
|
|
||||||
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select")
|
op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="")
|
||||||
op.task = task.ifc_definition_id
|
op.task = task.ifc_definition_id
|
||||||
row2 = col.row()
|
row2.prop(self.props, "show_nested_outputs", icon="OUTLINER", text="")
|
||||||
row2.prop(self.props, "show_nested_outputs", text="Show Nested")
|
|
||||||
row2 = col.row()
|
row2 = col.row()
|
||||||
row2.template_list(
|
row2.template_list(
|
||||||
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
|
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index"
|
||||||
@@ -893,7 +899,7 @@ class BIM_UL_tasks(UIList):
|
|||||||
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
|
split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
|
||||||
split2.prop(item, "name", emboss=False, text="")
|
split2.prop(item, "name", emboss=False, text="")
|
||||||
|
|
||||||
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task)
|
BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task, item.ifc_definition_id)
|
||||||
|
|
||||||
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
|
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
|
||||||
row.prop(
|
row.prop(
|
||||||
@@ -960,6 +966,7 @@ class BIM_UL_tasks(UIList):
|
|||||||
row: bpy.types.UILayout,
|
row: bpy.types.UILayout,
|
||||||
item: Optional[bpy.types.PropertyGroup] = None,
|
item: Optional[bpy.types.PropertyGroup] = None,
|
||||||
task: Optional[dict[str, Any]] = None,
|
task: Optional[dict[str, Any]] = None,
|
||||||
|
ifc_definition_id: int = 0,
|
||||||
*,
|
*,
|
||||||
header: bool = False,
|
header: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -999,6 +1006,21 @@ class BIM_UL_tasks(UIList):
|
|||||||
row.label(text=item.derived_calendar + "*")
|
row.label(text=item.derived_calendar + "*")
|
||||||
else:
|
else:
|
||||||
row.label(text=item.calendar or "-")
|
row.label(text=item.calendar or "-")
|
||||||
|
elif column.name == "Controls.TotalInputs":
|
||||||
|
if header:
|
||||||
|
row.label(text="# Inputs")
|
||||||
|
else:
|
||||||
|
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalInputs"]))
|
||||||
|
elif column.name == "Controls.TotalOutputs":
|
||||||
|
if header:
|
||||||
|
row.label(text="# Outputs")
|
||||||
|
else:
|
||||||
|
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalOutputs"]))
|
||||||
|
elif column.name == "Controls.TotalElements":
|
||||||
|
if header:
|
||||||
|
row.label(text="# Elements")
|
||||||
|
else:
|
||||||
|
row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalElements"]))
|
||||||
else:
|
else:
|
||||||
ifc_class, name = column.name.split(".")
|
ifc_class, name = column.name.split(".")
|
||||||
if header:
|
if header:
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class GridDecorator:
|
|||||||
|
|
||||||
grid_props = tool.Spatial.get_grid_props()
|
grid_props = tool.Spatial.get_grid_props()
|
||||||
for axis in grid_props.grid_axes:
|
for axis in grid_props.grid_axes:
|
||||||
if not (obj := axis.obj) or obj.hide_get() == True:
|
if not (obj := axis.obj) or obj.visible_get() is False:
|
||||||
continue
|
continue
|
||||||
if obj.select_get() and context.mode != "OBJECT":
|
if obj.select_get() and context.mode != "OBJECT":
|
||||||
continue
|
continue
|
||||||
@@ -125,21 +125,22 @@ class GridDecorator:
|
|||||||
unselected_edges = []
|
unselected_edges = []
|
||||||
grid_props = tool.Spatial.get_grid_props()
|
grid_props = tool.Spatial.get_grid_props()
|
||||||
for axis in grid_props.grid_axes:
|
for axis in grid_props.grid_axes:
|
||||||
if (obj := axis.obj) and obj.hide_get() == False:
|
if not (obj := axis.obj) or obj.visible_get() is False:
|
||||||
if obj.select_get():
|
continue
|
||||||
if context.mode != "OBJECT":
|
if obj.select_get():
|
||||||
continue
|
if context.mode != "OBJECT":
|
||||||
edges = selected_edges
|
continue
|
||||||
verts = selected_verts
|
edges = selected_edges
|
||||||
else:
|
verts = selected_verts
|
||||||
edges = unselected_edges
|
else:
|
||||||
verts = unselected_verts
|
edges = unselected_edges
|
||||||
i = len(verts)
|
verts = unselected_verts
|
||||||
edges.append([i, i + 1])
|
i = len(verts)
|
||||||
matrix_world = obj.matrix_world
|
edges.append([i, i + 1])
|
||||||
v1 = matrix_world @ obj.data.vertices[0].co
|
matrix_world = obj.matrix_world
|
||||||
v2 = matrix_world @ obj.data.vertices[1].co
|
v1 = matrix_world @ obj.data.vertices[0].co
|
||||||
verts.extend([v1, v2])
|
v2 = matrix_world @ obj.data.vertices[1].co
|
||||||
|
verts.extend([v1, v2])
|
||||||
|
|
||||||
if unselected_verts:
|
if unselected_verts:
|
||||||
self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges)
|
self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges)
|
||||||
|
|||||||
@@ -34,14 +34,11 @@ class BIM_PT_type(Panel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if not context.active_object:
|
return (
|
||||||
return False
|
(obj := tool.Blender.get_active_object())
|
||||||
element = tool.Ifc.get_entity(context.active_object)
|
and (element := tool.Ifc.get_entity(obj))
|
||||||
if not element:
|
and (element.is_a("IfcObject") or element.is_a("IfcTypeObject"))
|
||||||
return False
|
)
|
||||||
if not element.is_a("IfcProduct") and not element.is_a("IfcTypeProduct"):
|
|
||||||
return True
|
|
||||||
return True
|
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
if not TypeData.is_loaded:
|
if not TypeData.is_loaded:
|
||||||
|
|||||||
@@ -108,10 +108,6 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
|
|||||||
def regenerate_sheet(
|
def regenerate_sheet(
|
||||||
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
|
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
|
||||||
) -> Union[list[tool.Drawing.SheetWarningType], None]:
|
) -> Union[list[tool.Drawing.SheetWarningType], None]:
|
||||||
warnings = drawing.validate_sheet_files(sheet)
|
|
||||||
if warnings:
|
|
||||||
return warnings
|
|
||||||
|
|
||||||
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
|
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
|
||||||
assert titleblock_uri
|
assert titleblock_uri
|
||||||
|
|
||||||
@@ -320,6 +316,7 @@ def duplicate_drawing(
|
|||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance:
|
||||||
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
|
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
|
||||||
new_drawing = ifc.run("root.copy_class", product=drawing)
|
new_drawing = ifc.run("root.copy_class", product=drawing)
|
||||||
|
drawing_tool.clear_annotation_relationships(new_drawing)
|
||||||
drawing_tool.copy_representation(drawing, new_drawing)
|
drawing_tool.copy_representation(drawing, new_drawing)
|
||||||
drawing_tool.set_name(new_drawing, drawing_name)
|
drawing_tool.set_name(new_drawing, drawing_name)
|
||||||
group = drawing_tool.get_drawing_group(new_drawing)
|
group = drawing_tool.get_drawing_group(new_drawing)
|
||||||
@@ -475,46 +472,40 @@ def sync_references(
|
|||||||
if not drawing_tool.has_annotation(drawing):
|
if not drawing_tool.has_annotation(drawing):
|
||||||
return
|
return
|
||||||
|
|
||||||
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))
|
if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))):
|
||||||
if not context:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
group = drawing_tool.get_drawing_group(drawing)
|
group = drawing_tool.get_drawing_group(drawing)
|
||||||
for reference_element in drawing_tool.get_potential_reference_elements(drawing):
|
potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing)
|
||||||
|
|
||||||
|
for element in potential_reference_elements:
|
||||||
|
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
|
||||||
|
drawing_tool.sync_object_placement(obj)
|
||||||
|
|
||||||
|
for element in drawing_tool.get_group_elements(group):
|
||||||
|
if not drawing_tool.is_auto_annotation(element):
|
||||||
|
continue
|
||||||
|
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
|
||||||
|
drawing_tool.sync_object_placement(obj)
|
||||||
|
reference_element = drawing_tool.get_assigned_product(element)
|
||||||
reference_obj = ifc.get_object(reference_element)
|
reference_obj = ifc.get_object(reference_element)
|
||||||
annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element)
|
if reference_element not in potential_reference_elements:
|
||||||
|
# It was auto created, so it makes sense to auto delete
|
||||||
|
if obj := ifc.get_object(element):
|
||||||
|
drawing_tool.delete_object(obj)
|
||||||
|
ifc.run("root.remove_product", product=element)
|
||||||
|
elif not drawing_tool.regenerate_reference_annotation(drawing, element, reference_element, context):
|
||||||
|
if obj := ifc.get_object(element):
|
||||||
|
drawing_tool.delete_object(obj)
|
||||||
|
ifc.run("root.remove_product", product=element)
|
||||||
|
|
||||||
should_delete_existing_annotation = False
|
for reference_element in potential_reference_elements:
|
||||||
should_create_annotation = False
|
if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
|
||||||
|
if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
|
||||||
# remove annotation only if the reference object was changed
|
|
||||||
# otherwise we rely on the existing annotation
|
|
||||||
if annotation:
|
|
||||||
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
|
|
||||||
should_delete_existing_annotation = True
|
|
||||||
|
|
||||||
if should_delete_existing_annotation or not annotation:
|
|
||||||
should_create_annotation = True
|
|
||||||
|
|
||||||
if should_delete_existing_annotation:
|
|
||||||
annotation_obj = ifc.get_object(annotation)
|
|
||||||
if annotation_obj:
|
|
||||||
drawing_tool.delete_object(annotation_obj)
|
|
||||||
ifc.run("root.remove_product", product=annotation)
|
|
||||||
|
|
||||||
if should_create_annotation:
|
|
||||||
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
|
|
||||||
if annotation:
|
|
||||||
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
|
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
|
||||||
ifc.run("group.assign_group", group=group, products=[annotation])
|
ifc.run("group.assign_group", group=group, products=[annotation])
|
||||||
collector.assign(ifc.get_object(annotation))
|
collector.assign(ifc.get_object(annotation))
|
||||||
|
|
||||||
if reference_obj and ifc.is_moved(reference_obj):
|
|
||||||
drawing_tool.sync_object_placement(reference_obj)
|
|
||||||
|
|
||||||
if reference_obj and ifc.is_edited(reference_obj):
|
|
||||||
drawing_tool.sync_object_representation(reference_obj)
|
|
||||||
|
|
||||||
|
|
||||||
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
|
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
|
||||||
drawing.select_assigned_product(context)
|
drawing.select_assigned_product(context)
|
||||||
|
|||||||
@@ -1506,28 +1506,21 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
return bpy.context.preferences.addons[blender_package_name].preferences
|
return bpy.context.preferences.addons[blender_package_name].preferences
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_sun_position_addon(cls) -> Union[types.ModuleType, None]:
|
def get_addon(cls, name: str) -> Union[types.ModuleType, None]:
|
||||||
# Check if it's installed as legacy Blender addon.
|
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sun_position = importlib.import_module("sun_position")
|
return importlib.import_module(name) # Legacy Blender addon
|
||||||
except ImportError:
|
except ImportError:
|
||||||
sun_position = None
|
pass
|
||||||
|
|
||||||
if sun_position:
|
|
||||||
return sun_position
|
|
||||||
|
|
||||||
for package_name in bpy.context.preferences.addons.keys():
|
for package_name in bpy.context.preferences.addons.keys():
|
||||||
if package_name.endswith(".sun_position"):
|
if package_name.endswith(f".{name}"):
|
||||||
try:
|
try:
|
||||||
sun_position = importlib.import_module(package_name)
|
return importlib.import_module(package_name)
|
||||||
return sun_position
|
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return sun_position
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_sun_props(cls) -> Union[SunPosProperties, None]:
|
def get_sun_props(cls) -> Union[SunPosProperties, None]:
|
||||||
assert (scene := bpy.context.scene)
|
assert (scene := bpy.context.scene)
|
||||||
@@ -1889,6 +1882,24 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
obj.select_set(True)
|
obj.select_set(True)
|
||||||
bpy.context.view_layer.objects.active = previously_active
|
bpy.context.view_layer.objects.active = previously_active
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sync_render_visibility(cls):
|
||||||
|
# Doing bpy.ops.object.hide_render_clear_all() or
|
||||||
|
# bpy.ops.object.isolate_type_render() is extremely slow.
|
||||||
|
# Hopefully this doesn't crash on Windows, it doesn't crash on Linux.
|
||||||
|
should_hides = [0 if obj.visible_get() else 1 for obj in bpy.data.objects]
|
||||||
|
should_hides = np.fromiter(should_hides, dtype=np.uint8, count=len(should_hides))
|
||||||
|
bpy.data.objects.foreach_set("hide_render", should_hides)
|
||||||
|
return # Otherwise...
|
||||||
|
# for obj in bpy.data.objects:
|
||||||
|
# if not obj.data:
|
||||||
|
# continue
|
||||||
|
# # For speed, check equality prior to change to prevent needless updates
|
||||||
|
# if (is_visible := obj.visible_get()) and obj.hide_render is True:
|
||||||
|
# obj.hide_render = False
|
||||||
|
# elif not is_visible and obj.hide_render is False:
|
||||||
|
# obj.hide_render = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def hide_objects(cls, objs):
|
def hide_objects(cls, objs):
|
||||||
previously_selected = {o.name for o in bpy.context.selected_objects}
|
previously_selected = {o.name for o in bpy.context.selected_objects}
|
||||||
|
|||||||
@@ -51,9 +51,13 @@ class Collector(bonsai.core.tool.Collector):
|
|||||||
if tool.Geometry.is_locked(element):
|
if tool.Geometry.is_locked(element):
|
||||||
tool.Geometry.lock_object(obj)
|
tool.Geometry.lock_object(obj)
|
||||||
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
||||||
|
if not tool.Spatial.get_grid_props().is_visible:
|
||||||
|
obj.hide_viewport = True
|
||||||
elif element.is_a("IfcGrid"):
|
elif element.is_a("IfcGrid"):
|
||||||
if tool.Geometry.is_locked(element):
|
if tool.Geometry.is_locked(element):
|
||||||
tool.Geometry.lock_object(obj)
|
tool.Geometry.lock_object(obj)
|
||||||
|
if not tool.Spatial.get_grid_props().is_visible:
|
||||||
|
obj.hide_viewport = True
|
||||||
|
|
||||||
if element.is_a("IfcProject"):
|
if element.is_a("IfcProject"):
|
||||||
if tool.Geometry.is_locked(element):
|
if tool.Geometry.is_locked(element):
|
||||||
@@ -69,7 +73,8 @@ class Collector(bonsai.core.tool.Collector):
|
|||||||
tool.Geometry.lock_object(obj)
|
tool.Geometry.lock_object(obj)
|
||||||
collection = cls._create_project_child_collection("IfcSpace")
|
collection = cls._create_project_child_collection("IfcSpace")
|
||||||
cls.link_collection_object_safe(collection, obj)
|
cls.link_collection_object_safe(collection, obj)
|
||||||
obj.hide_viewport = True
|
if not tool.Spatial.get_spatial_props().is_visible:
|
||||||
|
obj.hide_viewport = True
|
||||||
elif element.is_a("IfcStructuralItem"):
|
elif element.is_a("IfcStructuralItem"):
|
||||||
collection = cls._create_project_child_collection("IfcStructuralItem")
|
collection = cls._create_project_child_collection("IfcStructuralItem")
|
||||||
cls.link_collection_object_safe(collection, obj)
|
cls.link_collection_object_safe(collection, obj)
|
||||||
@@ -93,7 +98,8 @@ class Collector(bonsai.core.tool.Collector):
|
|||||||
cls.link_collection_object_safe(collection, obj)
|
cls.link_collection_object_safe(collection, obj)
|
||||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||||
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
||||||
obj.hide_viewport = True
|
if not tool.Spatial.get_spatial_props().is_visible:
|
||||||
|
obj.hide_viewport = True
|
||||||
elif (
|
elif (
|
||||||
tool.Ifc.get_schema() != "IFC2X3"
|
tool.Ifc.get_schema() != "IFC2X3"
|
||||||
and element.is_a("IfcSpatialElement")
|
and element.is_a("IfcSpatialElement")
|
||||||
@@ -105,7 +111,8 @@ class Collector(bonsai.core.tool.Collector):
|
|||||||
cls.link_collection_object_safe(collection, obj)
|
cls.link_collection_object_safe(collection, obj)
|
||||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||||
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
||||||
obj.hide_viewport = True
|
if not tool.Spatial.get_spatial_props().is_visible:
|
||||||
|
obj.hide_viewport = True
|
||||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||||
if collection := cls._create_own_collection(obj):
|
if collection := cls._create_own_collection(obj):
|
||||||
cls.link_collection_object_safe(collection, obj)
|
cls.link_collection_object_safe(collection, obj)
|
||||||
|
|||||||
@@ -273,18 +273,16 @@ class Cost(bonsai.core.tool.Cost):
|
|||||||
return
|
return
|
||||||
props = cls.get_cost_props()
|
props = cls.get_cost_props()
|
||||||
props.cost_item_type_products.clear()
|
props.cost_item_type_products.clear()
|
||||||
# TODO implement process and resource types
|
props.cost_item_processes.clear()
|
||||||
# props.cost_item_processes.clear()
|
props.cost_item_resources.clear()
|
||||||
# props.cost_item_resources.clear()
|
|
||||||
for rel in cost_item.Controls or []:
|
for rel in cost_item.Controls or []:
|
||||||
for related_object in rel.RelatedObjects:
|
for related_object in rel.RelatedObjects:
|
||||||
if related_object.is_a("IfcTypeProduct"):
|
if related_object.is_a("IfcTypeProduct"):
|
||||||
new = props.cost_item_type_products.add()
|
new = props.cost_item_type_products.add()
|
||||||
# TODO implement process and resource types
|
elif related_object.is_a("IfcProcess"):
|
||||||
# elif related_object.is_a("IfcProcess"):
|
new = props.cost_item_processes.add()
|
||||||
# new = props.cost_item_processes.add()
|
elif related_object.is_a("IfcResource"):
|
||||||
# elif related_object.is_a("IfcResource"):
|
new = props.cost_item_resources.add()
|
||||||
# new = props.cost_item_resources.add()
|
|
||||||
new.ifc_definition_id = related_object.id()
|
new.ifc_definition_id = related_object.id()
|
||||||
new.name = related_object.Name or "Unnamed"
|
new.name = related_object.Name or "Unnamed"
|
||||||
|
|
||||||
|
|||||||
+104
-17
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
@@ -33,7 +34,7 @@ import bonsai.tool as tool
|
|||||||
from bonsai.bim.ifc import IfcStore
|
from bonsai.bim.ifc import IfcStore
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Literal, TYPE_CHECKING, assert_never
|
from typing import Literal, TYPE_CHECKING, assert_never, Union
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -136,12 +137,30 @@ class Debug(bonsai.core.tool.Debug):
|
|||||||
"PERSON",
|
"PERSON",
|
||||||
"PERSON_AND_ORGANIZATION",
|
"PERSON_AND_ORGANIZATION",
|
||||||
],
|
],
|
||||||
|
by_name_or_identification_only: bool = False,
|
||||||
) -> dict[str, list[str]]:
|
) -> dict[str, list[str]]:
|
||||||
"""Merge identical objects.
|
"""Merge identical objects.
|
||||||
|
|
||||||
Note that Styles UI (or other UI) should be updated manually after using this method.
|
Note that Styles UI (or other UI) should be updated manually after using this method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
object_type: The type of object to merge
|
||||||
|
by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
|
||||||
|
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
|
||||||
|
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
|
||||||
|
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def normalize_name(name: str) -> str:
|
||||||
|
"""Remove .XXX suffix pattern from names (e.g., 'foo.001' -> 'foo')"""
|
||||||
|
if not name:
|
||||||
|
return ""
|
||||||
|
# Match pattern: name ending with .digits
|
||||||
|
match = re.match(r"^(.+)\.\d+$", name)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return name
|
||||||
|
|
||||||
def get_hash(element: ifcopenshell.entity_instance) -> int:
|
def get_hash(element: ifcopenshell.entity_instance) -> int:
|
||||||
data = element.get_info_2(include_identifier=False, recursive=True)
|
data = element.get_info_2(include_identifier=False, recursive=True)
|
||||||
if object_type == "APPLICATION":
|
if object_type == "APPLICATION":
|
||||||
@@ -152,6 +171,49 @@ class Debug(bonsai.core.tool.Debug):
|
|||||||
data["TheOrganization"] = element.TheOrganization.id()
|
data["TheOrganization"] = element.TheOrganization.id()
|
||||||
return hash(json.dumps(data, sort_keys=True))
|
return hash(json.dumps(data, sort_keys=True))
|
||||||
|
|
||||||
|
def get_name_key(element: ifcopenshell.entity_instance) -> str:
|
||||||
|
"""Get key based on name/identifier attribute for the given object type"""
|
||||||
|
if object_type == "STYLE":
|
||||||
|
name = element.Name if element.Name else ""
|
||||||
|
return normalize_name(name)
|
||||||
|
elif object_type == "MATERIAL":
|
||||||
|
name = element.Name if element.Name else ""
|
||||||
|
return normalize_name(name)
|
||||||
|
elif object_type == "ORGANIZATION":
|
||||||
|
name = element.Name if element.Name else ""
|
||||||
|
return normalize_name(name)
|
||||||
|
elif object_type == "APPLICATION":
|
||||||
|
name = element.ApplicationFullName if element.ApplicationFullName else ""
|
||||||
|
return normalize_name(name)
|
||||||
|
elif object_type == "PERSON":
|
||||||
|
ident = element.Identification if element.Identification else ""
|
||||||
|
return normalize_name(ident)
|
||||||
|
elif object_type == "PERSON_AND_ORGANIZATION":
|
||||||
|
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
|
||||||
|
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
|
||||||
|
return f"{normalize_name(person_id)}|{normalize_name(org_name)}"
|
||||||
|
else:
|
||||||
|
assert_never(object_type)
|
||||||
|
|
||||||
|
def get_element_name(element: ifcopenshell.entity_instance) -> str:
|
||||||
|
"""Get the actual name/identifier from element for sorting purposes"""
|
||||||
|
if object_type == "STYLE":
|
||||||
|
return element.Name if element.Name else ""
|
||||||
|
elif object_type == "MATERIAL":
|
||||||
|
return element.Name if element.Name else ""
|
||||||
|
elif object_type == "ORGANIZATION":
|
||||||
|
return element.Name if element.Name else ""
|
||||||
|
elif object_type == "APPLICATION":
|
||||||
|
return element.ApplicationFullName if element.ApplicationFullName else ""
|
||||||
|
elif object_type == "PERSON":
|
||||||
|
return element.Identification if element.Identification else ""
|
||||||
|
elif object_type == "PERSON_AND_ORGANIZATION":
|
||||||
|
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
|
||||||
|
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
|
||||||
|
return f"{person_id}|{org_name}"
|
||||||
|
else:
|
||||||
|
assert_never(object_type)
|
||||||
|
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
merged_element_types: dict[str, list[str]] = {}
|
merged_element_types: dict[str, list[str]] = {}
|
||||||
|
|
||||||
@@ -179,22 +241,47 @@ class Debug(bonsai.core.tool.Debug):
|
|||||||
for element_type in element_types:
|
for element_type in element_types:
|
||||||
elements = ifc_file.by_type(element_type, include_subtypes=False)
|
elements = ifc_file.by_type(element_type, include_subtypes=False)
|
||||||
|
|
||||||
# Calculate hashes.
|
# Calculate hashes or name keys.
|
||||||
hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list)
|
hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
|
||||||
for element in elements:
|
|
||||||
# Except for styles, ignore unnamed elements as they may be not safe to merge
|
if by_name_or_identification_only:
|
||||||
merge_optional_names = ("STYLE", "PERSON")
|
# Group by name/identifier only (with .XXX suffix normalization)
|
||||||
not_optional_name = ("APPLICATION", "ORGANIZATION")
|
hash_to_elements = defaultdict(list)
|
||||||
has_no_name = ("PERSON_AND_ORGANIZATION",)
|
for element in elements:
|
||||||
if (
|
name_key = get_name_key(element)
|
||||||
object_type not in merge_optional_names
|
# Skip elements without a valid identifier
|
||||||
and object_type not in not_optional_name
|
if not name_key:
|
||||||
and object_type not in has_no_name
|
continue
|
||||||
and not element.Name
|
hash_to_elements[name_key].append(element)
|
||||||
):
|
|
||||||
continue
|
# Sort elements within each group to keep the one without suffix (or lowest suffix)
|
||||||
element_hash = get_hash(element)
|
for name_key in hash_to_elements:
|
||||||
hash_to_elements[element_hash].append(element)
|
# Sort by: 1) prefer names without .XXX suffix, 2) then by original name
|
||||||
|
def sort_key(el):
|
||||||
|
name = get_element_name(el)
|
||||||
|
# Check if name has .XXX suffix
|
||||||
|
has_suffix = bool(re.match(r"^.+\.\d+$", name))
|
||||||
|
# Return tuple: (has_suffix, name) - sort by no suffix first, then alphabetically
|
||||||
|
return (has_suffix, name)
|
||||||
|
|
||||||
|
hash_to_elements[name_key].sort(key=sort_key)
|
||||||
|
else:
|
||||||
|
# Group by full hash
|
||||||
|
hash_to_elements = defaultdict(list)
|
||||||
|
for element in elements:
|
||||||
|
# Except for styles, ignore unnamed elements as they may be not safe to merge
|
||||||
|
merge_optional_names = ("STYLE", "PERSON")
|
||||||
|
not_optional_name = ("APPLICATION", "ORGANIZATION")
|
||||||
|
has_no_name = ("PERSON_AND_ORGANIZATION",)
|
||||||
|
if (
|
||||||
|
object_type not in merge_optional_names
|
||||||
|
and object_type not in not_optional_name
|
||||||
|
and object_type not in has_no_name
|
||||||
|
and not element.Name
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
element_hash = get_hash(element)
|
||||||
|
hash_to_elements[element_hash].append(element)
|
||||||
|
|
||||||
merged_elements_names: list[str] = []
|
merged_elements_names: list[str] = []
|
||||||
# Merge elements.
|
# Merge elements.
|
||||||
|
|||||||
+498
-280
@@ -208,6 +208,27 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_annotation_drawing(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None:
|
||||||
|
for rel in element.HasAssignments:
|
||||||
|
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
|
||||||
|
for e in rel.RelatedObjects:
|
||||||
|
if e.ObjectType == "DRAWING":
|
||||||
|
return e
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def exclude_annotation_from_drawing(
|
||||||
|
cls, element: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance
|
||||||
|
) -> None:
|
||||||
|
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
|
||||||
|
if not pset:
|
||||||
|
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=drawing, name="EPset_Drawing")
|
||||||
|
exclude = ifcopenshell.util.element.get_property_definition(pset, prop="Exclude") or ""
|
||||||
|
if exclude:
|
||||||
|
exclude += "+"
|
||||||
|
exclude += element.GlobalId
|
||||||
|
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": exclude})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ensure_annotation_in_drawing_plane(
|
def ensure_annotation_in_drawing_plane(
|
||||||
cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None
|
cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None
|
||||||
@@ -218,11 +239,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
entity = tool.Ifc.get_entity(obj)
|
entity = tool.Ifc.get_entity(obj)
|
||||||
if not entity:
|
if not entity:
|
||||||
return
|
return
|
||||||
for rel in entity.HasAssignments:
|
return tool.Ifc.get_object(cls.get_annotation_drawing(entity))
|
||||||
if rel.is_a("IfcRelAssignsToGroup"):
|
|
||||||
for e in rel.RelatedObjects:
|
|
||||||
if e.ObjectType == "DRAWING":
|
|
||||||
return tool.Ifc.get_object(e)
|
|
||||||
|
|
||||||
if not camera:
|
if not camera:
|
||||||
camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera
|
camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera
|
||||||
@@ -236,7 +253,11 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_annotation_object(
|
def setup_annotation_object(
|
||||||
cls, obj: bpy.types.Object, object_type: str, related_object: Optional[bpy.types.Object] = None
|
cls,
|
||||||
|
obj: bpy.types.Object,
|
||||||
|
object_type: str,
|
||||||
|
related_object: Optional[bpy.types.Object] = None,
|
||||||
|
rotation_mode: str = "NONE",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Finish object's adjustments after both object and entity are created"""
|
"""Finish object's adjustments after both object and entity are created"""
|
||||||
|
|
||||||
@@ -308,6 +329,55 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
ifc_file, relating_product=related_entity, related_object=obj_entity
|
ifc_file, relating_product=related_entity, related_object=obj_entity
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if rotation_mode != "NONE" and related_object:
|
||||||
|
cls.apply_annotation_rotation(obj, related_object, rotation_mode)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apply_annotation_rotation(
|
||||||
|
cls, tag_obj: bpy.types.Object, related_object: bpy.types.Object, rotation_mode: str
|
||||||
|
) -> None:
|
||||||
|
"""Apply rotation to annotation based on the selected rotation mode"""
|
||||||
|
camera = bpy.context.scene.camera
|
||||||
|
camera_right = camera.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0))
|
||||||
|
|
||||||
|
if rotation_mode == "CAMERA_Horizontal":
|
||||||
|
location = tag_obj.location.copy()
|
||||||
|
camera_matrix = camera.matrix_world.copy()
|
||||||
|
camera_matrix.translation = location
|
||||||
|
tag_obj.matrix_world = camera_matrix
|
||||||
|
|
||||||
|
elif rotation_mode == "CAMERA_Vertical":
|
||||||
|
location = tag_obj.location.copy()
|
||||||
|
camera_matrix = camera.matrix_world.copy()
|
||||||
|
camera_matrix.translation = location
|
||||||
|
rotation_90z = mathutils.Matrix.Rotation(math.pi / 2, 4, "Z")
|
||||||
|
camera_matrix = camera_matrix @ rotation_90z
|
||||||
|
tag_obj.matrix_world = camera_matrix
|
||||||
|
|
||||||
|
elif rotation_mode == "LOCAL_X":
|
||||||
|
local_x = related_object.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0))
|
||||||
|
local_x = local_x.normalized()
|
||||||
|
if local_x.dot(camera_right) < 0:
|
||||||
|
local_x = -local_x
|
||||||
|
|
||||||
|
tag_obj.rotation_euler = local_x.to_track_quat("X", "Z").to_euler()
|
||||||
|
|
||||||
|
elif rotation_mode == "LOCAL_Y":
|
||||||
|
local_y = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 1, 0))
|
||||||
|
local_y = local_y.normalized()
|
||||||
|
if local_y.dot(camera_right) < 0:
|
||||||
|
local_y = -local_y
|
||||||
|
|
||||||
|
tag_obj.rotation_euler = local_y.to_track_quat("X", "Z").to_euler()
|
||||||
|
|
||||||
|
elif rotation_mode == "LOCAL_Z":
|
||||||
|
local_z = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))
|
||||||
|
local_z = local_z.normalized()
|
||||||
|
if local_z.dot(camera_right) < 0:
|
||||||
|
local_z = -local_z
|
||||||
|
|
||||||
|
tag_obj.rotation_euler = local_z.to_track_quat("X", "Z").to_euler()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_annotation_object_type(
|
def is_annotation_object_type(
|
||||||
cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]]
|
cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]]
|
||||||
@@ -815,7 +885,13 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
for rel in element.HasAssignments:
|
for rel in element.HasAssignments:
|
||||||
if rel.is_a("IfcRelAssignsToProduct"):
|
if rel.is_a("IfcRelAssignsToProduct"):
|
||||||
return rel.RelatingProduct
|
product = rel.RelatingProduct
|
||||||
|
if product.is_a("IfcGrid") and rel.Name:
|
||||||
|
for attribute in ("UAxes", "VAxes", "WAxes"):
|
||||||
|
for axis in getattr(product, attribute) or []:
|
||||||
|
if axis.AxisTag == rel.Name:
|
||||||
|
return axis
|
||||||
|
return product
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
|
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
|
||||||
@@ -1401,7 +1477,10 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
cls, drawing: ifcopenshell.entity_instance
|
cls, drawing: ifcopenshell.entity_instance
|
||||||
) -> list[ifcopenshell.entity_instance]:
|
) -> list[ifcopenshell.entity_instance]:
|
||||||
elements = []
|
elements = []
|
||||||
existing_references = cls.get_group_elements(cls.get_drawing_group(drawing))
|
existing_references = set(cls.get_group_elements(cls.get_drawing_group(drawing)))
|
||||||
|
if exclude := ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude"):
|
||||||
|
existing_references.update(ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), exclude))
|
||||||
|
|
||||||
for element in tool.Ifc.get().by_type("IfcAnnotation"):
|
for element in tool.Ifc.get().by_type("IfcAnnotation"):
|
||||||
if element in existing_references or element == drawing:
|
if element in existing_references or element == drawing:
|
||||||
continue
|
continue
|
||||||
@@ -1411,14 +1490,25 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
"GlobalReferencing", False
|
"GlobalReferencing", False
|
||||||
):
|
):
|
||||||
elements.append(element)
|
elements.append(element)
|
||||||
for element in tool.Ifc.get().by_type("IfcGridAxis"):
|
for element in tool.Ifc.get().by_type("IfcGrid"):
|
||||||
elements.append(element)
|
if element in existing_references:
|
||||||
|
continue
|
||||||
|
for axis in element.UAxes + element.VAxes + (element.WAxes or tuple()):
|
||||||
|
if axis in existing_references:
|
||||||
|
continue
|
||||||
|
elements.append(axis)
|
||||||
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
||||||
if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"):
|
if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"):
|
||||||
for element in tool.Ifc.get().by_type("IfcBuildingStorey"):
|
for element in tool.Ifc.get().by_type("IfcBuildingStorey"):
|
||||||
|
if element in existing_references:
|
||||||
|
continue
|
||||||
elements.append(element)
|
elements.append(element)
|
||||||
return elements
|
return elements
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_auto_annotation(cls, element: ifcopenshell.entity_instance):
|
||||||
|
return element.is_a("IfcAnnotation") and element.ObjectType in ("GRID", "SECTION", "ELEVATION", "SECTION_LEVEL")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_drawing_reference_annotation(
|
def get_drawing_reference_annotation(
|
||||||
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
|
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
|
||||||
@@ -1430,7 +1520,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
# IfcRelAssignsToProduct.RelatingProduct = IfcGrid
|
# IfcRelAssignsToProduct.RelatingProduct = IfcGrid
|
||||||
# IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag
|
# IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag
|
||||||
grid = None
|
grid = None
|
||||||
for attribute in ("PartOfW", "PartOfV", "PartOfU"):
|
for attribute in ("PartOfU", "PartOfV", "PartOfW"):
|
||||||
if getattr(reference_element, attribute, None):
|
if getattr(reference_element, attribute, None):
|
||||||
grid = getattr(reference_element, attribute)[0]
|
grid = getattr(reference_element, attribute)[0]
|
||||||
break
|
break
|
||||||
@@ -1452,6 +1542,26 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element:
|
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element:
|
||||||
return element
|
return element
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def regenerate_reference_annotation(
|
||||||
|
cls,
|
||||||
|
drawing: ifcopenshell.entity_instance,
|
||||||
|
annotation: ifcopenshell.entity_instance,
|
||||||
|
reference_element: ifcopenshell.entity_instance,
|
||||||
|
context: ifcopenshell.entity_instance,
|
||||||
|
) -> ifcopenshell.entity_instance:
|
||||||
|
if reference_element.is_a("IfcGridAxis"):
|
||||||
|
return cls.regenerate_grid_axis_reference_annotation(drawing, annotation, reference_element, context)
|
||||||
|
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
|
||||||
|
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
|
||||||
|
if target_view == "ELEVATION_VIEW":
|
||||||
|
return cls.regenerate_elevation_reference_annotation(drawing, annotation, reference_element, context)
|
||||||
|
elif target_view == "SECTION_VIEW":
|
||||||
|
return cls.regenerate_section_reference_annotation(drawing, annotation, reference_element, context)
|
||||||
|
elif reference_element.is_a("IfcBuildingStorey"):
|
||||||
|
return cls.regenerate_storey_annotation(drawing, annotation, reference_element, context)
|
||||||
|
return annotation
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_reference_annotation(
|
def generate_reference_annotation(
|
||||||
cls,
|
cls,
|
||||||
@@ -1461,333 +1571,410 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance:
|
||||||
if reference_element.is_a("IfcGridAxis"):
|
if reference_element.is_a("IfcGridAxis"):
|
||||||
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
|
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
|
||||||
|
|
||||||
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
|
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
|
||||||
|
|
||||||
def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
|
|
||||||
obj = tool.Ifc.get_object(drawing)
|
|
||||||
if obj is None:
|
|
||||||
# Annotations in that drawing are lazy loaded as needed
|
|
||||||
obj = cls.import_drawing(drawing)
|
|
||||||
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
|
|
||||||
|
|
||||||
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
|
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
|
||||||
if target_view == "ELEVATION_VIEW":
|
if target_view == "ELEVATION_VIEW":
|
||||||
ensure_referenced_drawing_obj_exists(reference_element)
|
|
||||||
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
|
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
|
||||||
elif target_view == "SECTION_VIEW":
|
elif target_view == "SECTION_VIEW":
|
||||||
ensure_referenced_drawing_obj_exists(reference_element)
|
|
||||||
return cls.generate_section_reference_annotation(drawing, reference_element, context)
|
return cls.generate_section_reference_annotation(drawing, reference_element, context)
|
||||||
|
|
||||||
elif reference_element.is_a("IfcBuildingStorey"):
|
elif reference_element.is_a("IfcBuildingStorey"):
|
||||||
return cls.generate_storey_annotation(drawing, reference_element, context)
|
return cls.generate_storey_annotation(drawing, reference_element, context)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_storey_points(
|
||||||
|
cls, drawing: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance
|
||||||
|
) -> list | None:
|
||||||
|
import bonsai.bim.module.drawing.helper as helper
|
||||||
|
|
||||||
|
camera = tool.Ifc.get_object(drawing)
|
||||||
|
if camera.data.type != "ORTHO":
|
||||||
|
return
|
||||||
|
if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix()):
|
||||||
|
return
|
||||||
|
|
||||||
|
xmin, xmax, ymin, ymax = helper.ortho_view_frame(camera.data)[:4]
|
||||||
|
rl = ifcopenshell.util.placement.get_local_placement(storey.ObjectPlacement)[2][3]
|
||||||
|
y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl))).y
|
||||||
|
if y < ymin or y > ymax:
|
||||||
|
return
|
||||||
|
|
||||||
|
return (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_storey_annotation(
|
def generate_storey_annotation(
|
||||||
cls,
|
cls,
|
||||||
drawing: ifcopenshell.entity_instance,
|
drawing: ifcopenshell.entity_instance,
|
||||||
reference_element: ifcopenshell.entity_instance,
|
storey: ifcopenshell.entity_instance,
|
||||||
context: ifcopenshell.entity_instance,
|
context: ifcopenshell.entity_instance,
|
||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance:
|
||||||
|
if not (points := cls.generate_storey_points(drawing, storey)):
|
||||||
|
return
|
||||||
|
|
||||||
|
camera = tool.Ifc.get_object(drawing)
|
||||||
|
mesh = bpy.data.meshes.new("Mesh")
|
||||||
|
obj = bpy.data.objects.new(storey.Name or "Unnamed", mesh)
|
||||||
|
obj.matrix_world = cls.get_default_annotation_matrix(camera)
|
||||||
|
element = cls.run_root_assign_class(
|
||||||
|
obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION_LEVEL", should_add_representation=False
|
||||||
|
)
|
||||||
|
tool.Geometry.run_edit_object_placement(obj)
|
||||||
|
element.Name = storey.Name or "Unnamed"
|
||||||
|
builder = ShapeBuilder(tool.Ifc.get())
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
points = [p / unit_scale for p in points]
|
||||||
|
representation = builder.get_representation(context, [builder.polyline(points)])
|
||||||
|
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||||
|
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
|
||||||
|
return element
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def regenerate_storey_annotation(
|
||||||
|
cls,
|
||||||
|
drawing: ifcopenshell.entity_instance,
|
||||||
|
annotation: ifcopenshell.entity_instance,
|
||||||
|
storey: ifcopenshell.entity_instance,
|
||||||
|
context: ifcopenshell.entity_instance,
|
||||||
|
) -> ifcopenshell.entity_instance:
|
||||||
|
if not (points := cls.generate_storey_points(drawing, storey)):
|
||||||
|
return
|
||||||
|
|
||||||
|
camera = tool.Ifc.get_object(drawing)
|
||||||
|
settings = ifcopenshell.geom.settings()
|
||||||
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||||
|
shape = ifcopenshell.geom.create_shape(settings, annotation)
|
||||||
|
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||||
|
mw = cls.get_default_annotation_matrix(camera)
|
||||||
|
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||||
|
|
||||||
|
new_points = None
|
||||||
|
if not np.allclose(m, np.array(mw), atol=1e-4):
|
||||||
|
new_points = points
|
||||||
|
elif len(existing_verts) != 2:
|
||||||
|
new_points = points
|
||||||
|
else:
|
||||||
|
existing_verts = sorted(existing_verts, key=lambda v: v.x)
|
||||||
|
xmin, xmax = [v.x for v in existing_verts]
|
||||||
|
y = points[0].y
|
||||||
|
if not tool.Cad.is_x(y, existing_verts[0].y) or not tool.Cad.is_x(y, existing_verts[1].y):
|
||||||
|
new_points = (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
|
||||||
|
|
||||||
|
if new_points:
|
||||||
|
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
|
||||||
|
ifcopenshell.api.geometry.unassign_representation(
|
||||||
|
tool.Ifc.get(), product=annotation, representation=representation
|
||||||
|
)
|
||||||
|
builder = ShapeBuilder(tool.Ifc.get())
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
new_points = [p / unit_scale for p in new_points]
|
||||||
|
representation = builder.get_representation(context, [builder.polyline(new_points)])
|
||||||
|
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
|
||||||
|
|
||||||
|
if obj := tool.Ifc.get_object(annotation):
|
||||||
|
obj.matrix_world = mw
|
||||||
|
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||||
|
bonsai.core.geometry.switch_representation(
|
||||||
|
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
|
||||||
|
|
||||||
|
annotation.Name = storey.Name or "Unnamed"
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_section_reference_points(
|
||||||
|
cls, drawing: ifcopenshell.entity_instance, section: ifcopenshell.entity_instance
|
||||||
|
) -> list | None:
|
||||||
import bonsai.bim.module.drawing.helper as helper
|
import bonsai.bim.module.drawing.helper as helper
|
||||||
|
|
||||||
camera = tool.Ifc.get_object(drawing)
|
camera = tool.Ifc.get_object(drawing)
|
||||||
assert isinstance(camera, bpy.types.Object)
|
if camera.data.type != "ORTHO":
|
||||||
assert isinstance((camera_data := camera.data), bpy.types.Camera)
|
return
|
||||||
props = tool.Drawing.get_camera_props(camera_data)
|
settings = ifcopenshell.geom.settings()
|
||||||
|
shape = ifcopenshell.geom.create_shape(settings, section)
|
||||||
|
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||||
|
if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix(m)):
|
||||||
|
return
|
||||||
|
if not cls.does_shape_intersect_camera(shape, camera):
|
||||||
|
return
|
||||||
|
|
||||||
bounds = helper.ortho_view_frame(camera_data) if camera_data.type == "ORTHO" else None
|
# Get cutting plane as a line
|
||||||
reference_obj = tool.Ifc.get_object(reference_element)
|
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
|
||||||
assert isinstance(reference_obj, bpy.types.Object)
|
cutting_plane_verts = sorted(verts, key=lambda v: v[2])[-4:]
|
||||||
|
v1, *_, v2 = sorted(cutting_plane_verts, key=lambda v: v[0]) # Cut is in +X direction
|
||||||
|
im = camera.matrix_world.inverted()
|
||||||
|
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in [v1, v2]]
|
||||||
|
|
||||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
bounds = helper.ortho_view_frame(camera.data)
|
||||||
mat = reference_obj.matrix_world.copy()
|
if not (points := helper.clip_segment(bounds, [v1, v2])):
|
||||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
return
|
||||||
xyz[2] = 0
|
for v in points:
|
||||||
xyz = camera.matrix_world @ xyz
|
v.z = 0
|
||||||
mat.translation = xyz
|
return points
|
||||||
annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
|
|
||||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
|
||||||
mat.translation += annotation_offset
|
|
||||||
return mat
|
|
||||||
|
|
||||||
def project_point_onto_camera(point: Vector, camera: bpy.types.Object) -> Vector:
|
|
||||||
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 props.raster_x > props.raster_y:
|
|
||||||
width = camera_data.ortho_scale
|
|
||||||
height = width / props.raster_x * props.raster_y
|
|
||||||
else:
|
|
||||||
height = camera_data.ortho_scale
|
|
||||||
width = height / props.raster_y * props.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,
|
|
||||||
)
|
|
||||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
|
||||||
cls.reload_representation(obj=obj, representation=representation)
|
|
||||||
bpy.data.curves.remove(data)
|
|
||||||
|
|
||||||
return element
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_section_reference_annotation(
|
def generate_section_reference_annotation(
|
||||||
cls,
|
cls,
|
||||||
drawing: ifcopenshell.entity_instance,
|
drawing: ifcopenshell.entity_instance,
|
||||||
reference_element: ifcopenshell.entity_instance,
|
section: ifcopenshell.entity_instance,
|
||||||
context: ifcopenshell.entity_instance,
|
context: ifcopenshell.entity_instance,
|
||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance:
|
||||||
import bonsai.bim.module.drawing.helper as helper
|
if not (points := cls.generate_section_reference_points(drawing, section)):
|
||||||
|
return
|
||||||
|
|
||||||
reference_obj = tool.Ifc.get_object(reference_element)
|
|
||||||
reference_obj.matrix_world
|
|
||||||
camera = tool.Ifc.get_object(drawing)
|
camera = tool.Ifc.get_object(drawing)
|
||||||
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
|
mesh = bpy.data.meshes.new("Mesh")
|
||||||
|
obj = bpy.data.objects.new(section.Name or "Unnamed", mesh)
|
||||||
|
obj.matrix_world = cls.get_default_annotation_matrix(camera)
|
||||||
|
element = cls.run_root_assign_class(
|
||||||
|
obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION", should_add_representation=False
|
||||||
|
)
|
||||||
|
element.Name = section.Name or "Unnamed"
|
||||||
|
builder = ShapeBuilder(tool.Ifc.get())
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
points = [p / unit_scale for p in points]
|
||||||
|
representation = builder.get_representation(context, [builder.polyline(points)])
|
||||||
|
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||||
|
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
|
||||||
|
return element
|
||||||
|
|
||||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
@classmethod
|
||||||
mat = reference_obj.matrix_world.copy()
|
def regenerate_section_reference_annotation(
|
||||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
cls,
|
||||||
xyz[2] = 0
|
drawing: ifcopenshell.entity_instance,
|
||||||
xyz = camera.matrix_world @ xyz
|
annotation: ifcopenshell.entity_instance,
|
||||||
mat.translation = xyz
|
section: ifcopenshell.entity_instance,
|
||||||
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
context: ifcopenshell.entity_instance,
|
||||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
) -> ifcopenshell.entity_instance:
|
||||||
mat.translation += annotation_offset
|
if not (points := cls.generate_section_reference_points(drawing, section)):
|
||||||
return mat
|
return
|
||||||
|
|
||||||
def clip_to_camera_boundary(
|
camera = tool.Ifc.get_object(drawing)
|
||||||
mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float]
|
settings = ifcopenshell.geom.settings()
|
||||||
) -> Union[bpy.types.Mesh, None]:
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||||
mesh.verts.ensure_lookup_table()
|
shape = ifcopenshell.geom.create_shape(settings, annotation)
|
||||||
points = [v.co for v in mesh.verts[0:2]]
|
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||||
points = helper.clip_segment(bounds, points)
|
mw = cls.get_default_annotation_matrix(camera)
|
||||||
if points is None:
|
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||||
return None
|
|
||||||
mesh.verts[0].co = points[0]
|
|
||||||
mesh.verts[1].co = points[1]
|
|
||||||
return mesh
|
|
||||||
|
|
||||||
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
|
new_points = None
|
||||||
reference_mesh = cls.get_camera_block(reference_obj)
|
if not np.allclose(m, np.array(mw), atol=1e-4):
|
||||||
obj_matrix = to_camera_coords(camera, reference_obj)
|
new_points = points
|
||||||
|
elif len(existing_verts) != 2:
|
||||||
|
new_points = points
|
||||||
|
else:
|
||||||
|
if not tool.Cad.are_edges_collinear(existing_verts, points):
|
||||||
|
# Attempt to update the section line by projecting existing verts onto the new line
|
||||||
|
v1 = tool.Cad.point_on_edge(existing_verts[0], points)
|
||||||
|
v2 = tool.Cad.point_on_edge(existing_verts[1], points)
|
||||||
|
existing_length = (existing_verts[0] - existing_verts[1]).length
|
||||||
|
new_length = (v2 - v1).length
|
||||||
|
if abs((existing_length - new_length) / existing_length) <= 0.10:
|
||||||
|
# If the projected line is within 10% of the previous length ...
|
||||||
|
new_points = (v1, v2)
|
||||||
|
else:
|
||||||
|
new_points = points
|
||||||
|
|
||||||
# The reference mesh vertices represent a view cube. To convert
|
if new_points:
|
||||||
# this into a section line we:
|
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
|
||||||
# 1. Select the 4 +Z vertices local to the reference element. This
|
ifcopenshell.api.geometry.unassign_representation(
|
||||||
# is the cutting plane.
|
tool.Ifc.get(), product=annotation, representation=representation
|
||||||
verts_local_to_reference = [reference_obj.matrix_world.inverted() @ v for v in reference_mesh["verts"]]
|
)
|
||||||
cutting_plane_verts = sorted(verts_local_to_reference, key=lambda x: x.z)[-4:]
|
builder = ShapeBuilder(tool.Ifc.get())
|
||||||
global_cutting_plane_verts = [reference_obj.matrix_world @ v for v in cutting_plane_verts]
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
# 2. Project the cutting plane onto our viewing camera.
|
new_points = [p / unit_scale for p in new_points]
|
||||||
verts_local_to_camera = [camera.matrix_world.inverted() @ v for v in global_cutting_plane_verts]
|
representation = builder.get_representation(context, [builder.polyline(new_points)])
|
||||||
# 3. Collapse verts with the same XY coords, and set Z to be just
|
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
|
||||||
# below the clip_start so it's visible
|
if obj := tool.Ifc.get_object(annotation):
|
||||||
collapsed_verts = []
|
obj.matrix_world = mw
|
||||||
for vert in verts_local_to_camera:
|
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||||
if not [True for v in collapsed_verts if (vert.xy - v.xy).length < 1e-2]:
|
bonsai.core.geometry.switch_representation(
|
||||||
collapsed_verts.append(mathutils.Vector((vert.x, vert.y, -camera.data.clip_start - 0.05)))
|
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||||
# 4. The first two vertices is the section line
|
)
|
||||||
section_line = collapsed_verts[0:2]
|
else:
|
||||||
# 5. Sort the vertices in the +X direction so that the vertices are
|
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
|
||||||
# ordered to "point" in the direction of the section cut.
|
annotation.Name = section.Name or "Unnamed"
|
||||||
section_line = sorted(
|
return annotation
|
||||||
section_line, key=lambda co: (reference_obj.matrix_world.inverted() @ camera.matrix_world @ co).x
|
|
||||||
)
|
|
||||||
global_section_line = [camera.matrix_world @ v for v in section_line]
|
|
||||||
local_section_line = [obj_matrix.inverted() @ v for v in global_section_line]
|
|
||||||
|
|
||||||
mesh = bpy.data.meshes.new(name="Annotation")
|
|
||||||
mesh.from_pydata(local_section_line, [(0, 1)], [])
|
|
||||||
bm = bmesh.new()
|
|
||||||
bm.from_mesh(mesh)
|
|
||||||
bm = clip_to_camera_boundary(bm, bounds)
|
|
||||||
bm.to_mesh(mesh)
|
|
||||||
bm.free()
|
|
||||||
|
|
||||||
obj = bpy.data.objects.new(reference_obj.name, mesh)
|
|
||||||
obj.matrix_world = obj_matrix
|
|
||||||
|
|
||||||
element = cls.run_root_assign_class(
|
|
||||||
obj=obj,
|
|
||||||
ifc_class="IfcAnnotation",
|
|
||||||
predefined_type="SECTION",
|
|
||||||
should_add_representation=True,
|
|
||||||
context=context,
|
|
||||||
ifc_representation_class=None,
|
|
||||||
)
|
|
||||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
|
||||||
cls.reload_representation(obj=obj, representation=representation)
|
|
||||||
return element
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_elevation_reference_annotation(
|
def generate_elevation_reference_annotation(
|
||||||
cls,
|
cls,
|
||||||
drawing: ifcopenshell.entity_instance,
|
drawing: ifcopenshell.entity_instance,
|
||||||
reference_element: ifcopenshell.entity_instance,
|
elevation: ifcopenshell.entity_instance,
|
||||||
context: ifcopenshell.entity_instance,
|
context: ifcopenshell.entity_instance,
|
||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance:
|
||||||
reference_obj = tool.Ifc.get_object(reference_element)
|
|
||||||
reference_obj.matrix_world
|
|
||||||
camera = tool.Ifc.get_object(drawing)
|
camera = tool.Ifc.get_object(drawing)
|
||||||
|
if camera.data.type != "ORTHO":
|
||||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
return
|
||||||
mat = reference_obj.matrix_world.copy()
|
settings = ifcopenshell.geom.settings()
|
||||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
shape = ifcopenshell.geom.create_shape(settings, elevation)
|
||||||
xyz[2] = 0
|
m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
|
||||||
xyz = camera.matrix_world @ xyz
|
if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
|
||||||
mat.translation = xyz
|
obj = bpy.data.objects.new(elevation.Name or "Unnamed", None)
|
||||||
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
|
||||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
|
||||||
mat.translation += annotation_offset
|
|
||||||
return mat
|
|
||||||
|
|
||||||
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
|
|
||||||
obj = bpy.data.objects.new(reference_obj.name, None)
|
|
||||||
obj.empty_display_size = 0.1
|
obj.empty_display_size = 0.1
|
||||||
obj.matrix_world = to_camera_coords(camera, reference_obj)
|
obj.matrix_world = cls.get_default_annotation_matrix(camera, matrix_world=m)
|
||||||
|
|
||||||
element = cls.run_root_assign_class(
|
element = cls.run_root_assign_class(
|
||||||
obj=obj,
|
obj=obj, ifc_class="IfcAnnotation", predefined_type="ELEVATION", should_add_representation=False
|
||||||
ifc_class="IfcAnnotation",
|
|
||||||
predefined_type="ELEVATION",
|
|
||||||
should_add_representation=False,
|
|
||||||
context=context,
|
|
||||||
ifc_representation_class=None,
|
|
||||||
)
|
)
|
||||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
element.Name = elevation.Name or "Unnamed"
|
||||||
cls.reload_representation(obj=obj, representation=representation)
|
|
||||||
return element
|
return element
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def regenerate_elevation_reference_annotation(
|
||||||
|
cls,
|
||||||
|
drawing: ifcopenshell.entity_instance,
|
||||||
|
annotation: ifcopenshell.entity_instance,
|
||||||
|
elevation: ifcopenshell.entity_instance,
|
||||||
|
context: ifcopenshell.entity_instance,
|
||||||
|
) -> ifcopenshell.entity_instance:
|
||||||
|
camera = tool.Ifc.get_object(drawing)
|
||||||
|
if camera.data.type != "ORTHO":
|
||||||
|
return
|
||||||
|
settings = ifcopenshell.geom.settings()
|
||||||
|
shape = ifcopenshell.geom.create_shape(settings, elevation)
|
||||||
|
m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
|
||||||
|
if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
|
||||||
|
existing_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(annotation.ObjectPlacement))
|
||||||
|
# The user is allowed to shift the elevation, but not rotate it
|
||||||
|
if not np.allclose(np.array(m.to_3x3()), np.array(existing_matrix.to_3x3()), atol=1e-4):
|
||||||
|
mw = cls.get_default_annotation_matrix(camera, matrix_world=m)
|
||||||
|
if obj := tool.Ifc.get_object(annotation):
|
||||||
|
obj.matrix_world = mw
|
||||||
|
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||||
|
else:
|
||||||
|
ifcopenshell.api.geometry.edit_object_placement(
|
||||||
|
tool.Ifc.get(), product=annotation, matrix=np.array(mw)
|
||||||
|
)
|
||||||
|
annotation.Name = elevation.Name or "Unnamed"
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_grid_axis_reference_points(
|
||||||
|
cls, drawing: ifcopenshell.entity_instance, axis: ifcopenshell.entity_instance
|
||||||
|
) -> list | None:
|
||||||
|
import bonsai.bim.module.drawing.helper as helper
|
||||||
|
|
||||||
|
camera = tool.Ifc.get_object(drawing)
|
||||||
|
if camera.data.type != "ORTHO":
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = ifcopenshell.geom.settings()
|
||||||
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||||
|
geometry = ifcopenshell.geom.create_shape(settings, axis.AxisCurve)
|
||||||
|
verts = ifcopenshell.util.shape.get_vertices(geometry)
|
||||||
|
grid = (axis.PartOfU or axis.PartOfV or axis.PartOfW)[0]
|
||||||
|
m = ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)
|
||||||
|
im = camera.matrix_world.inverted()
|
||||||
|
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in verts[:2]]
|
||||||
|
|
||||||
|
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
||||||
|
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"):
|
||||||
|
bounds = helper.ortho_view_frame(camera.data)
|
||||||
|
if not (points := helper.clip_segment(bounds, [v1, v2])):
|
||||||
|
return
|
||||||
|
elif target_view in ("ELEVATION_VIEW", "SECTION_VIEW"):
|
||||||
|
bounds = helper.ortho_view_frame(camera.data)
|
||||||
|
if not (points := helper.elevate_segment(bounds, [v1, v2])):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
for v in points:
|
||||||
|
v.z = 0
|
||||||
|
return points
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_grid_axis_reference_annotation(
|
def generate_grid_axis_reference_annotation(
|
||||||
cls,
|
cls,
|
||||||
drawing: ifcopenshell.entity_instance,
|
drawing: ifcopenshell.entity_instance,
|
||||||
reference_element: ifcopenshell.entity_instance,
|
axis: ifcopenshell.entity_instance,
|
||||||
context: ifcopenshell.entity_instance,
|
context: ifcopenshell.entity_instance,
|
||||||
) -> Union[ifcopenshell.entity_instance, None]:
|
) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
import bonsai.bim.module.drawing.helper as helper
|
if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
|
||||||
|
return
|
||||||
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
|
||||||
|
|
||||||
camera = tool.Ifc.get_object(drawing)
|
camera = tool.Ifc.get_object(drawing)
|
||||||
assert isinstance(camera, bpy.types.Object)
|
mesh = bpy.data.meshes.new("Mesh")
|
||||||
assert isinstance(camera_data := camera.data, bpy.types.Camera)
|
obj = bpy.data.objects.new(axis.AxisTag or "-", mesh)
|
||||||
|
obj.matrix_world = cls.get_default_annotation_matrix(camera)
|
||||||
is_ortho = camera_data.type == "ORTHO"
|
|
||||||
bounds = helper.ortho_view_frame(camera_data) if is_ortho else None
|
|
||||||
clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW")
|
|
||||||
elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW")
|
|
||||||
|
|
||||||
def clone(src: bpy.types.Object) -> bpy.types.Object:
|
|
||||||
dst = src.copy()
|
|
||||||
assert isinstance(dst.data, bpy.types.Mesh)
|
|
||||||
dst.data = dst.data.copy()
|
|
||||||
dst.name = dst.name.replace("IfcGridAxis/", "")
|
|
||||||
tool.Blender.get_object_bim_props(dst).ifc_definition_id = 0
|
|
||||||
tool.Geometry.get_mesh_props(dst.data).ifc_definition_id = 0
|
|
||||||
return dst
|
|
||||||
|
|
||||||
def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
|
|
||||||
assert isinstance(obj.data, bpy.types.Mesh)
|
|
||||||
mesh = bmesh.new()
|
|
||||||
mesh.verts.ensure_lookup_table()
|
|
||||||
mesh.from_mesh(obj.data)
|
|
||||||
return obj, mesh
|
|
||||||
|
|
||||||
def assemble(obj: bpy.types.Object, mesh: bmesh.types.BMesh) -> bpy.types.Object:
|
|
||||||
assert isinstance(obj.data, bpy.types.Mesh)
|
|
||||||
mesh.to_mesh(obj.data)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def to_camera_coords(
|
|
||||||
obj: bpy.types.Object, mesh: bmesh.types.BMesh
|
|
||||||
) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
|
|
||||||
mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
|
|
||||||
obj.matrix_world = camera.matrix_world
|
|
||||||
annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
|
|
||||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
|
||||||
obj.matrix_world.translation += annotation_offset
|
|
||||||
return obj, mesh
|
|
||||||
|
|
||||||
def clip_to_camera_boundary(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
|
|
||||||
mesh.verts.ensure_lookup_table()
|
|
||||||
points = [v.co for v in mesh.verts[0:2]]
|
|
||||||
points = helper.clip_segment(bounds, points)
|
|
||||||
if points is None:
|
|
||||||
return None
|
|
||||||
mesh.verts[0].co = points[0]
|
|
||||||
mesh.verts[1].co = points[1]
|
|
||||||
return mesh
|
|
||||||
|
|
||||||
def draw_grids_vertically(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
|
|
||||||
mesh.verts.ensure_lookup_table()
|
|
||||||
points = [v.co for v in mesh.verts[0:2]]
|
|
||||||
points = helper.elevate_segment(bounds, points)
|
|
||||||
if points is None:
|
|
||||||
return None
|
|
||||||
points = helper.clip_segment(bounds, points)
|
|
||||||
if points is None:
|
|
||||||
return None
|
|
||||||
mesh.verts[0].co = points[0]
|
|
||||||
mesh.verts[1].co = points[1]
|
|
||||||
return mesh
|
|
||||||
|
|
||||||
obj = tool.Ifc.get_object(reference_element)
|
|
||||||
if not obj:
|
|
||||||
return
|
|
||||||
assert isinstance(obj, bpy.types.Object)
|
|
||||||
obj, mesh = to_camera_coords(*disassemble(clone(obj)))
|
|
||||||
|
|
||||||
if clipping:
|
|
||||||
mesh = clip_to_camera_boundary(mesh)
|
|
||||||
elif elevating:
|
|
||||||
mesh = draw_grids_vertically(mesh)
|
|
||||||
|
|
||||||
if mesh is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
assemble(obj, mesh)
|
|
||||||
|
|
||||||
element = cls.run_root_assign_class(
|
element = cls.run_root_assign_class(
|
||||||
obj=obj,
|
obj=obj, ifc_class="IfcAnnotation", predefined_type="GRID", should_add_representation=False
|
||||||
ifc_class="IfcAnnotation",
|
|
||||||
predefined_type="GRID",
|
|
||||||
should_add_representation=True,
|
|
||||||
context=context,
|
|
||||||
ifc_representation_class=None,
|
|
||||||
)
|
)
|
||||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
element.Name = axis.AxisTag or "-"
|
||||||
cls.reload_representation(obj=obj, representation=representation)
|
builder = ShapeBuilder(tool.Ifc.get())
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
points = [p / unit_scale for p in points]
|
||||||
|
representation = builder.get_representation(context, [builder.polyline(points)])
|
||||||
|
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||||
|
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
|
||||||
return element
|
return element
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def regenerate_grid_axis_reference_annotation(
|
||||||
|
cls,
|
||||||
|
drawing: ifcopenshell.entity_instance,
|
||||||
|
annotation: ifcopenshell.entity_instance,
|
||||||
|
axis: ifcopenshell.entity_instance,
|
||||||
|
context: ifcopenshell.entity_instance,
|
||||||
|
) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
|
if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
|
||||||
|
return
|
||||||
|
|
||||||
|
camera = tool.Ifc.get_object(drawing)
|
||||||
|
settings = ifcopenshell.geom.settings()
|
||||||
|
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||||
|
shape = ifcopenshell.geom.create_shape(settings, annotation)
|
||||||
|
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||||
|
mw = cls.get_default_annotation_matrix(camera)
|
||||||
|
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||||
|
|
||||||
|
new_points = None
|
||||||
|
if not np.allclose(m, np.array(mw), atol=1e-4):
|
||||||
|
new_points = points
|
||||||
|
elif len(existing_verts) != 2:
|
||||||
|
new_points = points
|
||||||
|
else:
|
||||||
|
if not tool.Cad.are_edges_collinear(existing_verts, points):
|
||||||
|
# Attempt to update the section line by projecting existing verts onto the new line
|
||||||
|
v1 = tool.Cad.point_on_edge(existing_verts[0], points)
|
||||||
|
v2 = tool.Cad.point_on_edge(existing_verts[1], points)
|
||||||
|
existing_length = (existing_verts[0] - existing_verts[1]).length
|
||||||
|
new_length = (v2 - v1).length
|
||||||
|
if abs((existing_length - new_length) / existing_length) <= 0.10:
|
||||||
|
# If the projected line is within 10% of the previous length ...
|
||||||
|
new_points = (v1, v2)
|
||||||
|
else:
|
||||||
|
new_points = points
|
||||||
|
|
||||||
|
if new_points:
|
||||||
|
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
|
||||||
|
ifcopenshell.api.geometry.unassign_representation(
|
||||||
|
tool.Ifc.get(), product=annotation, representation=representation
|
||||||
|
)
|
||||||
|
builder = ShapeBuilder(tool.Ifc.get())
|
||||||
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
|
new_points = [p / unit_scale for p in new_points]
|
||||||
|
representation = builder.get_representation(context, [builder.polyline(new_points)])
|
||||||
|
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
|
||||||
|
if obj := tool.Ifc.get_object(annotation):
|
||||||
|
obj.matrix_world = mw
|
||||||
|
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||||
|
bonsai.core.geometry.switch_representation(
|
||||||
|
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
|
||||||
|
annotation.Name = axis.AxisTag or "-"
|
||||||
|
return annotation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default_annotation_matrix(cls, camera, matrix_world=None):
|
||||||
|
if matrix_world is None:
|
||||||
|
matrix_world = camera.matrix_world.copy()
|
||||||
|
annotation_offset = Vector((0, 0, -camera.data.clip_start - 0.05))
|
||||||
|
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
||||||
|
matrix_world.translation += annotation_offset
|
||||||
|
return matrix_world
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
|
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
|
||||||
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
|
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
|
||||||
@@ -1798,6 +1985,16 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_matrix_perpendicular(cls, a: Matrix, b: Matrix) -> bool:
|
||||||
|
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
|
||||||
|
a_quaternion = a.to_quaternion()
|
||||||
|
b_quaternion = b.to_quaternion()
|
||||||
|
for axis in axes:
|
||||||
|
if abs((a_quaternion @ axis).angle(b_quaternion @ axis) - (math.pi / 2)) < 1e-5:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_camera_block(cls, obj: bpy.types.Object) -> dict:
|
def get_camera_block(cls, obj: bpy.types.Object) -> dict:
|
||||||
assert isinstance(camera := obj.data, bpy.types.Camera)
|
assert isinstance(camera := obj.data, bpy.types.Camera)
|
||||||
@@ -1841,6 +2038,16 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"])
|
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"])
|
||||||
return bool(a_tree.overlap(b_tree))
|
return bool(a_tree.overlap(b_tree))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def does_shape_intersect_camera(cls, shape, camera) -> bool:
|
||||||
|
a_block = cls.get_camera_block(camera)
|
||||||
|
a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
|
||||||
|
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||||
|
verts = [(m @ np.append(v, 1.0))[:3] for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||||
|
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
|
||||||
|
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(verts, faces)
|
||||||
|
return bool(a_tree.overlap(b_tree))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def replace_text_literal_variables(
|
def replace_text_literal_variables(
|
||||||
cls,
|
cls,
|
||||||
@@ -2568,3 +2775,14 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference)
|
ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference)
|
||||||
|
|
||||||
tool.Drawing.import_sheets()
|
tool.Drawing.import_sheets()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def hide_all_drawing_collections(cls) -> None:
|
||||||
|
for element in tool.Ifc.get().by_type("IfcAnnotation"):
|
||||||
|
if element.ObjectType == "DRAWING" and (obj := tool.Ifc.get_object(element)):
|
||||||
|
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear_annotation_relationships(cls, drawing: ifcopenshell.entity_instance) -> None:
|
||||||
|
for rel in drawing.ReferencedBy:
|
||||||
|
tool.Ifc.get().remove(rel)
|
||||||
|
|||||||
@@ -233,20 +233,20 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if not element:
|
if not element:
|
||||||
return
|
return
|
||||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
elif element.is_a("IfcAnnotation"):
|
||||||
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
if element.ObjectType == "DRAWING":
|
||||||
|
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
||||||
|
elif tool.Drawing.is_auto_annotation(element):
|
||||||
|
return # For now, these are special referenced objects and cannot be deleted. Exclude instead.
|
||||||
elif element.is_a("IfcRelSpaceBoundary"):
|
elif element.is_a("IfcRelSpaceBoundary"):
|
||||||
ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element)
|
ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element)
|
||||||
tool.Boundary.undecorate_boundary(obj)
|
tool.Boundary.undecorate_boundary(obj)
|
||||||
return bpy.data.objects.remove(obj)
|
return bpy.data.objects.remove(obj)
|
||||||
elif element.is_a("IfcGridAxis"):
|
elif element.is_a("IfcGridAxis"):
|
||||||
is_last_axis = False
|
|
||||||
# Deleting the last W axis is OK
|
# Deleting the last W axis is OK
|
||||||
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
||||||
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
|
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
|
||||||
):
|
):
|
||||||
is_last_axis = True
|
|
||||||
if is_last_axis:
|
|
||||||
return
|
return
|
||||||
ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element)
|
ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element)
|
||||||
return bpy.data.objects.remove(obj)
|
return bpy.data.objects.remove(obj)
|
||||||
|
|||||||
@@ -1399,7 +1399,6 @@ class Model(bonsai.core.tool.Model):
|
|||||||
|
|
||||||
number_of_risers = number_of_treads + 1
|
number_of_risers = number_of_treads + 1
|
||||||
tread_rise = height / number_of_risers
|
tread_rise = height / number_of_risers
|
||||||
custom_tread_run = any(run != 0 for run in custom_first_last_tread_run)
|
|
||||||
nosing_overlap = max(nosing_length, 0)
|
nosing_overlap = max(nosing_length, 0)
|
||||||
nosing_tread_gap = -min(nosing_length, 0)
|
nosing_tread_gap = -min(nosing_length, 0)
|
||||||
nosing_overlap_offset = -V_(nosing_overlap, 0)
|
nosing_overlap_offset = -V_(nosing_overlap, 0)
|
||||||
@@ -1430,19 +1429,26 @@ class Model(bonsai.core.tool.Model):
|
|||||||
default_tread_offset = Vector([tread_run, tread_rise])
|
default_tread_offset = Vector([tread_run, tread_rise])
|
||||||
|
|
||||||
def get_tread_data(i):
|
def get_tread_data(i):
|
||||||
if custom_tread_run:
|
# Check if this is first or last tread with custom run
|
||||||
current_tread_run = None
|
current_tread_run = None
|
||||||
if i == 0:
|
if i == 0 and custom_first_last_tread_run[0] is not None:
|
||||||
current_tread_run = custom_first_last_tread_run[0]
|
current_tread_run = custom_first_last_tread_run[0]
|
||||||
elif i == number_of_risers - 1:
|
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
|
||||||
current_tread_run = custom_first_last_tread_run[1]
|
current_tread_run = custom_first_last_tread_run[1]
|
||||||
|
|
||||||
|
if current_tread_run is not None:
|
||||||
|
tread_offset = default_tread_offset.copy()
|
||||||
|
tread_offset.x = current_tread_run
|
||||||
|
|
||||||
|
# Handle zero-width treads
|
||||||
|
if current_tread_run == 0:
|
||||||
|
# For zero width, just return vertical offset with no horizontal tread
|
||||||
|
return tread_offset, ()
|
||||||
|
|
||||||
|
tread_verts = deepcopy(default_tread_verts)
|
||||||
|
tread_verts[-1].x = current_tread_run
|
||||||
|
return tread_offset, tread_verts
|
||||||
|
|
||||||
if current_tread_run:
|
|
||||||
tread_offset = default_tread_offset.copy()
|
|
||||||
tread_offset.x = current_tread_run
|
|
||||||
tread_verts = deepcopy(default_tread_verts)
|
|
||||||
tread_verts[-1].x = current_tread_run
|
|
||||||
return tread_offset, tread_verts
|
|
||||||
return default_tread_offset, default_tread_verts
|
return default_tread_offset, default_tread_verts
|
||||||
|
|
||||||
# treads
|
# treads
|
||||||
@@ -1450,9 +1456,13 @@ class Model(bonsai.core.tool.Model):
|
|||||||
for i in range(number_of_risers):
|
for i in range(number_of_risers):
|
||||||
last_vert_i = len(vertices) - 1
|
last_vert_i = len(vertices) - 1
|
||||||
tread_offset, tread_verts = get_tread_data(i)
|
tread_offset, tread_verts = get_tread_data(i)
|
||||||
current_tread_verts = [v + current_offset for v in tread_verts]
|
|
||||||
edges.extend(default_tread_edges + last_vert_i)
|
# Skip adding vertices/edges for zero-width treads
|
||||||
vertices.extend(current_tread_verts)
|
if tread_verts:
|
||||||
|
current_tread_verts = [v + current_offset for v in tread_verts]
|
||||||
|
edges.extend(default_tread_edges + last_vert_i)
|
||||||
|
vertices.extend(current_tread_verts)
|
||||||
|
|
||||||
current_offset += tread_offset
|
current_offset += tread_offset
|
||||||
|
|
||||||
if stair_type == "WOOD/STEEL":
|
if stair_type == "WOOD/STEEL":
|
||||||
@@ -1467,35 +1477,47 @@ class Model(bonsai.core.tool.Model):
|
|||||||
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
|
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
|
||||||
|
|
||||||
def get_tread_data(i):
|
def get_tread_data(i):
|
||||||
if custom_tread_run:
|
# Check if this is first or last tread with custom run
|
||||||
current_tread_run = None
|
current_tread_run = None
|
||||||
if i == 0 and custom_first_last_tread_run[0] != 0:
|
if i == 0 and custom_first_last_tread_run[0] is not None:
|
||||||
current_tread_run = custom_first_last_tread_run[0]
|
current_tread_run = custom_first_last_tread_run[0]
|
||||||
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 0:
|
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
|
||||||
current_tread_run = custom_first_last_tread_run[1]
|
current_tread_run = custom_first_last_tread_run[1]
|
||||||
|
|
||||||
|
if current_tread_run is not None:
|
||||||
|
tread_offset = default_tread_offset.copy()
|
||||||
|
tread_offset.x = current_tread_run + nosing_tread_gap
|
||||||
|
|
||||||
|
# Handle zero-width treads
|
||||||
|
if current_tread_run == 0:
|
||||||
|
return tread_offset, ()
|
||||||
|
|
||||||
|
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
|
||||||
|
return tread_offset, tread_verts
|
||||||
|
|
||||||
if current_tread_run:
|
|
||||||
tread_offset = default_tread_offset.copy()
|
|
||||||
tread_offset.x = current_tread_run + nosing_tread_gap
|
|
||||||
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
|
|
||||||
return tread_offset, tread_verts
|
|
||||||
return default_tread_offset, default_tread_verts
|
return default_tread_offset, default_tread_verts
|
||||||
|
|
||||||
# each tread is a separate shape
|
# each tread is a separate shape
|
||||||
cur_offset = V_(0, 0)
|
cur_offset = V_(0, 0)
|
||||||
|
tread_index = 0
|
||||||
for i in range(number_of_risers):
|
for i in range(number_of_risers):
|
||||||
tread_offset, tread_verts = get_tread_data(i)
|
tread_offset, tread_verts = get_tread_data(i)
|
||||||
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
|
|
||||||
vertices.extend(cur_trade_shape)
|
|
||||||
|
|
||||||
cur_vertex = i * 4
|
# Skip adding vertices/edges for zero-width treads
|
||||||
verts_to_add = (
|
if tread_verts:
|
||||||
(cur_vertex, cur_vertex + 1),
|
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
|
||||||
(cur_vertex + 1, cur_vertex + 2),
|
vertices.extend(cur_trade_shape)
|
||||||
(cur_vertex + 2, cur_vertex + 3),
|
|
||||||
(cur_vertex + 3, cur_vertex),
|
cur_vertex = tread_index * 4
|
||||||
)
|
verts_to_add = (
|
||||||
edges.extend(verts_to_add)
|
(cur_vertex, cur_vertex + 1),
|
||||||
|
(cur_vertex + 1, cur_vertex + 2),
|
||||||
|
(cur_vertex + 2, cur_vertex + 3),
|
||||||
|
(cur_vertex + 3, cur_vertex),
|
||||||
|
)
|
||||||
|
edges.extend(verts_to_add)
|
||||||
|
tread_index += 1
|
||||||
|
|
||||||
cur_offset += tread_offset
|
cur_offset += tread_offset
|
||||||
|
|
||||||
elif stair_type == "GENERIC":
|
elif stair_type == "GENERIC":
|
||||||
|
|||||||
@@ -512,14 +512,12 @@ class Sequence(bonsai.core.tool.Sequence):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_task_inputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
def get_task_inputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||||
props = cls.get_work_schedule_props()
|
props = cls.get_work_schedule_props()
|
||||||
is_deep = props.show_nested_inputs
|
return ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=props.show_nested_inputs)
|
||||||
return ifcopenshell.util.sequence.get_task_inputs(task, is_deep)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_task_outputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
def get_task_outputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
||||||
props = cls.get_work_schedule_props()
|
props = cls.get_work_schedule_props()
|
||||||
is_deep = props.show_nested_outputs
|
return ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=props.show_nested_outputs)
|
||||||
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def are_entities_same_class(cls, entities: list[ifcopenshell.entity_instance]) -> bool:
|
def are_entities_same_class(cls, entities: list[ifcopenshell.entity_instance]) -> bool:
|
||||||
@@ -540,8 +538,7 @@ class Sequence(bonsai.core.tool.Sequence):
|
|||||||
if not task:
|
if not task:
|
||||||
return
|
return
|
||||||
props = cls.get_work_schedule_props()
|
props = cls.get_work_schedule_props()
|
||||||
is_deep = props.show_nested_resources
|
return ifcopenshell.util.sequence.get_task_resources(task, props.show_nested_resources)
|
||||||
return ifcopenshell.util.sequence.get_task_resources(task, is_deep)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_task_inputs(cls, inputs: list[ifcopenshell.entity_instance]) -> None:
|
def load_task_inputs(cls, inputs: list[ifcopenshell.entity_instance]) -> None:
|
||||||
@@ -1582,7 +1579,7 @@ class Sequence(bonsai.core.tool.Sequence):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def create_new_task_json(cls, task, json, type_map=None, baseline_schedule=None):
|
def create_new_task_json(cls, task, json, type_map=None, baseline_schedule=None):
|
||||||
task_time = task.TaskTime
|
task_time = task.TaskTime
|
||||||
resources = ifcopenshell.util.sequence.get_task_resources(task, is_deep=False)
|
resources = ifcopenshell.util.sequence.get_task_resources(task, is_recursive=False)
|
||||||
|
|
||||||
string_resources = ""
|
string_resources = ""
|
||||||
resources_usage = ""
|
resources_usage = ""
|
||||||
@@ -1689,8 +1686,8 @@ class Sequence(bonsai.core.tool.Sequence):
|
|||||||
) -> list[ifcopenshell.entity_instance]:
|
) -> list[ifcopenshell.entity_instance]:
|
||||||
products = []
|
products = []
|
||||||
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
|
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
|
||||||
products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_deep=True))
|
products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=True))
|
||||||
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True))
|
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=True))
|
||||||
return products
|
return products
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1789,6 +1786,16 @@ class Sequence(bonsai.core.tool.Sequence):
|
|||||||
cls.load_task_outputs(outputs)
|
cls.load_task_outputs(outputs)
|
||||||
cls.load_task_resources(task)
|
cls.load_task_resources(task)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def select_active_task_elements(cls, task):
|
||||||
|
if not task:
|
||||||
|
return
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
props = cls.get_work_schedule_props()
|
||||||
|
for element in cls.get_task_inputs(task) | cls.get_task_outputs(task):
|
||||||
|
if obj := tool.Ifc.get_object(element):
|
||||||
|
obj.select_set(True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def refresh_task_resources(cls):
|
def refresh_task_resources(cls):
|
||||||
task = cls.get_highlighted_task()
|
task = cls.get_highlighted_task()
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ class Spatial(bonsai.core.tool.Spatial):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None:
|
def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None:
|
||||||
bpy.ops.object.select_all(action="DESELECT")
|
|
||||||
for product in products:
|
for product in products:
|
||||||
obj = tool.Ifc.get_object(product)
|
obj = tool.Ifc.get_object(product)
|
||||||
if obj and bpy.context.view_layer.objects.get(obj.name):
|
if obj and bpy.context.view_layer.objects.get(obj.name):
|
||||||
|
|||||||
@@ -176,6 +176,27 @@ class TestStairCalculatedParams(NewFile):
|
|||||||
calculated_data["Length"] += -0.2 + 0.1
|
calculated_data["Length"] += -0.2 + 0.1
|
||||||
self.compare_data(pset_data, calculated_data)
|
self.compare_data(pset_data, calculated_data)
|
||||||
|
|
||||||
|
# zero-width first tread
|
||||||
|
pset_data = pset_data_base.copy()
|
||||||
|
calculated_data = calculated_data_base.copy()
|
||||||
|
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
|
||||||
|
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
||||||
|
self.compare_data(pset_data, calculated_data)
|
||||||
|
|
||||||
|
# zero-width last tread
|
||||||
|
pset_data = pset_data_base.copy()
|
||||||
|
calculated_data = calculated_data_base.copy()
|
||||||
|
pset_data["custom_first_last_tread_run"] = (0.3, 0.0)
|
||||||
|
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
||||||
|
self.compare_data(pset_data, calculated_data)
|
||||||
|
|
||||||
|
# both first and last treads zero-width
|
||||||
|
pset_data = pset_data_base.copy()
|
||||||
|
calculated_data = calculated_data_base.copy()
|
||||||
|
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
|
||||||
|
calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
|
||||||
|
self.compare_data(pset_data, calculated_data)
|
||||||
|
|
||||||
# overlap affects stair length only by first tread
|
# overlap affects stair length only by first tread
|
||||||
pset_data = pset_data_base.copy()
|
pset_data = pset_data_base.copy()
|
||||||
calculated_data = calculated_data_base.copy()
|
calculated_data = calculated_data_base.copy()
|
||||||
@@ -296,6 +317,94 @@ class TestGenerateStair2DProfile(NewFile):
|
|||||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||||
self.compare_data(generated_profile, expected_profile)
|
self.compare_data(generated_profile, expected_profile)
|
||||||
|
|
||||||
|
def test_create_concrete_stair_zero_width_first_tread(self):
|
||||||
|
"""Test concrete stair with zero-width first tread"""
|
||||||
|
kwargs = {
|
||||||
|
"base_slab_depth": 0.25,
|
||||||
|
"has_top_nib": False,
|
||||||
|
"height": 1.0,
|
||||||
|
"number_of_treads": 3,
|
||||||
|
"stair_type": "CONCRETE",
|
||||||
|
"top_slab_depth": 0.25,
|
||||||
|
"tread_depth": 0.25,
|
||||||
|
"tread_run": 0.3,
|
||||||
|
"width": 1.2,
|
||||||
|
"custom_first_last_tread_run": (0.0, 0.0),
|
||||||
|
}
|
||||||
|
verts_data = (
|
||||||
|
V(0.0, 0, 0.0),
|
||||||
|
# First tread skipped - goes straight to second tread
|
||||||
|
V(0.0, 0, 0.5),
|
||||||
|
V(0.3, 0, 0.5),
|
||||||
|
V(0.3, 0, 0.75),
|
||||||
|
V(0.6, 0, 0.75),
|
||||||
|
V(0.6, 0, 1.0),
|
||||||
|
V(0.9, 0, 1.0),
|
||||||
|
V(0.9, 0, 0.67457),
|
||||||
|
V(0.0, 0, -0.25),
|
||||||
|
)
|
||||||
|
edges_data = (
|
||||||
|
(0, 1),
|
||||||
|
(1, 2),
|
||||||
|
(2, 3),
|
||||||
|
(3, 4),
|
||||||
|
(4, 5),
|
||||||
|
(5, 6),
|
||||||
|
(6, 7),
|
||||||
|
(8, 0),
|
||||||
|
(7, 8),
|
||||||
|
)
|
||||||
|
edges_data = [e[::-1] for e in edges_data]
|
||||||
|
faces_data = ()
|
||||||
|
expected_profile = (verts_data, edges_data, faces_data)
|
||||||
|
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||||
|
self.compare_data(generated_profile, expected_profile)
|
||||||
|
|
||||||
|
def test_create_concrete_stair_zero_width_last_tread(self):
|
||||||
|
"""Test concrete stair with zero-width last tread"""
|
||||||
|
kwargs = {
|
||||||
|
"base_slab_depth": 0.25,
|
||||||
|
"has_top_nib": False,
|
||||||
|
"height": 1.0,
|
||||||
|
"number_of_treads": 3,
|
||||||
|
"stair_type": "CONCRETE",
|
||||||
|
"top_slab_depth": 0.25,
|
||||||
|
"tread_depth": 0.25,
|
||||||
|
"tread_run": 0.3,
|
||||||
|
"width": 1.2,
|
||||||
|
"custom_first_last_tread_run": (0.0, 0.0),
|
||||||
|
}
|
||||||
|
verts_data = (
|
||||||
|
V(0.0, 0, 0.0),
|
||||||
|
V(0.0, 0, 0.25),
|
||||||
|
V(0.3, 0, 0.25),
|
||||||
|
V(0.3, 0, 0.5),
|
||||||
|
V(0.6, 0, 0.5),
|
||||||
|
V(0.6, 0, 0.75),
|
||||||
|
V(0.9, 0, 0.75),
|
||||||
|
# Last tread skipped
|
||||||
|
V(0.9, 0, 0.67457),
|
||||||
|
V(0.1, 0, -0.25),
|
||||||
|
V(0.0, 0, -0.25),
|
||||||
|
)
|
||||||
|
edges_data = (
|
||||||
|
(0, 1),
|
||||||
|
(1, 2),
|
||||||
|
(2, 3),
|
||||||
|
(3, 4),
|
||||||
|
(4, 5),
|
||||||
|
(5, 6),
|
||||||
|
(6, 7),
|
||||||
|
(9, 0),
|
||||||
|
(8, 9),
|
||||||
|
(7, 8),
|
||||||
|
)
|
||||||
|
edges_data = [e[::-1] for e in edges_data]
|
||||||
|
faces_data = ()
|
||||||
|
expected_profile = (verts_data, edges_data, faces_data)
|
||||||
|
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||||
|
self.compare_data(generated_profile, expected_profile)
|
||||||
|
|
||||||
def test_create_wood_steel_stair(self):
|
def test_create_wood_steel_stair(self):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"height": 1.0,
|
"height": 1.0,
|
||||||
@@ -348,6 +457,100 @@ class TestGenerateStair2DProfile(NewFile):
|
|||||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||||
self.compare_data(generated_profile, expected_profile)
|
self.compare_data(generated_profile, expected_profile)
|
||||||
|
|
||||||
|
def test_create_wood_steel_stair_zero_width_first_tread(self):
|
||||||
|
"""Test wood/steel stair with zero-width first tread"""
|
||||||
|
kwargs = {
|
||||||
|
"height": 1.0,
|
||||||
|
"number_of_treads": 3,
|
||||||
|
"stair_type": "WOOD/STEEL",
|
||||||
|
"tread_depth": 0.25,
|
||||||
|
"tread_run": 0.3,
|
||||||
|
"width": 1.2,
|
||||||
|
"custom_first_last_tread_run": (0.0, 0.0),
|
||||||
|
}
|
||||||
|
verts_data = (
|
||||||
|
# First tread skipped - start at second tread
|
||||||
|
V(0.0, 0, 0.25),
|
||||||
|
V(0.3, 0, 0.25),
|
||||||
|
V(0.3, 0, 0.5),
|
||||||
|
V(0.0, 0, 0.5),
|
||||||
|
V(0.3, 0, 0.5),
|
||||||
|
V(0.6, 0, 0.5),
|
||||||
|
V(0.6, 0, 0.75),
|
||||||
|
V(0.3, 0, 0.75),
|
||||||
|
V(0.6, 0, 0.75),
|
||||||
|
V(0.9, 0, 0.75),
|
||||||
|
V(0.9, 0, 1.0),
|
||||||
|
V(0.6, 0, 1.0),
|
||||||
|
)
|
||||||
|
edges_data = (
|
||||||
|
(0, 1),
|
||||||
|
(1, 2),
|
||||||
|
(2, 3),
|
||||||
|
(3, 0),
|
||||||
|
(4, 5),
|
||||||
|
(5, 6),
|
||||||
|
(6, 7),
|
||||||
|
(7, 4),
|
||||||
|
(8, 9),
|
||||||
|
(9, 10),
|
||||||
|
(10, 11),
|
||||||
|
(11, 8),
|
||||||
|
)
|
||||||
|
|
||||||
|
faces_data = ()
|
||||||
|
|
||||||
|
expected_profile = (verts_data, edges_data, faces_data)
|
||||||
|
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||||
|
self.compare_data(generated_profile, expected_profile)
|
||||||
|
|
||||||
|
def test_create_wood_steel_stair_zero_width_last_tread(self):
|
||||||
|
"""Test wood/steel stair with zero-width last tread"""
|
||||||
|
kwargs = {
|
||||||
|
"height": 1.0,
|
||||||
|
"number_of_treads": 3,
|
||||||
|
"stair_type": "WOOD/STEEL",
|
||||||
|
"tread_depth": 0.25,
|
||||||
|
"tread_run": 0.3,
|
||||||
|
"width": 1.2,
|
||||||
|
"custom_first_last_tread_run": (0.0, 0.0),
|
||||||
|
}
|
||||||
|
verts_data = (
|
||||||
|
V(0.0, 0, 0.0),
|
||||||
|
V(0.3, 0, 0.0),
|
||||||
|
V(0.3, 0, 0.25),
|
||||||
|
V(0.0, 0, 0.25),
|
||||||
|
V(0.3, 0, 0.25),
|
||||||
|
V(0.6, 0, 0.25),
|
||||||
|
V(0.6, 0, 0.5),
|
||||||
|
V(0.3, 0, 0.5),
|
||||||
|
V(0.6, 0, 0.5),
|
||||||
|
V(0.9, 0, 0.5),
|
||||||
|
V(0.9, 0, 0.75),
|
||||||
|
V(0.6, 0, 0.75),
|
||||||
|
# Last tread skipped
|
||||||
|
)
|
||||||
|
edges_data = (
|
||||||
|
(0, 1),
|
||||||
|
(1, 2),
|
||||||
|
(2, 3),
|
||||||
|
(3, 0),
|
||||||
|
(4, 5),
|
||||||
|
(5, 6),
|
||||||
|
(6, 7),
|
||||||
|
(7, 4),
|
||||||
|
(8, 9),
|
||||||
|
(9, 10),
|
||||||
|
(10, 11),
|
||||||
|
(11, 8),
|
||||||
|
)
|
||||||
|
|
||||||
|
faces_data = ()
|
||||||
|
|
||||||
|
expected_profile = (verts_data, edges_data, faces_data)
|
||||||
|
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||||
|
self.compare_data(generated_profile, expected_profile)
|
||||||
|
|
||||||
def test_create_generic_stair(self):
|
def test_create_generic_stair(self):
|
||||||
kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2}
|
kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2}
|
||||||
verts_data = (
|
verts_data = (
|
||||||
@@ -381,6 +584,41 @@ class TestGenerateStair2DProfile(NewFile):
|
|||||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||||
self.compare_data(generated_profile, expected_profile)
|
self.compare_data(generated_profile, expected_profile)
|
||||||
|
|
||||||
|
def test_create_generic_stair_zero_width_treads(self):
|
||||||
|
"""Test generic stair with zero-width first and last treads"""
|
||||||
|
kwargs = {
|
||||||
|
"height": 1.0,
|
||||||
|
"number_of_treads": 3,
|
||||||
|
"stair_type": "GENERIC",
|
||||||
|
"tread_run": 0.3,
|
||||||
|
"width": 1.2,
|
||||||
|
"custom_first_last_tread_run": (0.0, 0.0),
|
||||||
|
}
|
||||||
|
verts_data = (
|
||||||
|
V(0.0, 0, 0.0),
|
||||||
|
# First tread skipped
|
||||||
|
V(0.0, 0, 0.5),
|
||||||
|
V(0.3, 0, 0.5),
|
||||||
|
V(0.3, 0, 0.75),
|
||||||
|
V(0.6, 0, 0.75),
|
||||||
|
# Last tread skipped
|
||||||
|
V(0.6, 0, 0.0),
|
||||||
|
)
|
||||||
|
edges_data = (
|
||||||
|
(0, 1),
|
||||||
|
(1, 2),
|
||||||
|
(2, 3),
|
||||||
|
(3, 4),
|
||||||
|
(4, 5),
|
||||||
|
(5, 0),
|
||||||
|
)
|
||||||
|
edges_data = [e[::-1] for e in edges_data]
|
||||||
|
|
||||||
|
faces_data = ()
|
||||||
|
expected_profile = (verts_data, edges_data, faces_data)
|
||||||
|
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||||
|
self.compare_data(generated_profile, expected_profile)
|
||||||
|
|
||||||
|
|
||||||
class TestUsingArrays(NewFile):
|
class TestUsingArrays(NewFile):
|
||||||
def setup_array(self, add_second_layer=False, sync_children=False):
|
def setup_array(self, add_second_layer=False, sync_children=False):
|
||||||
|
|||||||
+2
-2
@@ -96,11 +96,11 @@ writer.write()
|
|||||||
### CLI app for converting IFC files to CSV, ODS or XLSX format.
|
### CLI app for converting IFC files to CSV, ODS or XLSX format.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python ifc5Dspreadsheet.py input_file output_file [-l log_file] [-f format_type]
|
python ifc5Dspreadsheet.py input_file output_dir [-l log_file] [-f format_type]
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
input_file (str): The path to the input IFC file to process.
|
input_file (str): The path to the input IFC file to process.
|
||||||
output_file (str): The output directory for CSV or filename for other formats.
|
output_dir (str): The output directory.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-l, --log log_file (str): The path to the file where errors should be logged. Default is process.log.
|
-l, --log log_file (str): The path to the file where errors should be logged. Default is process.log.
|
||||||
|
|||||||
@@ -319,8 +319,8 @@ class Ifc5Dwriter:
|
|||||||
cost_schedule: Optional[ifcopenshell.entity_instance] = None,
|
cost_schedule: Optional[ifcopenshell.entity_instance] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param file: IFC file to exprot cost schedules from.
|
:param file: IFC file to export cost schedules from.
|
||||||
:param output: Output directory for csv files.
|
:param output: Output directory.
|
||||||
:param cost_schedule: exported cost schedule. If not provided, will export all available schedules.
|
:param cost_schedule: exported cost schedule. If not provided, will export all available schedules.
|
||||||
"""
|
"""
|
||||||
self.output = output
|
self.output = output
|
||||||
@@ -404,6 +404,7 @@ class Ifc5DCsvWriter(Ifc5Dwriter):
|
|||||||
import csv
|
import csv
|
||||||
|
|
||||||
super().write()
|
super().write()
|
||||||
|
os.makedirs(self.output, exist_ok=True)
|
||||||
for sheet, data in self.sheet_data.items():
|
for sheet, data in self.sheet_data.items():
|
||||||
with open(
|
with open(
|
||||||
os.path.join(self.output, "{}.csv".format(data["Name"])), "w", newline="", encoding="utf-8"
|
os.path.join(self.output, "{}.csv".format(data["Name"])), "w", newline="", encoding="utf-8"
|
||||||
@@ -436,6 +437,7 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
|
|||||||
ns1.addElement(Number(decimalplaces="2", minintegerdigits="1", grouping="true"))
|
ns1.addElement(Number(decimalplaces="2", minintegerdigits="1", grouping="true"))
|
||||||
self.doc.styles.addElement(ns1)
|
self.doc.styles.addElement(ns1)
|
||||||
|
|
||||||
|
os.makedirs(self.output, exist_ok=True)
|
||||||
file_name = ""
|
file_name = ""
|
||||||
for cost_schedule in self.cost_schedules:
|
for cost_schedule in self.cost_schedules:
|
||||||
if file_name:
|
if file_name:
|
||||||
@@ -537,6 +539,7 @@ class Ifc5DXlsxWriter(Ifc5Dwriter):
|
|||||||
import xlsxwriter
|
import xlsxwriter
|
||||||
|
|
||||||
super().write()
|
super().write()
|
||||||
|
os.makedirs(self.output, exist_ok=True)
|
||||||
file_name = ""
|
file_name = ""
|
||||||
for cost_schedule in self.cost_schedules:
|
for cost_schedule in self.cost_schedules:
|
||||||
if file_name:
|
if file_name:
|
||||||
@@ -683,7 +686,7 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("input", type=str, help="Specify an IFC file to process")
|
parser.add_argument("input", type=str, help="Specify an IFC file to process")
|
||||||
parser.add_argument("output", help="The output directory for CSV or filename for other formats")
|
parser.add_argument("output", help="The output directory")
|
||||||
parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log")
|
parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-f", "--format", type=str, help="Choose which format to export in (CSV/ODS/XLSX)", default="CSV"
|
"-f", "--format", type=str, help="Choose which format to export in (CSV/ODS/XLSX)", default="CSV"
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ size_t read_filters_from_file(const std::string&, inclusion_filter&, inclusion_t
|
|||||||
void parse_filter(geom_filter &, const std::vector<std::string>&);
|
void parse_filter(geom_filter &, const std::vector<std::string>&);
|
||||||
std::vector<IfcGeom::filter_t> setup_filters(const std::vector<geom_filter>&, const std::string&);
|
std::vector<IfcGeom::filter_t> setup_filters(const std::vector<geom_filter>&, const std::string&);
|
||||||
|
|
||||||
bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap);
|
bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap, bool bypass_properties=false);
|
||||||
|
|
||||||
// from https://stackoverflow.com/questions/31696328/boost-program-options-using-zero-parameter-options-multiple-times
|
// from https://stackoverflow.com/questions/31696328/boost-program-options-using-zero-parameter-options-multiple-times
|
||||||
struct verbosity_counter {
|
struct verbosity_counter {
|
||||||
@@ -965,7 +965,10 @@ int main(int argc, char** argv) {
|
|||||||
time_t start,end;
|
time_t start,end;
|
||||||
time(&start);
|
time(&start);
|
||||||
|
|
||||||
if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap)) {
|
// @nb last argument true -> bypass_properties which are not read by any of the geometry serializers
|
||||||
|
// XML, RocksDB, IFC are already special-cased above
|
||||||
|
// SVG requires properties for IfcAnnotation/DRAWING properties
|
||||||
|
if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap, output_extension != SVG)) {
|
||||||
write_log(!quiet);
|
write_log(!quiet);
|
||||||
serializer.reset();
|
serializer.reset();
|
||||||
IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); /**< @todo Windows Unicode support */
|
IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); /**< @todo Windows Unicode support */
|
||||||
@@ -1336,7 +1339,7 @@ void write_log(bool header) {
|
|||||||
|
|
||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
|
|
||||||
bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap) {
|
bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap, bool bypass_properties) {
|
||||||
time_t start, end;
|
time_t start, end;
|
||||||
|
|
||||||
// Prevent IfcFile::Init() prints by setting output to null temporarily
|
// Prevent IfcFile::Init() prints by setting output to null temporarily
|
||||||
@@ -1344,20 +1347,36 @@ bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file,
|
|||||||
|
|
||||||
time(&start);
|
time(&start);
|
||||||
|
|
||||||
|
bool requires_init = false;
|
||||||
|
|
||||||
#ifdef WITH_IFCXML
|
#ifdef WITH_IFCXML
|
||||||
if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) {
|
if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) {
|
||||||
ifc_file = IfcParse::parse_ifcxml(filename);
|
ifc_file = IfcParse::parse_ifcxml(filename);
|
||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
|
{
|
||||||
|
ifc_file = new IfcParse::IfcFile(IfcParse::uninitialized_tag{});
|
||||||
|
requires_init = true;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
ifc_file->bypass_type("IfcRelDefinesByProperties");
|
||||||
|
ifc_file->bypass_type("IfcPropertySetDefinition");
|
||||||
|
ifc_file->bypass_type("IfcProperty");
|
||||||
|
ifc_file->bypass_type("IfcMaterialProperties");
|
||||||
|
ifc_file->bypass_type("IfcProfileProperties");
|
||||||
|
ifc_file->bypass_type("IfcPhysicalQuantity");
|
||||||
|
|
||||||
#ifdef USE_MMAP
|
#ifdef USE_MMAP
|
||||||
ifc_file = new IfcParse::IfcFile(filename, mmap);
|
if (mmap) {
|
||||||
|
ifc_file->initialize(filename, mmap);
|
||||||
|
requires_init = false;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
(void)mmap;
|
(void)mmap;
|
||||||
ifc_file = new IfcParse::IfcFile(filename);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
if (requires_init) {
|
||||||
|
ifc_file->initialize(filename);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ifc_file || !ifc_file->good()) {
|
if (!ifc_file || !ifc_file->good()) {
|
||||||
Logger::Error("Unable to parse input file '" + filename + "'");
|
Logger::Error("Unable to parse input file '" + filename + "'");
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto interpolated_loop = polygon_from_points(points);
|
auto interpolated_loop = polygon_from_points(points);
|
||||||
|
interpolated_loop->external = w1->external;
|
||||||
if (interpolated->kind() == taxonomy::FACE) {
|
if (interpolated->kind() == taxonomy::FACE) {
|
||||||
std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
|
std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
|||||||
auto jt = it + 1;
|
auto jt = it + 1;
|
||||||
std::array<taxonomy::item::ptr, 2> fa = { *it, *jt };
|
std::array<taxonomy::item::ptr, 2> fa = { *it, *jt };
|
||||||
std::array<TopoDS_Shape, 2> shps;
|
std::array<TopoDS_Shape, 2> shps;
|
||||||
std::array<TopoDS_Wire, 2> ws;
|
std::vector<std::array<TopoDS_Wire, 2>> ws;
|
||||||
|
ws.emplace_back();
|
||||||
for (int i = 0; i < 2; ++i) {
|
for (int i = 0; i < 2; ++i) {
|
||||||
if (fa[i]->kind() == taxonomy::FACE) {
|
if (fa[i]->kind() == taxonomy::FACE) {
|
||||||
if (!convert(std::static_pointer_cast<taxonomy::face>(fa[i]), shps[i])) {
|
if (!convert(std::static_pointer_cast<taxonomy::face>(fa[i]), shps[i])) {
|
||||||
@@ -135,11 +136,20 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
|||||||
if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) {
|
if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// @todo this is only outer wire
|
|
||||||
if (shps[i].ShapeType() == TopAbs_FACE) {
|
if (shps[i].ShapeType() == TopAbs_FACE) {
|
||||||
ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
|
ws[0][i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
|
||||||
|
size_t j = 1;
|
||||||
|
for (TopExp_Explorer exp(shps[i], TopAbs_WIRE); exp.More(); exp.Next()) {
|
||||||
|
if (exp.Current() != ws[0][i]) {
|
||||||
|
while (ws.size() <= j) {
|
||||||
|
ws.emplace_back();
|
||||||
|
}
|
||||||
|
ws[j++][i] = TopoDS::Wire(exp.Current());
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ws[i] = TopoDS::Wire(shps[i]);
|
ws[0][i] = TopoDS::Wire(shps[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shps[0].ShapeType() == TopAbs_FACE) {
|
if (shps[0].ShapeType() == TopAbs_FACE) {
|
||||||
@@ -154,35 +164,38 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
|||||||
BB.Add(comp, shps[1]);
|
BB.Add(comp, shps[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BRepTools_WireExplorer a(ws[0]);
|
|
||||||
BRepTools_WireExplorer b(ws[1]);
|
|
||||||
for (; a.More() && b.More(); a.Next(), b.Next()) {
|
|
||||||
auto& e1 = a.Current();
|
|
||||||
// auto e3 = TopoDS::Edge(b.Current().Reversed());
|
|
||||||
auto& e3 = b.Current();
|
|
||||||
|
|
||||||
// Documentation says unconnected edges are automatically connected, but this is not the case
|
for (auto& wp : ws) {
|
||||||
TopoDS_Vertex e1a, e1b, e3a, e3b;
|
BRepTools_WireExplorer a(wp[0]);
|
||||||
TopExp::Vertices(e1, e1a, e1b, true);
|
BRepTools_WireExplorer b(wp[1]);
|
||||||
TopExp::Vertices(e3, e3a, e3b, true);
|
for (; a.More() && b.More(); a.Next(), b.Next()) {
|
||||||
auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge();
|
auto& e1 = a.Current();
|
||||||
auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge();
|
// auto e3 = TopoDS::Edge(b.Current().Reversed());
|
||||||
|
auto& e3 = b.Current();
|
||||||
/*
|
|
||||||
BRepFill_Filling fill;
|
|
||||||
fill.Add(e1, GeomAbs_C0);
|
|
||||||
fill.Add(e2, GeomAbs_C0);
|
|
||||||
fill.Add(e3, GeomAbs_C0);
|
|
||||||
fill.Add(e4, GeomAbs_C0);
|
|
||||||
fill.Build();
|
|
||||||
// faces.Append(fill.Face());
|
|
||||||
BB.Add(comp, fill.Face());
|
|
||||||
*/
|
|
||||||
|
|
||||||
auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face();
|
// Documentation says unconnected edges are automatically connected, but this is not the case
|
||||||
BB.Add(comp, f);
|
TopoDS_Vertex e1a, e1b, e3a, e3b;
|
||||||
auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face();
|
TopExp::Vertices(e1, e1a, e1b, true);
|
||||||
BB.Add(comp, g);
|
TopExp::Vertices(e3, e3a, e3b, true);
|
||||||
|
auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge();
|
||||||
|
auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge();
|
||||||
|
|
||||||
|
/*
|
||||||
|
BRepFill_Filling fill;
|
||||||
|
fill.Add(e1, GeomAbs_C0);
|
||||||
|
fill.Add(e2, GeomAbs_C0);
|
||||||
|
fill.Add(e3, GeomAbs_C0);
|
||||||
|
fill.Add(e4, GeomAbs_C0);
|
||||||
|
fill.Build();
|
||||||
|
// faces.Append(fill.Face());
|
||||||
|
BB.Add(comp, fill.Face());
|
||||||
|
*/
|
||||||
|
|
||||||
|
auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face();
|
||||||
|
BB.Add(comp, f);
|
||||||
|
auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face();
|
||||||
|
BB.Add(comp, g);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,62 +25,58 @@ using namespace ifcopenshell::geometry;
|
|||||||
|
|
||||||
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst) {
|
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst) {
|
||||||
|
|
||||||
if (!inst->Location()->as<IfcSchema::IfcPointByDistanceExpression>())
|
if (!inst->Location()->as<IfcSchema::IfcPointByDistanceExpression>()) {
|
||||||
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
|
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
|
||||||
|
}
|
||||||
|
|
||||||
Eigen::Vector3d o, axis(0, 0, 1), refDirection;
|
Eigen::Vector3d o, axis(0, 0, 1), refDirection;
|
||||||
|
|
||||||
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
|
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
|
||||||
o = m->components().col(3).head<3>();
|
o = m->components().col(3).head<3>();
|
||||||
|
|
||||||
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
|
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
|
||||||
// 1) Axis is given but not RefDirection
|
// 1) Axis is given but not RefDirection
|
||||||
// 2) RefDirection is given but not Axis
|
// 2) RefDirection is given but not Axis
|
||||||
// 3) Neither Axis or RefDirection are provided
|
// 3) Neither Axis or RefDirection are provided
|
||||||
// 4) Both Axis and RefDirection are provided
|
// 4) Both Axis and RefDirection are provided
|
||||||
|
|
||||||
const bool hasAxis = inst->Axis() != nullptr;
|
const bool hasAxis = inst->Axis() != nullptr;
|
||||||
const bool hasRef = inst->RefDirection() != nullptr;
|
const bool hasRef = inst->RefDirection() != nullptr;
|
||||||
|
|
||||||
if (hasAxis != hasRef) {
|
/*
|
||||||
|
if (hasAxis != hasRef) {
|
||||||
Logger::Warning("Axis and RefDirection should be specified together", inst);
|
Logger::Warning("Axis and RefDirection should be specified together", inst);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if (hasAxis && !hasRef)
|
if (hasAxis && !hasRef) {
|
||||||
{
|
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
||||||
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
axis = *a->components_;
|
||||||
axis = *a->components_;
|
|
||||||
|
|
||||||
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
||||||
// refDirection is not necessarily orthogonal to axis.
|
// refDirection is not necessarily orthogonal to axis.
|
||||||
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
|
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
|
||||||
refDirection = axis.cross(refDirection).cross(axis);
|
refDirection = axis.cross(refDirection).cross(axis);
|
||||||
}
|
} else if (!hasAxis && hasRef) {
|
||||||
else if (!hasAxis && hasRef)
|
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
||||||
{
|
refDirection = *r->components_;
|
||||||
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
Eigen::Vector3d up(0, 0, 1);
|
||||||
refDirection = *r->components_;
|
axis = refDirection.cross(up.cross(refDirection));
|
||||||
Eigen::Vector3d up(0, 0, 1);
|
} else if (!hasAxis && !hasRef) {
|
||||||
axis = refDirection.cross(up.cross(refDirection));
|
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
||||||
}
|
Eigen::Vector3d up(0, 0, 1);
|
||||||
else if (!hasAxis && !hasRef)
|
axis = refDirection.cross(up.cross(refDirection));
|
||||||
{
|
} else {
|
||||||
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
||||||
Eigen::Vector3d up(0, 0, 1);
|
axis = *a->components_;
|
||||||
axis = refDirection.cross(up.cross(refDirection));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
|
||||||
axis = *a->components_;
|
|
||||||
|
|
||||||
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
||||||
refDirection = *r->components_;
|
refDirection = *r->components_;
|
||||||
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
|
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
|
||||||
}
|
}
|
||||||
|
|
||||||
// axis and refDirection need to be orthogonal
|
// axis and refDirection need to be orthogonal
|
||||||
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
|
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ using namespace ifcopenshell::geometry;
|
|||||||
|
|
||||||
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBoundingBox* inst) {
|
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBoundingBox* inst) {
|
||||||
if (!settings_.get<settings::KeepBoundingBoxes>().get()) {
|
if (!settings_.get<settings::KeepBoundingBoxes>().get()) {
|
||||||
// @todo make sure it doesn't log.
|
failed_on_purpose_.insert(inst);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,13 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
|
|||||||
rot = taxonomy::matrix4(
|
rot = taxonomy::matrix4(
|
||||||
Eigen::Vector3d(0, 0, 0),
|
Eigen::Vector3d(0, 0, 0),
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
|
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
|
||||||
}
|
} else if (csp->RefDirection()) {
|
||||||
|
rot = taxonomy::matrix4(
|
||||||
|
Eigen::Vector3d(0, 0, 0),
|
||||||
|
Eigen::Vector3d(0, 0, 1),
|
||||||
|
taxonomy::cast<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents()
|
||||||
|
).ccomponents().block<3, 3>(0, 0);
|
||||||
|
}
|
||||||
profile_rotations.push_back(rot);
|
profile_rotations.push_back(rot);
|
||||||
}
|
}
|
||||||
if (faces.size() != profile_offsets.size()) {
|
if (faces.size() != profile_offsets.size()) {
|
||||||
|
|||||||
@@ -82,7 +82,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
|
|||||||
rot = taxonomy::matrix4(
|
rot = taxonomy::matrix4(
|
||||||
Eigen::Vector3d(0, 0, 0),
|
Eigen::Vector3d(0, 0, 0),
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
|
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
|
||||||
}
|
} else if (csp->RefDirection()) {
|
||||||
|
rot = taxonomy::matrix4(
|
||||||
|
Eigen::Vector3d(0, 0, 0),
|
||||||
|
Eigen::Vector3d(0, 0, 1),
|
||||||
|
taxonomy::cast<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents())
|
||||||
|
.ccomponents()
|
||||||
|
.block<3, 3>(0, 0);
|
||||||
|
}
|
||||||
profile_rotations.push_back(rot);
|
profile_rotations.push_back(rot);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -372,6 +372,18 @@ const IfcUtil::IfcBaseEntity* mapping::get_single_material_association(const Ifc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef SCHEMA_HAS_IfcMaterialConstituentSet
|
||||||
|
if (associated_material->as<IfcSchema::IfcMaterialConstituentSet>()) {
|
||||||
|
IfcSchema::IfcMaterialConstituentSet* constituentset = associated_material->as<IfcSchema::IfcMaterialConstituentSet>();
|
||||||
|
if (settings_.get<settings::LayersetFirst>().value ? constituentset->MaterialConstituents()->get()->size() >= 1 : constituentset->MaterialConstituents()->get()->size() == 1) {
|
||||||
|
IfcSchema::IfcMaterialConstituent* constituent = (*constituentset->MaterialConstituents()->get()->begin());
|
||||||
|
if (auto* m_ = constituent->Material()) {
|
||||||
|
single_material = m_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ build-urls:
|
|||||||
test:
|
test:
|
||||||
pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py
|
pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py
|
||||||
|
|
||||||
|
.PHONY: test-parallel
|
||||||
|
test-parallel:
|
||||||
|
@NPROCS=$$(nproc 2>/dev/null || sysctl -n hw.ncpu); \
|
||||||
|
pytest -p no:pytest-blender -n $$NPROCS test --ignore=test/util/test_shape_builder.py
|
||||||
|
|
||||||
.PHONY: test-mathutils
|
.PHONY: test-mathutils
|
||||||
test-mathutils:
|
test-mathutils:
|
||||||
pytest -p no:pytest-blender test/util/test_shape_builder.py
|
pytest -p no:pytest-blender test/util/test_shape_builder.py
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import zipfile
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union, TYPE_CHECKING, Any, overload, Literal
|
from typing import Optional, Union, TYPE_CHECKING, Any, overload, Literal
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import ifcopenshell.express.schema_class
|
import ifcopenshell.express.schema_class
|
||||||
@@ -138,7 +139,12 @@ def open(
|
|||||||
path: Union[os.PathLike, str], format: Optional[str] = None, *, should_stream: bool = False, readonly: bool = False
|
path: Union[os.PathLike, str], format: Optional[str] = None, *, should_stream: bool = False, readonly: bool = False
|
||||||
) -> Union[_file, sqlite, _stream]: ...
|
) -> Union[_file, sqlite, _stream]: ...
|
||||||
def open(
|
def open(
|
||||||
path: Union[os.PathLike, str], format: Optional[str] = None, should_stream: bool = False, readonly: bool = False
|
path: Union[os.PathLike, str],
|
||||||
|
format: Optional[str] = None,
|
||||||
|
should_stream: bool = False,
|
||||||
|
readonly: bool = False,
|
||||||
|
mmap: bool = False,
|
||||||
|
bypass_types: Optional[Sequence[str]] = None,
|
||||||
) -> Union[_file, sqlite, _stream]:
|
) -> Union[_file, sqlite, _stream]:
|
||||||
"""Loads an IFC dataset from a filepath
|
"""Loads an IFC dataset from a filepath
|
||||||
|
|
||||||
@@ -186,7 +192,17 @@ def open(
|
|||||||
if should_stream:
|
if should_stream:
|
||||||
return stream(path)
|
return stream(path)
|
||||||
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux.
|
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux.
|
||||||
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly)
|
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly=readonly)
|
||||||
|
elif bypass_types:
|
||||||
|
f = ifcopenshell_wrapper.file(ifcopenshell_wrapper.uninitialized_tag())
|
||||||
|
for ty in bypass_types:
|
||||||
|
f.bypass_type(ty)
|
||||||
|
if mmap:
|
||||||
|
f.initialize(str(path.absolute()), mmap=mmap)
|
||||||
|
else:
|
||||||
|
f.initialize(str(path.absolute()))
|
||||||
|
elif mmap:
|
||||||
|
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap)
|
||||||
else:
|
else:
|
||||||
f = ifcopenshell_wrapper.open(str(path.absolute()))
|
f = ifcopenshell_wrapper.open(str(path.absolute()))
|
||||||
return file(f)
|
return file(f)
|
||||||
@@ -299,20 +315,44 @@ def guess_format(path: Path) -> Literal[".ifc", ".ifcZIP", ".ifcXML", ".ifcJSON"
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def stream2(path: Union[Path, str]):
|
def stream2(path: Union[Path, str], mmap: bool = False, page_size: int = 0):
|
||||||
"""Streams the content of a file path from disk, yielding each instance
|
"""Streams the content of a file path from disk, yielding each instance
|
||||||
as a dictionary.
|
as a dictionary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path (Union[Path, str]): input file path
|
path (Union[Path, str]): input file path
|
||||||
|
mmap (bool): open the file contents using memory mapping
|
||||||
|
page_size (int): open file in python and feed chunks to the parser
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
dict: entity instance dictionaries
|
dict: entity instance dictionaries
|
||||||
"""
|
"""
|
||||||
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path))
|
if page_size:
|
||||||
while streamer:
|
import builtins
|
||||||
if inst := streamer.read_instance_py():
|
|
||||||
yield inst
|
f = builtins.open(path, encoding="ascii")
|
||||||
|
strm = ifcopenshell_wrapper.InstanceStreamer()
|
||||||
|
strm.pushPage(f.read(page_size))
|
||||||
|
finished = False
|
||||||
|
while True:
|
||||||
|
while strm.hasSemicolon():
|
||||||
|
if inst := strm.readInstancePy():
|
||||||
|
yield inst
|
||||||
|
else:
|
||||||
|
finished = True
|
||||||
|
break
|
||||||
|
if finished:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if data := f.read(page_size):
|
||||||
|
strm.pushPage(data)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path), mmap)
|
||||||
|
while streamer:
|
||||||
|
if inst := streamer.readInstancePy():
|
||||||
|
yield inst
|
||||||
|
|
||||||
|
|
||||||
def stream2_from_string(data: str):
|
def stream2_from_string(data: str):
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class Usecase:
|
|||||||
return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask"))
|
return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask"))
|
||||||
|
|
||||||
def handle_resource_calculation(self):
|
def handle_resource_calculation(self):
|
||||||
resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False)
|
resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_recursive=False)
|
||||||
for resource in resources:
|
for resource in resources:
|
||||||
if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"):
|
if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"):
|
||||||
ifcopenshell.api.resource.calculate_resource_usage(self.file, resource=resource)
|
ifcopenshell.api.resource.calculate_resource_usage(self.file, resource=resource)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import sys, fileinput
|
||||||
|
|
||||||
|
if sys.platform == "win32" and not hasattr(sys.stdout, 'buffer'):
|
||||||
|
import os, msvcrt
|
||||||
|
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
||||||
|
|
||||||
|
files = sys.argv[1:]
|
||||||
|
if files[0] == '-o':
|
||||||
|
b = open(files[1], 'wb')
|
||||||
|
files = files[2:]
|
||||||
|
else:
|
||||||
|
b = getattr(sys.stdout, 'buffer', sys.stdout)
|
||||||
|
|
||||||
|
for line in fileinput.input(files=files, mode='rb'):
|
||||||
|
b.write(line)
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
:: python bootstrap.py express.bnf > express_parser.py
|
||||||
|
|
||||||
|
IF EXIST IFC2X3_TC1.exp (
|
||||||
|
python express_parser.py IFC2X3_TC1.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc2x3-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3.cpp txt/header_ifc2x3.txt Ifc2x3.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3.h txt/header_ifc2x3.txt Ifc2x3.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3-schema.cpp txt/header_ifc2x3.txt Ifc2x3-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3-definitions.h txt/header_ifc2x3.txt Ifc2x3-definitions.h
|
||||||
|
) ELSE (
|
||||||
|
:: v0.5.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3.cpp txt/header_ifc2x3.txt txt/ifndef_ifc4.txt Ifc2x3.cpp txt/endif.txt
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3.h txt/header_ifc2x3.txt Ifc2x3.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3enum.h txt/header_ifc2x3.txt Ifc2x3enum.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3-latebound.cpp txt/header_ifc2x3.txt txt/ifndef_ifc4.txt Ifc2x3-latebound.cpp txt/endif.txt
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc2x3-latebound.h txt/header_ifc2x3.txt Ifc2x3-latebound.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4_ADD2TC1.exp (
|
||||||
|
python express_parser.py IFC4_ADD2TC1.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4.cpp txt/header_ifc4.txt Ifc4.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4.h txt/header_ifc4.txt Ifc4.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4-schema.cpp txt/header_ifc4.txt Ifc4-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4-definitions.h txt/header_ifc4.txt Ifc4-definitions.h
|
||||||
|
) ELSE (
|
||||||
|
:: v0.5.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4.cpp txt/header_ifc4.txt txt/ifdef_ifc4.txt Ifc4.cpp txt/endif.txt
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4.h txt/header_ifc4.txt Ifc4.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4enum.h txt/header_ifc4.txt Ifc4enum.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4-latebound.cpp txt/header_ifc4.txt txt/ifdef_ifc4.txt Ifc4-latebound.cpp txt/endif.txt
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4-latebound.h txt/header_ifc4.txt Ifc4-latebound.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4x1.exp (
|
||||||
|
python express_parser.py IFC4x1.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x1-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x1.cpp txt/header_ifc4x1.txt Ifc4x1.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x1.h txt/header_ifc4x1.txt Ifc4x1.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x1-schema.cpp txt/header_ifc4x1.txt Ifc4x1-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x1-definitions.h txt/header_ifc4x1.txt Ifc4x1-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4x2.exp (
|
||||||
|
python express_parser.py IFC4x2.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x2-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x2.cpp txt/header_ifc4x2.txt Ifc4x2.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x2.h txt/header_ifc4x2.txt Ifc4x2.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x2-schema.cpp txt/header_ifc4x2.txt Ifc4x2-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x2-definitions.h txt/header_ifc4x2.txt Ifc4x2-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4x3_RC1.exp (
|
||||||
|
python express_parser.py IFC4x3_RC1.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x3_rc1-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1.cpp txt/header_ifc4x3_rc1.txt Ifc4x3_rc1.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1.h txt/header_ifc4x3_rc1.txt Ifc4x3_rc1.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1-schema.cpp txt/header_ifc4x3_rc1.txt Ifc4x3_rc1-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1-definitions.h txt/header_ifc4x3_rc1.txt Ifc4x3_rc1-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4x3_RC2.exp (
|
||||||
|
python express_parser.py IFC4x3_RC2.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x3_rc2-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc2.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc2.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc2-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc2-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4x3_RC3.exp (
|
||||||
|
python express_parser.py IFC4x3_RC3.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x3_rc3-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc3.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc3.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc3-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc3-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4x3_RC4.exp (
|
||||||
|
python express_parser.py IFC4x3_RC4.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x3_rc4-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc4.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc4.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc4-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc4-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4X3.exp (
|
||||||
|
python express_parser.py IFC4X3.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x3-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3.cpp txt/header_ifc4x3_rc2.txt Ifc4x3.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3.h txt/header_ifc4x3_rc2.txt Ifc4x3.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4X3_TC1.exp (
|
||||||
|
python express_parser.py IFC4X3_TC1.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x3_tc1-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1.cpp txt/header_ifc4x3_tc1.txt Ifc4x3_tc1.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1.h txt/header_ifc4x3_tc1.txt Ifc4x3_tc1.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1-schema.cpp txt/header_ifc4x3_tc1.txt Ifc4x3_tc1-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1-definitions.h txt/header_ifc4x3_tc1.txt Ifc4x3_tc1-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4X3_ADD1.exp (
|
||||||
|
python express_parser.py IFC4X3_ADD1.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x3_add1-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1.cpp txt/header_ifc4x3_add1.txt Ifc4x3_add1.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1.h txt/header_ifc4x3_add1.txt Ifc4x3_add1.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1-schema.cpp txt/header_ifc4x3_add1.txt Ifc4x3_add1-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1-definitions.h txt/header_ifc4x3_add1.txt Ifc4x3_add1-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
|
|
||||||
|
IF EXIST IFC4X3_ADD2.exp (
|
||||||
|
python express_parser.py IFC4X3_ADD2.exp header implementation schema_class definitions
|
||||||
|
|
||||||
|
IF EXIST Ifc4x3_add2-schema.cpp (
|
||||||
|
:: v0.6.0
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2.cpp txt/header_ifc4x3_add2.txt Ifc4x3_add2.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2.h txt/header_ifc4x3_add2.txt Ifc4x3_add2.h
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2-schema.cpp txt/header_ifc4x3_add2.txt Ifc4x3_add2-schema.cpp
|
||||||
|
python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2-definitions.h txt/header_ifc4x3_add2.txt Ifc4x3_add2-definitions.h
|
||||||
|
)
|
||||||
|
|
||||||
|
del *.cpp *.h
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#endif
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC2X3_TC1.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC4.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC4x1.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC4x2.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC4X3_ADD1.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC4X3_ADD2.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC4x3_RC1.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC4x3_RC2.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* This file has been generated from IFC4X3_TC1.exp. Do not make modifications *
|
||||||
|
* but instead modify the python script that has been used to generate this. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
#ifdef USE_IFC4
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
#ifdef USE_IFC4X1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
#ifndef USE_IFC4
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
#ifndef USE_IFC4X1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
#ifndef USE_IFC4X1
|
||||||
@@ -248,6 +248,10 @@ READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
|
|||||||
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
|
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
|
||||||
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
|
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
|
||||||
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
|
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
|
||||||
|
try:
|
||||||
|
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
|
||||||
|
except:
|
||||||
|
UNKNOWN = 5 # Workaround
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
@@ -496,6 +500,24 @@ class rocksdb_file_storage:
|
|||||||
return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2]))
|
return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2]))
|
||||||
|
|
||||||
|
|
||||||
|
class file_header:
|
||||||
|
def __init__(self, file, header_data):
|
||||||
|
self.file = file
|
||||||
|
self.header_data = header_data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_description(self) -> entity_instance:
|
||||||
|
return entity_instance.wrap_value(self.header_data.file_description_py(), file=self.file)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_name(self) -> entity_instance:
|
||||||
|
return entity_instance.wrap_value(self.header_data.file_name_py(), file=self.file)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_schema(self) -> entity_instance:
|
||||||
|
return entity_instance.wrap_value(self.header_data.file_schema_py(), file=self.file)
|
||||||
|
|
||||||
|
|
||||||
class file:
|
class file:
|
||||||
"""Base class for containing IFC files.
|
"""Base class for containing IFC files.
|
||||||
|
|
||||||
@@ -513,7 +535,7 @@ class file:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
wrapped_data: ifcopenshell_wrapper.file
|
wrapped_data: ifcopenshell_wrapper.file
|
||||||
header: ifcopenshell_wrapper.IfcSpfHeader
|
header: file_header
|
||||||
units: dict[str, entity_instance] = {}
|
units: dict[str, entity_instance] = {}
|
||||||
history_size: int = 64
|
history_size: int = 64
|
||||||
history: list[Transaction]
|
history: list[Transaction]
|
||||||
@@ -586,8 +608,11 @@ class file:
|
|||||||
"Unsupported schema: %s" % ",".join(self.header.file_schema.schema_identifiers),
|
"Unsupported schema: %s" % ",".join(self.header.file_schema.schema_identifiers),
|
||||||
),
|
),
|
||||||
INVALID_SYNTAX: lambda: (Error, "Syntax error during parse, check logs"),
|
INVALID_SYNTAX: lambda: (Error, "Syntax error during parse, check logs"),
|
||||||
|
# This is the case when passing uninitialized_tag
|
||||||
|
UNKNOWN: lambda: (None, None),
|
||||||
}[f.good().value()]()
|
}[f.good().value()]()
|
||||||
raise exc(msg)
|
if exc is not None:
|
||||||
|
raise exc(msg)
|
||||||
else:
|
else:
|
||||||
args = filter(None, [schema])
|
args = filter(None, [schema])
|
||||||
args = map(ifcopenshell_wrapper.schema_by_name, args)
|
args = map(ifcopenshell_wrapper.schema_by_name, args)
|
||||||
@@ -1025,12 +1050,10 @@ class file:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def header(self):
|
def header(self):
|
||||||
try: # Temporary workaround until new builds are ready. See #7131.
|
try:
|
||||||
h = self.wrapped_data.header()
|
return file_header(self, self.wrapped_data.header())
|
||||||
except:
|
except:
|
||||||
return self.wrapped_data.header
|
return self.wrapped_data.header
|
||||||
object.__setattr__(h, "file_ref", lambda inst: entity_instance.wrap_value(inst, file=self))
|
|
||||||
return h
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def storage(self) -> Optional[rocksdb_file_storage]:
|
def storage(self) -> Optional[rocksdb_file_storage]:
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import sys
|
||||||
|
from typing import Optional, IO
|
||||||
|
from itertools import accumulate
|
||||||
|
|
||||||
|
from . import ifcopenshell_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class TransformDefaultDict:
|
||||||
|
def __init__(self, keyfunc=lambda x: x, default_factory=None):
|
||||||
|
"""
|
||||||
|
Similar to collections.defaultdict, but allows for storing
|
||||||
|
non-hashable keys by means of a transform function
|
||||||
|
"""
|
||||||
|
self.keyfunc = keyfunc
|
||||||
|
self.default_factory = default_factory
|
||||||
|
self._data = {}
|
||||||
|
self._original_keys = {}
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
k = self.keyfunc(key)
|
||||||
|
self._data[k] = value
|
||||||
|
self._original_keys[k] = key
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
k = self.keyfunc(key)
|
||||||
|
if k in self._data:
|
||||||
|
return self._data[k]
|
||||||
|
if self.default_factory is not None:
|
||||||
|
value = self.default_factory()
|
||||||
|
self._data[k] = value
|
||||||
|
self._original_keys[k] = key
|
||||||
|
return value
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return self.keyfunc(key) in self._data
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
k = self.keyfunc(key)
|
||||||
|
del self._data[k]
|
||||||
|
del self._original_keys[k]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._original_keys.values())
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return ((self._original_keys[k], v) for k, v in self._data.items())
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return self._original_keys.values()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
return self._data.values()
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return self._data.get(self.keyfunc(key), default)
|
||||||
|
|
||||||
|
def setdefault(self, key, default=None):
|
||||||
|
k = self.keyfunc(key)
|
||||||
|
if k not in self._data:
|
||||||
|
self._data[k] = default
|
||||||
|
self._original_keys[k] = key
|
||||||
|
return self._data[k]
|
||||||
|
|
||||||
|
def update(self, other):
|
||||||
|
for k, v in other.items():
|
||||||
|
self[k] = v
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self._data.clear()
|
||||||
|
self._original_keys.clear()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
items = ", ".join(f"{k!r}: {v!r}" for k, v in self.items())
|
||||||
|
return f"{self.__class__.__name__}({{{items}}})"
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCounter:
|
||||||
|
def __init__(self, only_with_representation=True):
|
||||||
|
self.total = 0
|
||||||
|
transform_func = lambda decl: decl.name() if hasattr(decl, "name") else decl
|
||||||
|
self.counts = TransformDefaultDict(keyfunc=transform_func, default_factory=int)
|
||||||
|
self.counts_excl = TransformDefaultDict(keyfunc=transform_func, default_factory=int)
|
||||||
|
self.only_with_representation = only_with_representation
|
||||||
|
|
||||||
|
def count(self, decl, values):
|
||||||
|
if decl._is("IfcProduct"):
|
||||||
|
if self.only_with_representation and values.get("Representation") is None:
|
||||||
|
return
|
||||||
|
self.counts_excl[decl] += 1
|
||||||
|
while decl.name() != "IfcProduct":
|
||||||
|
self.counts[decl] += 1
|
||||||
|
decl = decl.supertype()
|
||||||
|
self.total += 1
|
||||||
|
|
||||||
|
def print(self, out=sys.stdout):
|
||||||
|
if self.counts:
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def process(ent, level=-1):
|
||||||
|
if ent in self.counts:
|
||||||
|
results.append((level, ent.name(), self.counts[ent]))
|
||||||
|
for subtype in ent.subtypes():
|
||||||
|
process(subtype, level=level + 1)
|
||||||
|
|
||||||
|
prod = next(iter(self.counts.keys())).schema().declaration_by_name("ifcproduct")
|
||||||
|
process(prod)
|
||||||
|
max_width = max(level * 2 + len(name) for level, name, _ in results)
|
||||||
|
for level, name, count in results:
|
||||||
|
print(" " * (level * 2), f"{name:<{max_width - level * 2}}", ": ", count, file=out, sep="")
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanResultCounter:
|
||||||
|
def __init__(self, exclude_union=True):
|
||||||
|
self.total = 0
|
||||||
|
self.exclude_union = exclude_union
|
||||||
|
|
||||||
|
def count(self, decl, values):
|
||||||
|
if decl._is("IfcBooleanResult"):
|
||||||
|
if self.exclude_union and values.get("Operator") == "UNION":
|
||||||
|
return
|
||||||
|
self.total += 1
|
||||||
|
|
||||||
|
def print(self, out=sys.stdout):
|
||||||
|
print("IfcBooleanResult:", self.total, file=out)
|
||||||
|
|
||||||
|
|
||||||
|
class StatsCollector:
|
||||||
|
streamer: ifcopenshell_wrapper.InstanceStreamer
|
||||||
|
page_size: Optional[int]
|
||||||
|
file_stream: Optional[IO[str]]
|
||||||
|
|
||||||
|
finalized: bool = False
|
||||||
|
needs_data: bool = False
|
||||||
|
|
||||||
|
counters: list
|
||||||
|
|
||||||
|
num_semis: int = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.streamer = ifcopenshell_wrapper.InstanceStreamer()
|
||||||
|
self.needs_data = True
|
||||||
|
self.counters = [ProductCounter(), BooleanResultCounter()]
|
||||||
|
|
||||||
|
def feedFromFile(self, f: Optional[IO[str]] = None):
|
||||||
|
if f:
|
||||||
|
self.file_stream = f
|
||||||
|
self.feed(self.file_stream.read(self.page_size))
|
||||||
|
|
||||||
|
def feed(self, data: str):
|
||||||
|
self.streamer.pushPage(data)
|
||||||
|
self.needs_data = False
|
||||||
|
self.num_semis = self.streamer.semicolonCount()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromFilePath(fn, page_size: int = 102400):
|
||||||
|
collector = StatsCollector()
|
||||||
|
collector.page_size = page_size
|
||||||
|
collector.feedFromFile(open(str(fn), encoding="ascii"))
|
||||||
|
return collector
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
if self.num_semis > 0:
|
||||||
|
if inst := self.streamer.readInstancePy(True):
|
||||||
|
self.num_semis -= 1
|
||||||
|
return inst["type"], dict(list(inst.items())[2:])
|
||||||
|
else:
|
||||||
|
self.finalized = True
|
||||||
|
return None
|
||||||
|
elif self.file_stream:
|
||||||
|
self.feedFromFile()
|
||||||
|
return self.next()
|
||||||
|
else:
|
||||||
|
self.needs_data = True
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
if n := self.next():
|
||||||
|
decl, values = n
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if decl.schema().name() == "HEADER_SECTION_SCHEMA":
|
||||||
|
return
|
||||||
|
for cnt in self.counters:
|
||||||
|
cnt.count(decl, values)
|
||||||
|
|
||||||
|
def print(self, out=sys.stdout):
|
||||||
|
for cnt in self.counters:
|
||||||
|
print(type(cnt).__name__, file=out)
|
||||||
|
print("=" * len(type(cnt).__name__), file=out)
|
||||||
|
cnt.print(out)
|
||||||
|
|
||||||
|
def includeElementTypesBasedOnBudget(self, priorities: dict, budget):
|
||||||
|
def specificity(decl, target):
|
||||||
|
if decl._is(target):
|
||||||
|
i = 0
|
||||||
|
while decl.name() != target:
|
||||||
|
decl = decl.supertype()
|
||||||
|
i += 1
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
|
||||||
|
def calc_prio(ty):
|
||||||
|
# find most specific priority directive for the given type in ty
|
||||||
|
return min(((specificity(ty, k), v) for k, v in priorities.items() if ty._is(k)), default=(0, 0))[1]
|
||||||
|
|
||||||
|
sorted_types = sorted(self.counters[0].counts_excl.keys(), key=calc_prio, reverse=True)
|
||||||
|
sorted_types = [s for s in sorted_types if not s._is("IfcFeatureElement")]
|
||||||
|
counts = [self.counters[0].counts_excl[v] for v in sorted_types]
|
||||||
|
|
||||||
|
ccounts = list(accumulate(counts))
|
||||||
|
|
||||||
|
if ccounts[0] >= budget:
|
||||||
|
# At the very least include one type
|
||||||
|
return sorted_types[0:1]
|
||||||
|
|
||||||
|
for i, s in enumerate(ccounts):
|
||||||
|
if s >= budget:
|
||||||
|
over = s - budget
|
||||||
|
under = budget - ccounts[i - 1]
|
||||||
|
k = i if over <= under else i - 1
|
||||||
|
return sorted_types[0 : k + 1]
|
||||||
|
|
||||||
|
# We're lucky we can render everything in budget
|
||||||
|
return sorted_types[:]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
collector = StatsCollector.fromFilePath(sys.argv[1])
|
||||||
|
while not collector.finalized:
|
||||||
|
collector.process()
|
||||||
|
collector.print()
|
||||||
|
print(
|
||||||
|
*collector.includeElementTypesBasedOnBudget(
|
||||||
|
{
|
||||||
|
"IfcWall": 3,
|
||||||
|
"IfcSlab": 3,
|
||||||
|
"IfcWindow": 2,
|
||||||
|
"IfcDoor": 2,
|
||||||
|
"IfcBuildingElement": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -117,6 +117,7 @@ def get_pset(
|
|||||||
if (
|
if (
|
||||||
psets_only
|
psets_only
|
||||||
and not pset.is_a("IfcPropertySet")
|
and not pset.is_a("IfcPropertySet")
|
||||||
|
and not pset.is_a("IfcPreDefinedPropertySet")
|
||||||
and not (is_ifc2x3 and pset.is_a("IfcExtendedMaterialProperties"))
|
and not (is_ifc2x3 and pset.is_a("IfcExtendedMaterialProperties"))
|
||||||
):
|
):
|
||||||
pset = None
|
pset = None
|
||||||
@@ -126,7 +127,11 @@ def get_pset(
|
|||||||
if type_pset is not None and not prop:
|
if type_pset is not None and not prop:
|
||||||
if psets_only or qtos_only:
|
if psets_only or qtos_only:
|
||||||
type_pset_element = element.file.by_id(type_pset["id"])
|
type_pset_element = element.file.by_id(type_pset["id"])
|
||||||
if psets_only and not type_pset_element.is_a("IfcPropertySet"):
|
if (
|
||||||
|
psets_only
|
||||||
|
and not type_pset_element.is_a("IfcPropertySet")
|
||||||
|
and not type_pset_element.is_a("IfcPreDefinedPropertySet")
|
||||||
|
):
|
||||||
type_pset = None
|
type_pset = None
|
||||||
elif qtos_only and not type_pset_element.is_a("IfcElementQuantity"):
|
elif qtos_only and not type_pset_element.is_a("IfcElementQuantity"):
|
||||||
type_pset = None
|
type_pset = None
|
||||||
@@ -177,7 +182,7 @@ def get_psets(
|
|||||||
psets = {}
|
psets = {}
|
||||||
if element.is_a("IfcTypeObject"):
|
if element.is_a("IfcTypeObject"):
|
||||||
for definition in element.HasPropertySets or []:
|
for definition in element.HasPropertySets or []:
|
||||||
if psets_only and not definition.is_a("IfcPropertySet"):
|
if psets_only and not definition.is_a("IfcPropertySet") and not definition.is_a("IfcPreDefinedPropertySet"):
|
||||||
continue
|
continue
|
||||||
if qtos_only and not definition.is_a("IfcElementQuantity"):
|
if qtos_only and not definition.is_a("IfcElementQuantity"):
|
||||||
continue
|
continue
|
||||||
@@ -213,7 +218,11 @@ def get_psets(
|
|||||||
for relationship in is_defined_by:
|
for relationship in is_defined_by:
|
||||||
if relationship.is_a("IfcRelDefinesByProperties"):
|
if relationship.is_a("IfcRelDefinesByProperties"):
|
||||||
definition = relationship.RelatingPropertyDefinition
|
definition = relationship.RelatingPropertyDefinition
|
||||||
if psets_only and not definition.is_a("IfcPropertySet"):
|
if (
|
||||||
|
psets_only
|
||||||
|
and not definition.is_a("IfcPropertySet")
|
||||||
|
and not definition.is_a("IfcPreDefinedPropertySet")
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
if qtos_only and not definition.is_a("IfcElementQuantity"):
|
if qtos_only and not definition.is_a("IfcElementQuantity"):
|
||||||
continue
|
continue
|
||||||
@@ -565,6 +574,40 @@ def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, Non
|
|||||||
return predefined_type
|
return predefined_type
|
||||||
|
|
||||||
|
|
||||||
|
def is_userdefined_type(element: ifcopenshell.entity_instance) -> bool:
|
||||||
|
"""Checks if the predefined type is userdefined
|
||||||
|
|
||||||
|
:param element: The IFC Element entity
|
||||||
|
:return: True if userdefined
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
element = ifcopenshell.by_type("IfcWall")[0]
|
||||||
|
is_userdefined_type = ifcopenshell.util.element.is_userdefined_type(element)
|
||||||
|
"""
|
||||||
|
if element_type := get_type(element):
|
||||||
|
predefined_type = getattr(element_type, "PredefinedType", None)
|
||||||
|
if predefined_type == "USERDEFINED":
|
||||||
|
return True
|
||||||
|
elif not predefined_type:
|
||||||
|
predefined_type = getattr(element_type, "ElementType", ...)
|
||||||
|
if predefined_type == ...:
|
||||||
|
predefined_type = getattr(element_type, "ProcessType", None)
|
||||||
|
if predefined_type:
|
||||||
|
return True
|
||||||
|
if predefined_type and predefined_type != "NOTDEFINED":
|
||||||
|
return False
|
||||||
|
|
||||||
|
predefined_type = getattr(element, "PredefinedType", None)
|
||||||
|
if predefined_type == "USERDEFINED":
|
||||||
|
return True
|
||||||
|
elif not predefined_type:
|
||||||
|
return bool(getattr(element, "ObjectType", None))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_type(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
def get_type(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
"""Retrieves the construction type element of an element occurrence.
|
"""Retrieves the construction type element of an element occurrence.
|
||||||
|
|
||||||
|
|||||||
@@ -329,61 +329,28 @@ def guess_date_range(work_schedule: ifcopenshell.entity_instance):
|
|||||||
return earliest, latest
|
return earliest, latest
|
||||||
|
|
||||||
|
|
||||||
def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
|
def get_task_outputs(
|
||||||
return [rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")]
|
task: ifcopenshell.entity_instance, is_recursive: bool = False
|
||||||
|
) -> set[ifcopenshell.entity_instance]:
|
||||||
|
if is_recursive:
|
||||||
|
return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_outputs(subtask)}
|
||||||
|
return {rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")}
|
||||||
|
|
||||||
|
|
||||||
def get_task_outputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
|
def get_task_inputs(
|
||||||
if not is_deep:
|
task: ifcopenshell.entity_instance, is_recursive: bool = False
|
||||||
return get_direct_task_outputs(task)
|
) -> set[ifcopenshell.entity_instance]:
|
||||||
else:
|
if is_recursive:
|
||||||
return [output for nested_task in get_all_nested_tasks(task) for output in get_direct_task_outputs(nested_task)]
|
return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_inputs(subtask)}
|
||||||
|
return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcProduct")}
|
||||||
|
|
||||||
|
|
||||||
def get_task_inputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
|
def get_task_resources(
|
||||||
if not is_deep:
|
task: ifcopenshell.entity_instance, is_recursive: bool = False
|
||||||
return [
|
) -> set[ifcopenshell.entity_instance]:
|
||||||
object
|
if is_recursive:
|
||||||
for rel in task.OperatesOn
|
return {r for subtask in [task] + list(get_all_nested_tasks(task)) for r in get_task_resources(subtask)}
|
||||||
if rel.is_a("IfcRelAssignsToProcess")
|
return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcResource")}
|
||||||
for object in rel.RelatedObjects
|
|
||||||
if object.is_a("IfcProduct")
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
return [
|
|
||||||
output
|
|
||||||
for nested_task in get_all_nested_tasks(task)
|
|
||||||
for output in [
|
|
||||||
object
|
|
||||||
for rel in nested_task.OperatesOn
|
|
||||||
if rel.is_a("IfcRelAssignsToProcess")
|
|
||||||
for object in rel.RelatedObjects
|
|
||||||
if object.is_a("IfcProduct")
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_task_resources(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]:
|
|
||||||
if not is_deep:
|
|
||||||
return [
|
|
||||||
object
|
|
||||||
for rel in task.OperatesOn
|
|
||||||
if rel.is_a("IfcRelAssignsToProcess")
|
|
||||||
for object in rel.RelatedObjects
|
|
||||||
if object.is_a("IfcResource")
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
return [
|
|
||||||
resource
|
|
||||||
for nested_task in get_all_nested_tasks(task)
|
|
||||||
for resource in [
|
|
||||||
object
|
|
||||||
for rel in nested_task.OperatesOn
|
|
||||||
if rel.is_a("IfcRelAssignsToProcess")
|
|
||||||
for object in rel.RelatedObjects
|
|
||||||
if object.is_a("IfcResource")
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def has_task_outputs(task: ifcopenshell.entity_instance) -> bool:
|
def has_task_outputs(task: ifcopenshell.entity_instance) -> bool:
|
||||||
|
|||||||
@@ -320,14 +320,16 @@ def log_internal_cpp_errors(
|
|||||||
|
|
||||||
chr_offset_re = re.compile(r"at offset (\d+)\s*")
|
chr_offset_re = re.compile(r"at offset (\d+)\s*")
|
||||||
for_instance_re = re.compile(r"\s*for instance #(\d+)\s*")
|
for_instance_re = re.compile(r"\s*for instance #(\d+)\s*")
|
||||||
|
for_header_ent_re = re.compile(r"\s*for header entity (\w+)")
|
||||||
|
|
||||||
if log_content is None:
|
if log_content is None:
|
||||||
log_content = ifcopenshell.get_log()
|
log_content = ifcopenshell.get_log()
|
||||||
msgs = list(map(json.loads, filter(None, log_content.split("\n"))))
|
msgs = list(map(json.loads, filter(None, log_content.split("\n"))))
|
||||||
chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs]
|
chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs]
|
||||||
instance_messages = [for_instance_re.findall(m["message"]) for m in msgs]
|
instance_messages = [for_instance_re.findall(m["message"]) for m in msgs]
|
||||||
|
header_messages = [for_header_ent_re.findall(m["message"]) for m in msgs]
|
||||||
|
|
||||||
if chr_offsets or (instance_messages and f is None):
|
if any(chr_offsets) or (any(instance_messages) and f is None):
|
||||||
# The file is opened in binary mode, in order
|
# The file is opened in binary mode, in order
|
||||||
# to correspond with the offsets reported by
|
# to correspond with the offsets reported by
|
||||||
# IfcOpenShell C++
|
# IfcOpenShell C++
|
||||||
@@ -348,7 +350,7 @@ def log_internal_cpp_errors(
|
|||||||
else:
|
else:
|
||||||
logger.error("For instance:\n %s\n%s", line, m)
|
logger.error("For instance:\n %s\n%s", line, m)
|
||||||
|
|
||||||
if instance_messages:
|
if any(instance_messages):
|
||||||
for instid, msg in zip(instance_messages, msgs):
|
for instid, msg in zip(instance_messages, msgs):
|
||||||
if instid:
|
if instid:
|
||||||
m = for_instance_re.sub("", msg["message"])
|
m = for_instance_re.sub("", msg["message"])
|
||||||
@@ -376,6 +378,12 @@ def log_internal_cpp_errors(
|
|||||||
else:
|
else:
|
||||||
logger.error(m)
|
logger.error(m)
|
||||||
|
|
||||||
|
if any(header_messages):
|
||||||
|
for hent, msg in zip(header_messages, msgs):
|
||||||
|
if hent:
|
||||||
|
m = msg["message"]
|
||||||
|
logger.error(m)
|
||||||
|
|
||||||
|
|
||||||
entity_attribute_map: dict[tuple[str, str], tuple[entity_type, tuple[attribute, ...]]] = {}
|
entity_attribute_map: dict[tuple[str, str], tuple[entity_type, tuple[attribute, ...]]] = {}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
ISO-10303-21;
|
ISO-10303-21;
|
||||||
HEADER;
|
HEADER;
|
||||||
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
|
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
|
||||||
FILE_NAME('','2023-01-11T09:47:45',(),(),'IfcOpenShell v0.7.0-8a108f37','IfcOpenShell v0.7.0-8a108f37','');
|
FILE_NAME('','2023-01-11T09:47:45',(''),(''),'IfcOpenShell v0.7.0-8a108f37','IfcOpenShell v0.7.0-8a108f37','');
|
||||||
FILE_SCHEMA(('IFC4X3_ADD2'));
|
FILE_SCHEMA(('IFC4X3_ADD2'));
|
||||||
ENDSEC;
|
ENDSEC;
|
||||||
DATA;
|
DATA;
|
||||||
#1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$);
|
#1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$,$);
|
||||||
#2=IFCGEOGRAPHICCRS('EPSG:3857',$,$,$,$);
|
#2=IFCGEOGRAPHICCRS('EPSG:3857',$,$,$,$,$);
|
||||||
#3=IFCRIGIDOPERATION(#1,#2,IFCPLANEANGLEMEASURE(0.),IFCPLANEANGLEMEASURE(0.),0.);
|
#3=IFCRIGIDOPERATION(#1,#2,IFCPLANEANGLEMEASURE(0.),IFCPLANEANGLEMEASURE(0.),0.);
|
||||||
ENDSEC;
|
ENDSEC;
|
||||||
END-ISO-10303-21;
|
END-ISO-10303-21;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
import ifcopenshell
|
||||||
|
from ifcopenshell.geom.stats import StatsCollector
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"file",
|
||||||
|
[os.path.join(os.path.dirname(__file__), "../../../../test/input/Allplan 2017 Test Bauteil-Oberflächen.ifc")],
|
||||||
|
)
|
||||||
|
def test_stats(file):
|
||||||
|
collector = StatsCollector.fromFilePath(file)
|
||||||
|
while not collector.finalized:
|
||||||
|
collector.process()
|
||||||
|
collector.print()
|
||||||
|
prio = {
|
||||||
|
"IfcWall": 2,
|
||||||
|
"IfcBuildingElement": 1,
|
||||||
|
"IfcBuildingElementProxy": -1,
|
||||||
|
}
|
||||||
|
ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 1)]
|
||||||
|
assert ents == ["IfcWall"] or ents == ["IfcWallStandardCase"]
|
||||||
|
ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 100)]
|
||||||
|
assert "IfcBuildingElementProxy" in ents
|
||||||
|
ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 50)]
|
||||||
|
assert "IfcBuildingElementProxy" not in ents
|
||||||
@@ -23,6 +23,11 @@ import pytest
|
|||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
except ImportError:
|
||||||
|
psutil = None
|
||||||
|
|
||||||
fn = os.path.join(os.path.dirname(__file__), "fixtures/ColumnPSetsOfSets.ifc")
|
fn = os.path.join(os.path.dirname(__file__), "fixtures/ColumnPSetsOfSets.ifc")
|
||||||
|
|
||||||
|
|
||||||
@@ -33,12 +38,50 @@ def test_stream():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_chunked_stream():
|
||||||
|
assert list(ifcopenshell.stream2(fn)) == list(ifcopenshell.stream2(fn, page_size=1024))
|
||||||
|
|
||||||
|
|
||||||
|
def test_mmaped_stream():
|
||||||
|
assert list(ifcopenshell.stream2(fn)) == list(ifcopenshell.stream2(fn, mmap=True))
|
||||||
|
|
||||||
|
|
||||||
def test_file():
|
def test_file():
|
||||||
f = ifcopenshell.open(fn)
|
f = ifcopenshell.open(fn)
|
||||||
assert f[139].RelatingPropertyDefinition.is_a("IfcPropertySetDefinitionSet")
|
assert f[139].RelatingPropertyDefinition.is_a("IfcPropertySetDefinitionSet")
|
||||||
assert {x.id() for x in f[139].RelatingPropertyDefinition[0]} == {136, 138}
|
assert {x.id() for x in f[139].RelatingPropertyDefinition[0]} == {136, 138}
|
||||||
|
|
||||||
|
|
||||||
|
def test_partial_open():
|
||||||
|
f = ifcopenshell.open(fn)
|
||||||
|
assert len(f.by_type("ifccartesianpoint"))
|
||||||
|
f = ifcopenshell.open(fn, bypass_types=("IfcRepresentationItem",))
|
||||||
|
assert len(f.by_type("ifccartesianpoint")) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_opening_unicode():
|
||||||
|
import ifcopenshell.template
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
fn = os.path.join(d, "ხყჯ𐰢ᨕதకᎣᚱᾗ.ifc")
|
||||||
|
f = ifcopenshell.template.create()
|
||||||
|
f.write(fn)
|
||||||
|
g = ifcopenshell.open(fn)
|
||||||
|
assert g.by_type("ifcproject")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(psutil is None, reason="psutil not installed")
|
||||||
|
def test_memusage_partial_open():
|
||||||
|
m0 = psutil.Process().memory_info().rss
|
||||||
|
f = ifcopenshell.open(fn)
|
||||||
|
m1 = psutil.Process().memory_info().rss
|
||||||
|
g = ifcopenshell.open(fn, bypass_types=("IfcRepresentationItem",))
|
||||||
|
m2 = psutil.Process().memory_info().rss
|
||||||
|
# arbitrary...
|
||||||
|
expected_ratio = 0.75
|
||||||
|
assert (m2 - m1) < (m1 - m0) * expected_ratio
|
||||||
|
|
||||||
|
|
||||||
def test_rocks():
|
def test_rocks():
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
rfn = os.path.join(d, os.path.basename(fn))
|
rfn = os.path.join(d, os.path.basename(fn))
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ class TestWrite:
|
|||||||
def test_write_ifcspf(self):
|
def test_write_ifcspf(self):
|
||||||
self.assert_model_is_written("model.ifc")
|
self.assert_model_is_written("model.ifc")
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="IFCXML writing not supported")
|
||||||
def test_write_ifcxml(self):
|
def test_write_ifcxml(self):
|
||||||
self.assert_model_is_written("model.ifcXML")
|
self.assert_model_is_written("model.ifcXML")
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="IFCXML writing not supported")
|
||||||
def test_write_ifc_zip_ifcxml_format(self):
|
def test_write_ifc_zip_ifcxml_format(self):
|
||||||
self.assert_model_is_written("model.ifcZIP", format=".ifcXML", zipped=True)
|
self.assert_model_is_written("model.ifcZIP", format=".ifcXML", zipped=True)
|
||||||
|
|
||||||
@@ -58,9 +60,11 @@ class TestWrite:
|
|||||||
def test_write_anyextension_ifczip_ifcspf_format(self):
|
def test_write_anyextension_ifczip_ifcspf_format(self):
|
||||||
self.assert_model_is_written("model.anyextension", format=".ifc", zipped=True)
|
self.assert_model_is_written("model.anyextension", format=".ifc", zipped=True)
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="IFCXML writing not supported")
|
||||||
def test_write_anyextension_ifcxml_format(self):
|
def test_write_anyextension_ifcxml_format(self):
|
||||||
self.assert_model_is_written("model.anyextension", format=".ifcXML")
|
self.assert_model_is_written("model.anyextension", format=".ifcXML")
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="IFCXML writing not supported")
|
||||||
def test_write_anyextension_ifczip_ifcxml_format(self):
|
def test_write_anyextension_ifczip_ifcxml_format(self):
|
||||||
self.assert_model_is_written("model.anyextension", format=".ifcXML", zipped=True)
|
self.assert_model_is_written("model.anyextension", format=".ifcXML", zipped=True)
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import ifcopenshell.util.element as subject
|
|||||||
from ifcopenshell.util.shape_builder import ShapeBuilder
|
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||||
|
|
||||||
|
|
||||||
class TestIFC2X3MaterialProfilePsts(test.bootstrap.IFC2X3):
|
class TestIFC2X3MaterialProfilePsets(test.bootstrap.IFC2X3):
|
||||||
def test_get_profile_pset(self):
|
def test_get_profile_pset(self):
|
||||||
profile = ifcopenshell.api.profile.add_parameterized_profile(self.file, "IfcRectangleProfileDef")
|
profile = ifcopenshell.api.profile.add_parameterized_profile(self.file, "IfcRectangleProfileDef")
|
||||||
pset = ifcopenshell.api.pset.add_pset(self.file, profile, "")
|
pset = ifcopenshell.api.pset.add_pset(self.file, profile, "")
|
||||||
@@ -159,6 +159,15 @@ class TestGetPsetIFC4(test.bootstrap.IFC4):
|
|||||||
assert subject.get_pset(self.file.create_entity("IfcPerson"), "name") is None
|
assert subject.get_pset(self.file.create_entity("IfcPerson"), "name") is None
|
||||||
assert subject.get_pset(self.file.create_entity("IfcPerson"), "name", "a") is None
|
assert subject.get_pset(self.file.create_entity("IfcPerson"), "name", "a") is None
|
||||||
|
|
||||||
|
def test_getting_predefined_psets(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoorType")
|
||||||
|
pset = self.file.create_entity("IfcDoorLiningProperties", ifcopenshell.guid.new())
|
||||||
|
pset.Name = "My Lining"
|
||||||
|
pset.LiningDepth = 42
|
||||||
|
element.HasPropertySets = [pset]
|
||||||
|
assert subject.get_pset(element, "My Lining") == {"LiningDepth": 42, "id": pset.id()}
|
||||||
|
assert subject.get_pset(element, "My Lining", psets_only=True) == {"LiningDepth": 42, "id": pset.id()}
|
||||||
|
|
||||||
|
|
||||||
class TestGetPsetsIFC4(test.bootstrap.IFC4):
|
class TestGetPsetsIFC4(test.bootstrap.IFC4):
|
||||||
def test_getting_the_psets_of_a_product_as_a_dictionary(self):
|
def test_getting_the_psets_of_a_product_as_a_dictionary(self):
|
||||||
@@ -222,6 +231,15 @@ class TestGetPsetsIFC4(test.bootstrap.IFC4):
|
|||||||
ifcopenshell.api.pset.edit_qto(self.file, qto=qto, properties={"x": 42})
|
ifcopenshell.api.pset.edit_qto(self.file, qto=qto, properties={"x": 42})
|
||||||
assert subject.get_psets(element, qtos_only=True) == {"qto": {"x": 42, "id": qto.id()}}
|
assert subject.get_psets(element, qtos_only=True) == {"qto": {"x": 42, "id": qto.id()}}
|
||||||
|
|
||||||
|
def test_getting_predefined_psets(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoorType")
|
||||||
|
pset = self.file.create_entity("IfcDoorLiningProperties", ifcopenshell.guid.new())
|
||||||
|
pset.Name = "My Lining"
|
||||||
|
pset.LiningDepth = 42
|
||||||
|
element.HasPropertySets = [pset]
|
||||||
|
assert subject.get_psets(element) == {"My Lining": {"LiningDepth": 42, "id": pset.id()}}
|
||||||
|
assert subject.get_psets(element, psets_only=True) == {"My Lining": {"LiningDepth": 42, "id": pset.id()}}
|
||||||
|
|
||||||
|
|
||||||
class TestGetPropertyDefinitionIFC4(test.bootstrap.IFC4):
|
class TestGetPropertyDefinitionIFC4(test.bootstrap.IFC4):
|
||||||
def test_getting_the_properties_of_a_pset(self):
|
def test_getting_the_properties_of_a_pset(self):
|
||||||
@@ -369,6 +387,65 @@ class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4):
|
|||||||
assert subject.get_predefined_type(element_type) == "NOTDEFINED"
|
assert subject.get_predefined_type(element_type) == "NOTDEFINED"
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsUserdefinedTypeIFC4(test.bootstrap.IFC4):
|
||||||
|
def test_getting_a_predefined_element(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
|
element.PredefinedType = "PARTITIONING"
|
||||||
|
assert not subject.is_userdefined_type(element)
|
||||||
|
|
||||||
|
def test_getting_an_element_userdefined_type(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
|
element.PredefinedType = "USERDEFINED"
|
||||||
|
element.ObjectType = "FOOBAR"
|
||||||
|
assert subject.is_userdefined_type(element)
|
||||||
|
|
||||||
|
def test_getting_an_element_type_without_a_predefined_type_attribute(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcAnnotation")
|
||||||
|
element.ObjectType = "FOOBAR"
|
||||||
|
assert subject.is_userdefined_type(element)
|
||||||
|
|
||||||
|
def test_getting_an_inherited_predefined_type(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
|
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
|
||||||
|
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
|
||||||
|
element_type.PredefinedType = "PARTITIONING"
|
||||||
|
assert not subject.is_userdefined_type(element)
|
||||||
|
|
||||||
|
def test_getting_an_inherited_userdefined_type_for_an_element_type(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
|
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
|
||||||
|
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
|
||||||
|
element_type.PredefinedType = "USERDEFINED"
|
||||||
|
element_type.ElementType = "FOOBAR"
|
||||||
|
assert subject.is_userdefined_type(element)
|
||||||
|
|
||||||
|
def test_getting_an_overriden_predefined_type(self):
|
||||||
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
|
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
|
||||||
|
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
|
||||||
|
element_type.PredefinedType = "NOTDEFINED"
|
||||||
|
element.PredefinedType = "PARTITIONING"
|
||||||
|
assert not subject.is_userdefined_type(element)
|
||||||
|
|
||||||
|
def test_getting_an_inherited_userdefined_type_for_a_process_type(self):
|
||||||
|
element = ifcopenshell.api.sequence.add_task(self.file)
|
||||||
|
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcTaskType")
|
||||||
|
ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type)
|
||||||
|
element_type.PredefinedType = "USERDEFINED"
|
||||||
|
element_type.ProcessType = "FOOBAR"
|
||||||
|
assert subject.is_userdefined_type(element)
|
||||||
|
|
||||||
|
def test_getting_an_element_type_predefined_type(self):
|
||||||
|
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
|
||||||
|
element_type.PredefinedType = "PARTITIONING"
|
||||||
|
assert not subject.is_userdefined_type(element_type)
|
||||||
|
|
||||||
|
def test_getting_an_element_type_null_predefined_type(self):
|
||||||
|
element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType")
|
||||||
|
element_type.PredefinedType = "NOTDEFINED"
|
||||||
|
assert not subject.is_userdefined_type(element_type)
|
||||||
|
|
||||||
|
|
||||||
class TestGetTypeIFC4(test.bootstrap.IFC4):
|
class TestGetTypeIFC4(test.bootstrap.IFC4):
|
||||||
def test_getting_the_type_of_a_product(self):
|
def test_getting_the_type_of_a_product(self):
|
||||||
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
#include "FileReader.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <list>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
#ifdef USE_MMAP
|
||||||
|
#include <boost/iostreams/device/mapped_file.hpp>
|
||||||
|
#include <boost/filesystem/path.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
inline void file_seek_abs(FILE* f, std::uint64_t off) {
|
||||||
|
if (_fseeki64(f, static_cast<long long>(off), SEEK_SET) != 0)
|
||||||
|
throw std::runtime_error("fseek failed");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
inline void file_seek_abs(FILE* f, std::uint64_t off) {
|
||||||
|
if (fseeko(f, static_cast<off_t>(off), SEEK_SET) != 0)
|
||||||
|
throw std::runtime_error("fseeko failed");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
using namespace IfcParse;
|
||||||
|
|
||||||
|
struct FullBufferImpl final : FileReader::Impl {
|
||||||
|
std::vector<char> buf_;
|
||||||
|
explicit FullBufferImpl(const std::string& fn) {
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
std::wstring fn_ws = IfcUtil::path::from_utf8(fn);
|
||||||
|
const wchar_t* fn_wide = fn_ws.c_str();
|
||||||
|
auto stream = _wfopen(fn_wide, L"rb");
|
||||||
|
#else
|
||||||
|
auto stream = fopen(fn.c_str(), "rb");
|
||||||
|
#endif
|
||||||
|
fseek(stream, 0, SEEK_END);
|
||||||
|
buf_.resize((size_t)ftell(stream));
|
||||||
|
rewind(stream);
|
||||||
|
buf_.resize((size_t) fread(buf_.data(), 1, buf_.capacity(), stream));
|
||||||
|
fclose(stream);
|
||||||
|
}
|
||||||
|
size_t size() const override { return buf_.size(); }
|
||||||
|
char get(size_t pos) const override {
|
||||||
|
if (pos >= buf_.size()) throw std::out_of_range("get out of range");
|
||||||
|
return buf_[pos];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PagedFileImpl final : FileReader::Impl {
|
||||||
|
std::string fn_;
|
||||||
|
FILE* fp_ = nullptr;
|
||||||
|
size_t file_size_ = 0;
|
||||||
|
size_t page_size_ = 4096;
|
||||||
|
|
||||||
|
// LRU cache
|
||||||
|
size_t capacity_ = 8;
|
||||||
|
mutable std::list<size_t> lru_;
|
||||||
|
struct Entry {
|
||||||
|
FileReader::Page page;
|
||||||
|
std::list<size_t>::iterator it;
|
||||||
|
};
|
||||||
|
mutable std::unordered_map<size_t, Entry> map_;
|
||||||
|
|
||||||
|
PagedFileImpl(const std::string& fn, size_t page_size, size_t cap)
|
||||||
|
: fn_(fn), page_size_(std::max<size_t>(512, page_size)), capacity_(std::max<size_t>(2, cap))
|
||||||
|
{
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
std::wstring fn_ws = IfcUtil::path::from_utf8(fn);
|
||||||
|
const wchar_t* fn_wide = fn_ws.c_str();
|
||||||
|
fp_ = _wfopen(fn_wide, L"rb");
|
||||||
|
#else
|
||||||
|
fp_ = fopen(fn.c_str(), "rb");
|
||||||
|
#endif
|
||||||
|
fseek(fp_, 0, SEEK_END);
|
||||||
|
file_size_ = (size_t)ftell(fp_);
|
||||||
|
rewind(fp_);
|
||||||
|
}
|
||||||
|
|
||||||
|
~PagedFileImpl() override {
|
||||||
|
if (fp_) std::fclose(fp_);
|
||||||
|
fp_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const override { return file_size_; }
|
||||||
|
|
||||||
|
char get(size_t pos) const override {
|
||||||
|
if (pos >= file_size_) throw std::out_of_range("get out of range");
|
||||||
|
const size_t pidx = pos / page_size_;
|
||||||
|
const FileReader::Page& p = fetchPage_(pidx);
|
||||||
|
const size_t off = pos % page_size_;
|
||||||
|
if (off >= p.data.size()) throw std::out_of_range("offset beyond valid page bytes");
|
||||||
|
return p.data[off];
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const FileReader::Page& fetchPage_(size_t idx) const {
|
||||||
|
auto it = map_.find(idx);
|
||||||
|
if (it != map_.end()) {
|
||||||
|
touch_(it);
|
||||||
|
return it->second.page;
|
||||||
|
}
|
||||||
|
// Load page from disk using persistent FILE*
|
||||||
|
FileReader::Page pg;
|
||||||
|
pg.data.resize(page_size_);
|
||||||
|
const size_t begin = idx * page_size_;
|
||||||
|
const size_t avail = std::min(page_size_, file_size_ - begin);
|
||||||
|
|
||||||
|
file_seek_abs(fp_, begin);
|
||||||
|
if (avail > 0) {
|
||||||
|
const size_t nread = std::fread(pg.data.data(), 1, avail, fp_);
|
||||||
|
if (nread != avail) throw std::runtime_error("Short fread on page");
|
||||||
|
}
|
||||||
|
// trim to actual size
|
||||||
|
pg.data.resize(avail);
|
||||||
|
|
||||||
|
// Insert into LRU
|
||||||
|
if (map_.size() >= capacity_) evict_();
|
||||||
|
lru_.push_front(idx);
|
||||||
|
auto lit = lru_.begin();
|
||||||
|
auto [emplaced_it, ok] = map_.emplace(idx, Entry{ std::move(pg), lit });
|
||||||
|
(void)ok;
|
||||||
|
return emplaced_it->second.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_(typename std::unordered_map<size_t, Entry>::iterator it) const {
|
||||||
|
lru_.erase(it->second.it);
|
||||||
|
lru_.push_front(it->first);
|
||||||
|
it->second.it = lru_.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void evict_() const {
|
||||||
|
if (lru_.empty()) return;
|
||||||
|
const size_t victim = lru_.back();
|
||||||
|
lru_.pop_back();
|
||||||
|
map_.erase(victim);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef USE_MMAP
|
||||||
|
struct MMapImpl final : FileReader::Impl {
|
||||||
|
boost::iostreams::mapped_file_source map_;
|
||||||
|
size_t size_ = 0;
|
||||||
|
|
||||||
|
explicit MMapImpl(const std::string& fn) {
|
||||||
|
map_.open(boost::filesystem::path(IfcUtil::path::from_utf8(fn)));
|
||||||
|
if (!map_.is_open()) throw std::runtime_error("Failed to open mapped_file_source");
|
||||||
|
size_ = static_cast<size_t>(map_.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const override { return size_; }
|
||||||
|
|
||||||
|
char get(size_t pos) const override {
|
||||||
|
if (pos >= size_) throw std::out_of_range("get out of range");
|
||||||
|
return map_.data()[pos];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// User-pushed sequential backend with an arbitrary-length queue of future pages.
|
||||||
|
/// We keep a deque of pages; when reads move forward, we drop fully-consumed
|
||||||
|
/// pages from the front to release memory.
|
||||||
|
struct PushedSequentialImpl final : std::enable_shared_from_this<PushedSequentialImpl>, FileReader::Impl {
|
||||||
|
// Deque of pages, front is earliest in file.
|
||||||
|
std::deque<FileReader::Page> pages_;
|
||||||
|
// total bytes in dropped pages
|
||||||
|
size_t discarded_page_bytes_ = 0;
|
||||||
|
|
||||||
|
size_t size() const override {
|
||||||
|
size_t n = discarded_page_bytes_;
|
||||||
|
for (auto& pg : pages_) n += pg.data.size();
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop fully-consumed pages so pos is guaranteed to be within the first page
|
||||||
|
void dropPages(size_t pos) override {
|
||||||
|
while (!pages_.empty()) {
|
||||||
|
if (pos - discarded_page_bytes_ >= pages_.front().data.size()) {
|
||||||
|
discarded_page_bytes_ += pages_.front().data.size();
|
||||||
|
pages_.pop_front();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char get(size_t pos) const override {
|
||||||
|
/*
|
||||||
|
auto self = const_cast<PushedSequentialImpl*>(this);
|
||||||
|
// We do not do this automatically because all variable width tokens:
|
||||||
|
// ENUM/STRING/BINARY/KEYWORD are stored as file offsets until a full
|
||||||
|
// entity instance is finalized.
|
||||||
|
if (this->shared_from_this().use_count() == 2) {
|
||||||
|
// only drop pages when there is only one active client.
|
||||||
|
// NB this->shared_from_this() increases count by 1
|
||||||
|
self->drop_consumed_up_to(pos);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const size_t avail_end = size();
|
||||||
|
if (pos >= avail_end) throw std::out_of_range("pushed backend: position not committed yet");
|
||||||
|
|
||||||
|
pos -= discarded_page_bytes_;
|
||||||
|
|
||||||
|
size_t page_start = 0;
|
||||||
|
for (const auto& pg : pages_) {
|
||||||
|
if (pos < page_start + pg.data.size()) {
|
||||||
|
const size_t off = pos - page_start;
|
||||||
|
return pg.data[off];
|
||||||
|
} else {
|
||||||
|
page_start += pg.data.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::out_of_range("pushed backend: internal inconsistency");
|
||||||
|
}
|
||||||
|
|
||||||
|
void pushNextPage(const std::string& data) override {
|
||||||
|
FileReader::Page p; p.data.assign(data.data(), data.data() + data.size());
|
||||||
|
pages_.push_back(std::move(p));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
IfcParse::FileReader::FileReader(const std::string& fn)
|
||||||
|
: cursor_(0)
|
||||||
|
{
|
||||||
|
impl_ = std::make_shared<FullBufferImpl>(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
IfcParse::FileReader::FileReader(const std::string& fn, const mmap_tag&)
|
||||||
|
: cursor_(0)
|
||||||
|
{
|
||||||
|
#ifdef USE_MMAP
|
||||||
|
impl_ = std::make_shared<MMapImpl>(fn);
|
||||||
|
#else
|
||||||
|
(void)fn;
|
||||||
|
throw std::runtime_error("IfcParse::FileReader: mmap_tag specified but library not compiled with USE_MMAP");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
IfcParse::FileReader::FileReader(const caller_fed_tag&)
|
||||||
|
: cursor_(0)
|
||||||
|
{
|
||||||
|
impl_ = std::make_shared<PushedSequentialImpl>();
|
||||||
|
}
|
||||||
|
|
||||||
|
IfcParse::FileReader::FileReader(const std::string& content, const caller_fed_tag&)
|
||||||
|
{
|
||||||
|
impl_ = std::make_shared<PushedSequentialImpl>();
|
||||||
|
impl_->pushNextPage(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
IfcParse::FileReader::FileReader(const std::string& fn, size_t page_size, size_t page_capacity)
|
||||||
|
: cursor_(0)
|
||||||
|
{
|
||||||
|
impl_ = std::make_shared<PagedFileImpl>(fn, page_size, page_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileReader FileReader::clone() const {
|
||||||
|
FileReader c(*this);
|
||||||
|
c.cursor_ = this->cursor_;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileReader::seek(size_t pos) {
|
||||||
|
if (pos > impl_->size()) throw std::out_of_range("seek out of range");
|
||||||
|
cursor_ = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t FileReader::tell() const { return cursor_; }
|
||||||
|
|
||||||
|
size_t FileReader::size() const { return impl_->size(); }
|
||||||
|
|
||||||
|
char FileReader::peek() const {
|
||||||
|
if (cursor_ >= impl_->size()) throw std::out_of_range("peek at EOF");
|
||||||
|
return impl_->get(cursor_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileReader::increment(size_t n) {
|
||||||
|
if (cursor_ + n > impl_->size()) throw std::out_of_range("increment past EOF");
|
||||||
|
cursor_ += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IfcParse::FileReader::pushNextPage(const std::string& data)
|
||||||
|
{
|
||||||
|
impl_->pushNextPage(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IfcParse::FileReader::dropPages()
|
||||||
|
{
|
||||||
|
impl_->dropPages(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IfcParse::FileReader::dropPages(size_t up_to_pos)
|
||||||
|
{
|
||||||
|
impl_->dropPages(up_to_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IfcParse::FileReader::eof() const
|
||||||
|
{
|
||||||
|
return cursor_ >= impl_->size();
|
||||||
|
}
|
||||||
|
|
||||||
|
char IfcParse::FileReader::read()
|
||||||
|
{
|
||||||
|
auto c = peek();
|
||||||
|
increment(1);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
char IfcParse::FileReader::get(size_t offset) const
|
||||||
|
{
|
||||||
|
return impl_->get(offset);
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/********************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
/*********************************************************************************
|
||||||
|
* *
|
||||||
|
* Reads a file and provides functions to access its *
|
||||||
|
* contents randomly and character by character *
|
||||||
|
* *
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
#ifndef IFCSPFSTREAM_H
|
||||||
|
#define IFCSPFSTREAM_H
|
||||||
|
|
||||||
|
#include "ifc_parse_api.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace IfcParse {
|
||||||
|
|
||||||
|
/// \brief Read-only file accessor that supports four backends:
|
||||||
|
/// - full-buffer in RAM
|
||||||
|
/// - paged (LRU-cached with capacity)
|
||||||
|
/// - memory-mapped via boost::iostreams
|
||||||
|
/// - user-pushed sequential pages (caller feeds pages intended for streaming reads in WASM)
|
||||||
|
class IFC_PARSE_API FileReader {
|
||||||
|
public:
|
||||||
|
struct Page { std::vector<char> data; };
|
||||||
|
|
||||||
|
/// \brief Tag to choose memory-mapped backend via boost::iostreams::mapped_file_source.
|
||||||
|
struct mmap_tag {};
|
||||||
|
/// \brief Tag to choose user-pushed sequential pages.
|
||||||
|
struct caller_fed_tag {};
|
||||||
|
|
||||||
|
/// \brief Construct a FileReader.
|
||||||
|
/// \param fn File path.
|
||||||
|
/// \param maybe_mmaped_or_chunked
|
||||||
|
/// - std::nullopt: full-buffer mode (entire file loaded into memory)
|
||||||
|
/// - size_t: paged mode with this page size in bytes (e.g., 4096)
|
||||||
|
/// - mmap_tag: memory-mapped mode using boost::iostreams::mapped_file_source
|
||||||
|
/// \param page_cache_capacity Number of pages to keep in the LRU when in paged mode.
|
||||||
|
FileReader(const std::string& fn);
|
||||||
|
|
||||||
|
FileReader(const std::string& fn, const mmap_tag&);
|
||||||
|
|
||||||
|
FileReader(const caller_fed_tag&);
|
||||||
|
FileReader(const std::string& content, const caller_fed_tag&);
|
||||||
|
|
||||||
|
FileReader(const std::string& fn, size_t page_size, size_t page_capacity);
|
||||||
|
|
||||||
|
/// \brief Copy-construct a new reader sharing the underlying storage but with its own cursor.
|
||||||
|
FileReader clone() const;
|
||||||
|
|
||||||
|
/// \brief Seek to an absolute byte position.
|
||||||
|
/// \throws std::out_of_range if pos > size().
|
||||||
|
void seek(size_t pos);
|
||||||
|
|
||||||
|
/// \brief Return the current cursor position.
|
||||||
|
size_t tell() const;
|
||||||
|
|
||||||
|
/// \brief Total file size in bytes.
|
||||||
|
size_t size() const;
|
||||||
|
|
||||||
|
/// \brief Peek the byte at the current cursor.
|
||||||
|
/// \throws std::out_of_range at EOF.
|
||||||
|
char peek() const;
|
||||||
|
|
||||||
|
/// \brief Advance the cursor by n bytes (default 1).
|
||||||
|
/// \throws std::out_of_range if advancing crosses EOF.
|
||||||
|
void increment(size_t n = 1);
|
||||||
|
|
||||||
|
/// \brief Push the next sequential page (pushed backend only).
|
||||||
|
/// \param data Contents of the page.
|
||||||
|
/// \throws std::logic_error if the current backend is not in pushed mode
|
||||||
|
void pushNextPage(const std::string& data);
|
||||||
|
|
||||||
|
/// \brief Drops pages up to cursor position or provided offset. Does nothing when current backend is not in pushed mode
|
||||||
|
/// \param up_to_pos Pages with an end offset before up_to_pos are dropped from memory
|
||||||
|
void dropPages();
|
||||||
|
void dropPages(size_t up_to_pos);
|
||||||
|
|
||||||
|
/// \brief Returns true if the cursor is at or beyond the end of available data.
|
||||||
|
/// For the pushed backend, EOF means all pushed bytes have been consumed.
|
||||||
|
bool eof() const;
|
||||||
|
|
||||||
|
/// \brief Equivalent of peek() followed by increment(1)
|
||||||
|
char read();
|
||||||
|
|
||||||
|
/// \brief Equivalent of peek() followed by increment(1)
|
||||||
|
char get(size_t offset) const;
|
||||||
|
|
||||||
|
struct Impl {
|
||||||
|
virtual ~Impl() = default;
|
||||||
|
virtual size_t size() const = 0;
|
||||||
|
virtual char get(size_t pos) const = 0;
|
||||||
|
/// \brief Backend may support pushing pages; default throws.
|
||||||
|
virtual void pushNextPage(const std::string&) {
|
||||||
|
throw std::logic_error("push_next_page: backend does not support pushed mode");
|
||||||
|
}
|
||||||
|
virtual void dropPages(size_t) {
|
||||||
|
// empty on purpose
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Impl> impl_;
|
||||||
|
size_t cursor_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IfcParse
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
#include "IfcCharacterDecoder.h"
|
#include "IfcCharacterDecoder.h"
|
||||||
|
|
||||||
#include "IfcException.h"
|
#include "IfcException.h"
|
||||||
#include "IfcSpfStream.h"
|
#include "FileReader.h"
|
||||||
#include "IfcLogger.h"
|
#include "IfcLogger.h"
|
||||||
|
|
||||||
#include <codecvt>
|
#include <codecvt>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
using namespace IfcParse;
|
using namespace IfcParse;
|
||||||
|
|
||||||
IfcCharacterDecoder::IfcCharacterDecoder(IfcParse::IfcSpfStream* stream) {
|
IfcCharacterDecoder::IfcCharacterDecoder(IfcParse::FileReader* stream) {
|
||||||
stream_ = stream;
|
stream_ = stream;
|
||||||
codepage_ = 0;
|
codepage_ = 0;
|
||||||
}
|
}
|
||||||
@@ -86,49 +86,9 @@ IfcCharacterDecoder::~IfcCharacterDecoder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
unsigned int reference_helper = 0;
|
std::string read_string(IfcParse::FileReader& stream_, IfcParse::IfcCharacterDecoder::ConversionMode mode, char substitution_character) {
|
||||||
|
std::u32string builder_;
|
||||||
|
|
||||||
class pure_impure_helper {
|
|
||||||
private:
|
|
||||||
bool pure_;
|
|
||||||
IfcParse::IfcSpfStream* stream_;
|
|
||||||
unsigned int& pointer_;
|
|
||||||
std::u32string builder_;
|
|
||||||
|
|
||||||
char peek() {
|
|
||||||
if (pure_) {
|
|
||||||
return stream_->peek_at(pointer_);
|
|
||||||
}
|
|
||||||
return stream_->Peek();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int tell() {
|
|
||||||
if (pure_) {
|
|
||||||
return pointer_;
|
|
||||||
}
|
|
||||||
return stream_->Tell();
|
|
||||||
}
|
|
||||||
|
|
||||||
void increment() {
|
|
||||||
if (pure_) {
|
|
||||||
stream_->increment_at(pointer_);
|
|
||||||
} else {
|
|
||||||
stream_->Inc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
pure_impure_helper(IfcParse::IfcSpfStream* stream)
|
|
||||||
: pure_(false),
|
|
||||||
stream_(stream),
|
|
||||||
pointer_(reference_helper) {}
|
|
||||||
|
|
||||||
pure_impure_helper(IfcParse::IfcSpfStream* stream, unsigned int& pointer)
|
|
||||||
: pure_(true),
|
|
||||||
stream_(stream),
|
|
||||||
pointer_(pointer) {}
|
|
||||||
|
|
||||||
std::string get(IfcParse::IfcCharacterDecoder::ConversionMode mode, char substitution_character) {
|
|
||||||
unsigned int parse_state = 0;
|
unsigned int parse_state = 0;
|
||||||
builder_.clear();
|
builder_.clear();
|
||||||
builder_.push_back('\'');
|
builder_.push_back('\'');
|
||||||
@@ -137,7 +97,7 @@ class pure_impure_helper {
|
|||||||
unsigned int hex = 0;
|
unsigned int hex = 0;
|
||||||
unsigned int hex_count = 0;
|
unsigned int hex_count = 0;
|
||||||
|
|
||||||
while ((current_char = peek()) != 0) {
|
while ((current_char = stream_.peek()) != 0) {
|
||||||
if (EXPECTS_CHARACTER(parse_state)) {
|
if (EXPECTS_CHARACTER(parse_state)) {
|
||||||
builder_.push_back(IfcUtil::convert_codepage(codepage, current_char + 0x80));
|
builder_.push_back(IfcUtil::convert_codepage(codepage, current_char + 0x80));
|
||||||
parse_state = 0;
|
parse_state = 0;
|
||||||
@@ -178,7 +138,7 @@ class pure_impure_helper {
|
|||||||
} else if (IS_HEXADECIMAL(current_char) && EXPECTS_HEX(parse_state)) {
|
} else if (IS_HEXADECIMAL(current_char) && EXPECTS_HEX(parse_state)) {
|
||||||
if (IS_LOWERCASE_HEX(current_char)) {
|
if (IS_LOWERCASE_HEX(current_char)) {
|
||||||
Logger::Warning("Lowercase hexadecimal character '" + std::string(1, current_char) +
|
Logger::Warning("Lowercase hexadecimal character '" + std::string(1, current_char) +
|
||||||
"' found at offset " + std::to_string(pointer_) +
|
"' found at offset " + std::to_string(stream_.tell()) +
|
||||||
". It is recommended to use uppercase for hexadecimal.");
|
". It is recommended to use uppercase for hexadecimal.");
|
||||||
}
|
}
|
||||||
hex <<= 4;
|
hex <<= 4;
|
||||||
@@ -202,12 +162,12 @@ class pure_impure_helper {
|
|||||||
if (parse_state == APOSTROPHE && current_char != '\'') {
|
if (parse_state == APOSTROPHE && current_char != '\'') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
throw IfcInvalidTokenException(tell(), current_char);
|
throw IfcInvalidTokenException(stream_.tell(), current_char);
|
||||||
} else {
|
} else {
|
||||||
parse_state = hex = hex_count = 0;
|
parse_state = hex = hex_count = 0;
|
||||||
builder_.push_back(current_char);
|
builder_.push_back(current_char);
|
||||||
}
|
}
|
||||||
increment();
|
stream_.increment();
|
||||||
}
|
}
|
||||||
builder_.push_back('\'');
|
builder_.push_back('\'');
|
||||||
|
|
||||||
@@ -248,22 +208,25 @@ class pure_impure_helper {
|
|||||||
}
|
}
|
||||||
throw IfcParse::IfcException("Invalid conversion mode");
|
throw IfcParse::IfcException("Invalid conversion mode");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
IfcCharacterDecoder::operator std::string() {
|
IfcCharacterDecoder::operator std::string() {
|
||||||
return pure_impure_helper(stream_).get(mode, substitution_character);
|
return read_string(*stream_, mode, substitution_character);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string IfcCharacterDecoder::get(unsigned int& ptr) {
|
std::string IfcCharacterDecoder::get(size_t& ptr) {
|
||||||
return pure_impure_helper(stream_, ptr).get(mode, substitution_character);
|
auto local_stream = *stream_;
|
||||||
|
local_stream.seek(ptr);
|
||||||
|
auto s = read_string(local_stream, mode, substitution_character);
|
||||||
|
ptr = local_stream.tell();
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IfcCharacterDecoder::skip() {
|
void IfcCharacterDecoder::skip() {
|
||||||
unsigned int parse_state = 0;
|
unsigned int parse_state = 0;
|
||||||
char current_char;
|
char current_char;
|
||||||
unsigned int hex_count = 0;
|
unsigned int hex_count = 0;
|
||||||
while ((current_char = stream_->Peek()) != 0) {
|
while ((current_char = stream_->peek()) != 0) {
|
||||||
if (EXPECTS_CHARACTER(parse_state)) {
|
if (EXPECTS_CHARACTER(parse_state)) {
|
||||||
parse_state = 0;
|
parse_state = 0;
|
||||||
} else if (current_char == '\'' && (parse_state == 0U)) {
|
} else if (current_char == '\'' && (parse_state == 0U)) {
|
||||||
@@ -318,11 +281,11 @@ void IfcCharacterDecoder::skip() {
|
|||||||
if (parse_state == APOSTROPHE && current_char != '\'') {
|
if (parse_state == APOSTROPHE && current_char != '\'') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
throw IfcInvalidTokenException(stream_->Tell(), current_char);
|
throw IfcInvalidTokenException(stream_->tell(), current_char);
|
||||||
} else {
|
} else {
|
||||||
parse_state = hex_count = 0;
|
parse_state = hex_count = 0;
|
||||||
}
|
}
|
||||||
stream_->Inc();
|
stream_->increment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
#ifndef IFCCHARACTERDECODER_H
|
#ifndef IFCCHARACTERDECODER_H
|
||||||
#define IFCCHARACTERDECODER_H
|
#define IFCCHARACTERDECODER_H
|
||||||
|
|
||||||
#include "IfcSpfStream.h"
|
#include "FileReader.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ namespace IfcParse {
|
|||||||
|
|
||||||
class IFC_PARSE_API IfcCharacterDecoder {
|
class IFC_PARSE_API IfcCharacterDecoder {
|
||||||
private:
|
private:
|
||||||
IfcParse::IfcSpfStream* stream_;
|
IfcParse::FileReader* stream_;
|
||||||
int codepage_;
|
int codepage_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -52,7 +52,7 @@ class IFC_PARSE_API IfcCharacterDecoder {
|
|||||||
};
|
};
|
||||||
static ConversionMode mode;
|
static ConversionMode mode;
|
||||||
static char substitution_character;
|
static char substitution_character;
|
||||||
IfcCharacterDecoder(IfcParse::IfcSpfStream* stream);
|
IfcCharacterDecoder(IfcParse::FileReader* stream);
|
||||||
~IfcCharacterDecoder();
|
~IfcCharacterDecoder();
|
||||||
// Only advances the underlying token stream read pointer
|
// Only advances the underlying token stream read pointer
|
||||||
// to the next token.
|
// to the next token.
|
||||||
@@ -62,7 +62,7 @@ class IFC_PARSE_API IfcCharacterDecoder {
|
|||||||
operator std::string();
|
operator std::string();
|
||||||
// Gets a decoded string representation at the offset provided,
|
// Gets a decoded string representation at the offset provided,
|
||||||
// does not mutate the underlying token stream read pointer.
|
// does not mutate the underlying token stream read pointer.
|
||||||
std::string get(unsigned int&);
|
std::string get(size_t&);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace IfcParse
|
} // namespace IfcParse
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
#ifdef IFOPSH_WITH_ROCKSDB
|
#ifdef IFOPSH_WITH_ROCKSDB
|
||||||
else {
|
else {
|
||||||
T val;
|
T val = T{};
|
||||||
const bool is_header = entity_or_type->schema() == &Header_section_schema::get_schema();
|
const bool is_header = entity_or_type->schema() == &Header_section_schema::get_schema();
|
||||||
if constexpr (
|
if constexpr (
|
||||||
// the following types cannot be directly deserialized from rocksdb, but need to be constructed
|
// the following types cannot be directly deserialized from rocksdb, but need to be constructed
|
||||||
@@ -51,6 +51,12 @@ namespace {
|
|||||||
(is_header ? entity_or_type->name() : std::to_string(instance_name_)) + "|" +
|
(is_header ? entity_or_type->name() : std::to_string(instance_name_)) + "|" +
|
||||||
std::to_string(index_), &str);
|
std::to_string(index_), &str);
|
||||||
impl::deserialize(array_.db_ptr, str, val);
|
impl::deserialize(array_.db_ptr, str, val);
|
||||||
|
} else {
|
||||||
|
static_assert(
|
||||||
|
std::is_same_v<T, EnumerationReference> ||
|
||||||
|
std::is_same_v<std::remove_cv_t<std::remove_pointer_t<T>>, IfcUtil::IfcBaseClass>,
|
||||||
|
"RocksDB deserialization must be specialized for this EnumerationReference and IfcBaseClass*"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
@@ -194,13 +200,15 @@ AttributeValue::operator IfcUtil::IfcBaseClass* () const
|
|||||||
std::to_string(index_), &str);
|
std::to_string(index_), &str);
|
||||||
size_t v;
|
size_t v;
|
||||||
memcpy(&v, str.data() + 2, sizeof(size_t));
|
memcpy(&v, str.data() + 2, sizeof(size_t));
|
||||||
if (str[1] == 'i') {
|
if (str.size() > 1 && str[1] == 'i') {
|
||||||
// entity reference, by #Name
|
// entity reference, by #Name
|
||||||
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::entityinstance_ref);
|
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::entityinstance_ref);
|
||||||
} else if (str[1] == 't') {
|
} else if (str.size() > 1 && str[1] == 't') {
|
||||||
// type reference by Identity
|
// type reference by Identity
|
||||||
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::typedecl_ref);
|
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::typedecl_ref);
|
||||||
}
|
} else {
|
||||||
|
throw std::runtime_error("Invalid data encountered");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -339,28 +347,28 @@ bool impl::serialize(std::string& val, const aggregate_of_aggregate_of_instance:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool impl::serialize(std::string& val, const Blank& t)
|
bool impl::serialize(std::string& val, const Blank&)
|
||||||
{
|
{
|
||||||
val.resize(1);
|
val.resize(1);
|
||||||
val[0] = TypeEncoder::encode_type<Blank>();
|
val[0] = TypeEncoder::encode_type<Blank>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool impl::serialize(std::string& val, const Derived& t)
|
bool impl::serialize(std::string& val, const Derived&)
|
||||||
{
|
{
|
||||||
val.resize(1);
|
val.resize(1);
|
||||||
val[0] = TypeEncoder::encode_type<Derived>();
|
val[0] = TypeEncoder::encode_type<Derived>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool impl::serialize(std::string& val, const empty_aggregate_t& t)
|
bool impl::serialize(std::string& val, const empty_aggregate_t&)
|
||||||
{
|
{
|
||||||
val.resize(1);
|
val.resize(1);
|
||||||
val[0] = TypeEncoder::encode_type<empty_aggregate_t>();
|
val[0] = TypeEncoder::encode_type<empty_aggregate_t>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool impl::serialize(std::string& val, const empty_aggregate_of_aggregate_t& t)
|
bool impl::serialize(std::string& val, const empty_aggregate_of_aggregate_t&)
|
||||||
{
|
{
|
||||||
val.resize(1);
|
val.resize(1);
|
||||||
val[0] = TypeEncoder::encode_type<empty_aggregate_of_aggregate_t>();
|
val[0] = TypeEncoder::encode_type<empty_aggregate_of_aggregate_t>();
|
||||||
@@ -397,6 +405,7 @@ bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string& val, boost::dynamic_bitset<>& t) {
|
bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string& val, boost::dynamic_bitset<>& t) {
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ struct TypeEncoder_t<parameter_pack<Types...>> {
|
|||||||
using TypeEncoder = TypeEncoder_t<type_variant_parameter_pack>;
|
using TypeEncoder = TypeEncoder_t<type_variant_parameter_pack>;
|
||||||
|
|
||||||
struct IFC_PARSE_API MutableAttributeValue {
|
struct IFC_PARSE_API MutableAttributeValue {
|
||||||
int name_;
|
uint32_t name_;
|
||||||
uint8_t index_;
|
uint8_t index_;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ namespace IfcParse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if IFOPSH_WITH_ROCKSDB
|
#ifdef IFOPSH_WITH_ROCKSDB
|
||||||
|
|
||||||
namespace impl {
|
namespace impl {
|
||||||
|
|
||||||
@@ -266,7 +266,6 @@ namespace impl {
|
|||||||
if (val[0] != TypeEncoder::encode_type<T>()) {
|
if (val[0] != TypeEncoder::encode_type<T>()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto s = (val.size() - 1) / sizeof(T);
|
|
||||||
memcpy(&t, val.data() + 1, sizeof(T));
|
memcpy(&t, val.data() + 1, sizeof(T));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -404,7 +403,7 @@ public:
|
|||||||
template<typename Visitor>
|
template<typename Visitor>
|
||||||
auto apply_visitor(void* storage, const IfcParse::declaration* decl, std::size_t identity, std::size_t index, Visitor&& visitor) const {
|
auto apply_visitor(void* storage, const IfcParse::declaration* decl, std::size_t identity, std::size_t index, Visitor&& visitor) const {
|
||||||
// @todo do we need visitation on all data/storage/attribute levels?
|
// @todo do we need visitation on all data/storage/attribute levels?
|
||||||
AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, index).apply_visitor(std::forward<Visitor>(visitor));
|
AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, (uint8_t) index).apply_visitor(std::forward<Visitor>(visitor));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user