From 8527d0fcbda6011888003c09302233378ac6ed61 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 1 Dec 2021 10:32:20 +0800 Subject: [PATCH] merage new 0.6 --- .gitignore | 29 + .travis.yml | 16 +- COPYING | 2 +- COPYING.LESSER | 2 +- README.md | 39 +- cmake/CMakeLists.txt | 4 +- src/bcf/COPYING | 621 + src/bcf/COPYING.LESSER | 165 + src/bcf/MANIFEST.in | 2 + src/bcf/README.md | 49 +- src/bcf/pyproject.toml | 12 + src/bcf/setup.cfg | 37 + src/bcf/src/bcf/__init__.py | 17 + src/bcf/{ => src}/bcf/bcfxml.py | 26 +- src/bcf/src/bcf/v2/__init__.py | 19 + src/bcf/{ => src}/bcf/v2/bcfxml.py | 19 + src/bcf/{ => src}/bcf/v2/data.py | 19 + src/bcf/{ => src}/bcf/v2/xsd/markup.xsd | 0 src/bcf/{ => src}/bcf/v2/xsd/project.xsd | 0 src/bcf/{ => src}/bcf/v2/xsd/version.xsd | 0 src/bcf/{ => src}/bcf/v2/xsd/visinfo.xsd | 0 src/bcf/src/bcf/v3/__init__.py | 19 + src/bcf/src/bcf/v3/bcfapi.py | 603 + src/bcf/{ => src}/bcf/v3/bcfxml.py | 25 +- src/bcf/{ => src}/bcf/v3/data.py | 19 + src/bcf/{ => src}/bcf/v3/xsd/documents.xsd | 0 src/bcf/{ => src}/bcf/v3/xsd/extensions.xsd | 0 src/bcf/{ => src}/bcf/v3/xsd/markup.xsd | 0 src/bcf/{ => src}/bcf/v3/xsd/project.xsd | 0 src/bcf/{ => src}/bcf/v3/xsd/shared-types.xsd | 0 src/bcf/{ => src}/bcf/v3/xsd/version.xsd | 0 src/bcf/{ => src}/bcf/v3/xsd/visinfo.xsd | 0 src/blenderbim/COPYING | 621 + src/blenderbim/Makefile | 246 +- src/blenderbim/RenameObjects.py | 119 - src/blenderbim/blenderbim/__init__.py | 54 +- src/blenderbim/blenderbim/bim/__init__.py | 297 +- src/blenderbim/blenderbim/bim/export_ifc.py | 43 +- src/blenderbim/blenderbim/bim/handler.py | 180 +- src/blenderbim/blenderbim/bim/helper.py | 81 +- src/blenderbim/blenderbim/bim/ifc.py | 108 +- src/blenderbim/blenderbim/bim/import_ifc.py | 650 +- .../blenderbim/bim/module/__init__.py | 17 + .../bim/module/aggregate/__init__.py | 18 + .../bim/module/aggregate/operator.py | 32 +- .../blenderbim/bim/module/aggregate/ui.py | 24 +- .../bim/module/attribute/__init__.py | 18 + .../bim/module/attribute/operator.py | 37 +- .../blenderbim/bim/module/attribute/prop.py | 19 + .../blenderbim/bim/module/attribute/ui.py | 26 +- .../blenderbim/bim/module/augin/__init__.py | 18 + .../blenderbim/bim/module/augin/operator.py | 35 +- .../blenderbim/bim/module/augin/prop.py | 18 + .../blenderbim/bim/module/augin/ui.py | 20 +- .../blenderbim/bim/module/bcf/__init__.py | 19 + .../blenderbim/bim/module/bcf/bcfstore.py | 19 + .../blenderbim/bim/module/bcf/operator.py | 666 +- .../blenderbim/bim/module/bcf/prop.py | 108 +- .../blenderbim/bim/module/bcf/ui.py | 74 +- .../bim/module/bimtester/__init__.py | 18 + .../bim/module/bimtester/operator.py | 83 +- .../blenderbim/bim/module/bimtester/prop.py | 18 + .../blenderbim/bim/module/bimtester/ui.py | 18 + .../bim/module/boundary/__init__.py | 22 +- .../blenderbim/bim/module/boundary/ui.py | 18 + .../blenderbim/bim/module/clash/__init__.py | 18 + .../blenderbim/bim/module/clash/operator.py | 176 +- .../blenderbim/bim/module/clash/prop.py | 32 +- .../blenderbim/bim/module/clash/ui.py | 20 +- .../bim/module/classification/__init__.py | 18 + .../bim/module/classification/operator.py | 36 +- .../bim/module/classification/prop.py | 21 +- .../bim/module/classification/ui.py | 29 +- .../blenderbim/bim/module/cobie/__init__.py | 18 + .../blenderbim/bim/module/cobie/operator.py | 45 +- .../blenderbim/bim/module/cobie/prop.py | 19 +- .../blenderbim/bim/module/cobie/ui.py | 23 +- .../bim/module/constraint/__init__.py | 18 + .../bim/module/constraint/operator.py | 89 +- .../blenderbim/bim/module/constraint/prop.py | 18 + .../blenderbim/bim/module/constraint/ui.py | 34 +- .../blenderbim/bim/module/context/__init__.py | 18 + .../blenderbim/bim/module/context/operator.py | 24 +- .../blenderbim/bim/module/context/ui.py | 18 + .../blenderbim/bim/module/cost/__init__.py | 53 +- .../blenderbim/bim/module/cost/operator.py | 679 +- .../blenderbim/bim/module/cost/prop.py | 203 +- .../blenderbim/bim/module/cost/ui.py | 618 +- .../bim/module/covetool/__init__.py | 18 + .../blenderbim/bim/module/covetool/api.py | 18 + .../bim/module/covetool/operator.py | 46 +- .../blenderbim/bim/module/covetool/prop.py | 18 + .../blenderbim/bim/module/covetool/ui.py | 18 + .../blenderbim/bim/module/csv/__init__.py | 20 +- .../blenderbim/bim/module/csv/operator.py | 18 + .../blenderbim/bim/module/csv/prop.py | 18 + .../blenderbim/bim/module/csv/ui.py | 18 + .../blenderbim/bim/module/debug/__init__.py | 20 + .../blenderbim/bim/module/debug/operator.py | 154 +- .../blenderbim/bim/module/debug/prop.py | 20 +- .../blenderbim/bim/module/debug/ui.py | 37 +- .../blenderbim/bim/module/diff/__init__.py | 20 +- .../blenderbim/bim/module/diff/operator.py | 53 +- .../blenderbim/bim/module/diff/prop.py | 18 + .../blenderbim/bim/module/diff/ui.py | 18 + .../bim/module/document/__init__.py | 18 + .../bim/module/document/operator.py | 75 +- .../blenderbim/bim/module/document/prop.py | 18 + .../blenderbim/bim/module/document/ui.py | 36 +- .../blenderbim/bim/module/drawing/__init__.py | 18 + .../bim/module/drawing/annotation.py | 64 +- .../bim/module/drawing/decoration.py | 114 +- .../blenderbim/bim/module/drawing/gizmos.py | 52 +- .../blenderbim/bim/module/drawing/handler.py | 18 + .../blenderbim/bim/module/drawing/helper.py | 20 +- .../blenderbim/bim/module/drawing/operator.py | 531 +- .../blenderbim/bim/module/drawing/prop.py | 68 +- .../bim/module/drawing/scheduler.py | 24 +- .../blenderbim/bim/module/drawing/shaders.py | 20 +- .../blenderbim/bim/module/drawing/sheeter.py | 18 + .../bim/module/drawing/svgwriter.py | 52 +- .../blenderbim/bim/module/drawing/ui.py | 45 +- .../bim/module/geometry/__init__.py | 18 + .../blenderbim/bim/module/geometry/helper.py | 18 + .../bim/module/geometry/operator.py | 157 +- .../blenderbim/bim/module/geometry/prop.py | 18 + .../blenderbim/bim/module/geometry/ui.py | 27 +- .../bim/module/georeference/__init__.py | 18 + .../bim/module/georeference/operator.py | 211 +- .../bim/module/georeference/prop.py | 43 +- .../blenderbim/bim/module/georeference/ui.py | 61 +- .../blenderbim/bim/module/group/__init__.py | 21 + .../blenderbim/bim/module/group/operator.py | 93 +- .../blenderbim/bim/module/group/prop.py | 19 + .../blenderbim/bim/module/group/ui.py | 99 +- .../blenderbim/bim/module/layer/__init__.py | 18 + .../blenderbim/bim/module/layer/operator.py | 38 +- .../blenderbim/bim/module/layer/prop.py | 18 + .../blenderbim/bim/module/layer/ui.py | 22 +- .../blenderbim/bim/module/lca/__init__.py | 34 + .../blenderbim/bim/module/lca/operator.py | 45 + .../blenderbim/bim/module/lca/prop.py | 55 + .../blenderbim/bim/module/lca/ui.py | 38 + .../bim/module/material/__init__.py | 19 + .../bim/module/material/operator.py | 266 +- .../blenderbim/bim/module/material/prop.py | 29 +- .../blenderbim/bim/module/material/ui.py | 60 +- .../blenderbim/bim/module/model/__init__.py | 40 +- .../blenderbim/bim/module/model/column.py | 168 - .../blenderbim/bim/module/model/door.py | 72 +- .../blenderbim/bim/module/model/grid.py | 25 +- .../blenderbim/bim/module/model/handler.py | 61 +- .../blenderbim/bim/module/model/opening.py | 192 + .../blenderbim/bim/module/model/pie.py | 108 +- .../blenderbim/bim/module/model/product.py | 253 +- .../blenderbim/bim/module/model/profile.py | 377 + .../blenderbim/bim/module/model/prop.py | 20 + .../blenderbim/bim/module/model/root.py | 40 + .../blenderbim/bim/module/model/slab.py | 144 +- .../blenderbim/bim/module/model/stair.py | 18 + .../blenderbim/bim/module/model/task.py | 46 + .../blenderbim/bim/module/model/ui.py | 44 +- .../blenderbim/bim/module/model/wall.py | 186 +- .../blenderbim/bim/module/model/window.py | 68 +- .../blenderbim/bim/module/model/workspace.py | 185 +- .../blenderbim/bim/module/owner/__init__.py | 19 + .../blenderbim/bim/module/owner/operator.py | 191 +- .../blenderbim/bim/module/owner/prop.py | 60 +- .../blenderbim/bim/module/owner/ui.py | 161 +- .../bim/module/parametric/__init__.py | 15 - .../bim/module/parametric/operator.py | 10 - .../blenderbim/bim/module/parametric/ui.py | 22 - .../blenderbim/bim/module/patch/__init__.py | 19 + .../blenderbim/bim/module/patch/operator.py | 73 +- .../blenderbim/bim/module/patch/prop.py | 31 +- .../blenderbim/bim/module/patch/ui.py | 36 +- .../blenderbim/bim/module/profile/__init__.py | 41 + .../blenderbim/bim/module/profile/operator.py | 116 + .../blenderbim/bim/module/profile/prop.py | 48 + .../blenderbim/bim/module/profile/ui.py | 85 + .../blenderbim/bim/module/project/__init__.py | 31 +- .../blenderbim/bim/module/project/operator.py | 430 +- .../blenderbim/bim/module/project/prop.py | 97 +- .../blenderbim/bim/module/project/ui.py | 130 +- .../blenderbim/bim/module/pset/__init__.py | 38 +- .../blenderbim/bim/module/pset/operator.py | 228 +- .../blenderbim/bim/module/pset/prop.py | 127 +- .../bim/module/pset/qto_calculator.py | 24 +- .../blenderbim/bim/module/pset/ui.py | 240 +- .../bim/module/pset_template/__init__.py | 18 + .../bim/module/pset_template/operator.py | 184 +- .../bim/module/pset_template/prop.py | 18 + .../blenderbim/bim/module/pset_template/ui.py | 22 +- .../blenderbim/bim/module/qto/__init__.py | 18 + .../blenderbim/bim/module/qto/helper.py | 69 +- .../blenderbim/bim/module/qto/operator.py | 103 +- .../blenderbim/bim/module/qto/prop.py | 19 + .../blenderbim/bim/module/qto/ui.py | 18 + .../bim/module/resource/__init__.py | 35 + .../bim/module/resource/operator.py | 466 +- .../blenderbim/bim/module/resource/prop.py | 57 + .../blenderbim/bim/module/resource/ui.py | 233 +- .../blenderbim/bim/module/root/__init__.py | 18 + .../blenderbim/bim/module/root/operator.py | 128 +- .../blenderbim/bim/module/root/prop.py | 36 +- .../blenderbim/bim/module/root/ui.py | 50 +- .../blenderbim/bim/module/search/__init__.py | 18 + .../blenderbim/bim/module/search/operator.py | 144 +- .../blenderbim/bim/module/search/prop.py | 19 + .../blenderbim/bim/module/search/ui.py | 22 +- .../bim/module/sequence/__init__.py | 31 + .../blenderbim/bim/module/sequence/helper.py | 20 +- .../bim/module/sequence/operator.py | 520 +- .../blenderbim/bim/module/sequence/prop.py | 73 +- .../blenderbim/bim/module/sequence/ui.py | 374 +- .../blenderbim/bim/module/spatial/__init__.py | 18 + .../blenderbim/bim/module/spatial/operator.py | 92 +- .../blenderbim/bim/module/spatial/prop.py | 21 +- .../blenderbim/bim/module/spatial/ui.py | 18 + .../bim/module/structural/__init__.py | 18 + .../bim/module/structural/operator.py | 90 +- .../blenderbim/bim/module/structural/prop.py | 30 +- .../blenderbim/bim/module/structural/ui.py | 55 +- .../blenderbim/bim/module/style/__init__.py | 30 +- .../blenderbim/bim/module/style/operator.py | 137 +- .../blenderbim/bim/module/style/prop.py | 37 + .../blenderbim/bim/module/style/ui.py | 72 +- .../blenderbim/bim/module/system/__init__.py | 48 + .../blenderbim/bim/module/system/operator.py | 224 + .../blenderbim/bim/module/system/prop.py | 45 + .../blenderbim/bim/module/system/ui.py | 144 + .../blenderbim/bim/module/type/__init__.py | 20 + .../blenderbim/bim/module/type/operator.py | 83 +- .../blenderbim/bim/module/type/prop.py | 22 +- .../blenderbim/bim/module/type/ui.py | 52 +- .../blenderbim/bim/module/unit/__init__.py | 38 +- .../blenderbim/bim/module/unit/operator.py | 272 +- .../blenderbim/bim/module/unit/prop.py | 83 + .../blenderbim/bim/module/unit/ui.py | 107 + .../blenderbim/bim/module/void/__init__.py | 20 + .../blenderbim/bim/module/void/operator.py | 170 +- .../blenderbim/bim/module/void/prop.py | 18 + .../blenderbim/bim/module/void/ui.py | 18 + src/blenderbim/blenderbim/bim/operator.py | 513 +- src/blenderbim/blenderbim/bim/prop.py | 127 +- src/blenderbim/blenderbim/bim/schema.py | 25 +- src/blenderbim/blenderbim/bim/ui.py | 31 +- .../blenderbim/libs/site/packages/.gitignore} | 0 .../blenderbim/libs/site/packages/OCC.pth | 3 - .../libs/site/packages/ifcopenshell.pth | 3 - .../libs/site/packages/svgwrite.pth | 3 - src/blenderbim/demo-library.blend | Bin 0 -> 948556 bytes src/blenderbim/docs/blenderbim.rst | 1 + .../docs/blenderbim/georeferencing.rst | 72 + .../docs/blenderbim/installation.rst | 22 + src/blenderbim/docs/conf.py | 18 + .../ifcopenshell-python/api-documentation.rst | 3 + .../docs/ifcsverchok/installation.rst | 10 + src/blenderbim/dxf2ifc.py | 24 +- src/blenderbim/extract.py | 18 + src/blenderbim/gbxml.py | 18 + src/blenderbim/generate_demo_library.py | 193 + src/blenderbim/generate_site_library.py | 113 + src/blenderbim/generate_util_type_json.py | 18 + src/blenderbim/getIfcElements.py | 18 + src/blenderbim/get_description.py | 18 + src/blenderbim/get_volume_by_material.py | 18 + .../libraries/blenderbim-demo-library.ifc | 920 + .../libraries/blenderbim-site-library.ifc | 311 + src/blenderbim/runpytest.py | 24 +- src/blenderbim/setup_pytest.py | 29 + src/blenderbim/site-library.blend | Bin 0 -> 1014680 bytes src/blenderbim/test/__init__.py | 17 + src/blenderbim/test/bim/__init__.py | 17 + src/blenderbim/test/bim/bootstrap.py | 356 + src/blenderbim/test/bim/module/__init__.py | 17 + .../test/bim/module/bimtester/__init__.py | 17 + .../bim/module/bimtester/test_operator.py | 31 + .../test/bim/module/drawing/__init__.py | 17 + .../module/drawing}/test_segment_clipping.py | 32 +- .../test/bim/module/patch/__init__.py | 17 + .../test/bim/module/patch/test_operator.py | 32 + .../test/bim/module/project/__init__.py | 17 + .../test/bim/module/project/test_operator.py | 344 + .../test/bim/module/root/__init__.py | 17 + .../test/bim/module/root/test_operator.py | 134 + .../test/bim/module/sequence/__init__.py | 17 + .../test/bim/module/sequence/test_operator.py | 241 + .../test/bim/module/void/__init__.py | 17 + .../test/bim/module/void/test_operator.py | 115 + src/blenderbim/test/files/basic.blend | Bin 0 -> 844288 bytes src/blenderbim/test/files/basic.ifc | 155 + src/blenderbim/test/files/decomposition.ifc | 122 + .../test/files/manual-geolocation.ifc | 116 + src/blenderbim/test/files/sample-ids.xml | 37 + src/bsdd/COPYING | 621 + src/bsdd/COPYING.LESSER | 165 + src/bsdd/bsdd.py | 19 + src/examples/IfcAdvancedHouse.cpp | 5 +- src/examples/IfcOpenHouse.cpp | 23 +- src/ifc2ca/COPYING | 621 + src/ifc2ca/COPYING.LESSER | 165 + src/ifc2ca/ca2ifc.py | 19 + src/ifc2ca/ifc2ca.py | 19 + src/ifc2ca/scriptCodeAster.py | 2159 +- src/ifc2ca/scriptCodeAsterBonded.py | 19 + src/ifc2ca/scriptSalome.py | 19 + src/ifc2ca/scriptSalomeBonded.py | 19 + src/ifc4d/COPYING | 621 + src/ifc4d/COPYING.LESSER | 165 + src/ifc4d/README.md | 16 + src/{ifcp6/ifcp6 => ifc4d/ifc4d}/msp2ifc.py | 58 +- src/{ifcp6/ifcp6 => ifc4d/ifc4d}/p62ifc.py | 19 + src/ifc5d/COPYING | 621 + src/ifc5d/COPYING.LESSER | 165 + src/ifc5d/README.md | 18 + src/ifc5d/ifc5d/csv2ifc.py | 158 + src/ifc5d/rates.csv | 14 + src/ifc5d/schedule.csv | 14 + src/ifcbimtester/COPYING | 621 + src/ifcbimtester/COPYING.LESSER | 165 + .../bimtester/features/steps/all.py | 5 +- .../features/steps/attributes_psets/de.py | 8 + .../features/steps/attributes_psets/en.py | 91 + src/ifcbimtester/bimtester/reports.py | 10 +- src/ifcbimtester/bimtester/run.py | 2 +- .../examples/steps/attributes_psets.py | 53 - .../examples/steps/attributes_psets_de.py | 18 - .../examples/steps/attributes_psets_fr.py | 21 - .../steps/attributes_psets_methods.py | 36 - src/ifccityjson/COPYING | 621 + src/ifccityjson/COPYING.LESSER | 165 + src/ifccityjson/README.md | 8 +- src/ifccityjson/cityjson2ifc.py | 258 +- src/ifccityjson/example/3DBAG_example.json | 1 - src/ifccityjson/example/geometries.json | 485 + src/ifccityjson/geometry.py | 67 +- src/ifccityjson/ifccityjson.py | 25 + src/ifcclash/COPYING | 621 + src/ifcclash/COPYING.LESSER | 165 + src/ifcclash/README.md | 121 + src/ifcclash/bootstrap.py | 21 + src/ifcclash/clashsets.json | 17 - src/ifcclash/collision.py | 663 - src/ifcclash/ifcclash.py | 454 - src/ifcclash/ifcclash/__main__.py | 45 + src/ifcclash/ifcclash/collider.py | 121 + src/ifcclash/ifcclash/ifcclash.py | 298 + src/ifcclash/make.py | 27 + src/ifccobie/COPYING | 621 + src/ifccobie/COPYING.LESSER | 165 + src/ifccobie/cobie.py | 19 + src/ifccobie/get_maintainable_assets.py | 19 + src/ifcconvert/IfcConvert.cpp | 31 +- src/ifccsv/COPYING | 621 + src/ifccsv/COPYING.LESSER | 165 + src/ifccsv/ifccsv.py | 19 + src/ifcdiff/COPYING | 621 + src/ifcdiff/COPYING.LESSER | 165 + src/ifcdiff/ifcdiff.py | 19 + src/ifcfm/COPYING | 621 + src/ifcfm/COPYING.LESSER | 165 + src/ifcfm/README.md | 8 + src/ifcfm/ifcfm/parser.py | 399 + src/ifcfm/ifcfm/writer.py | 600 + src/ifcgeom/IfcGeom.h | 19 +- src/ifcgeom/IfcGeomFunctions.cpp | 11 +- src/ifcgeom/IfcGeomIteratorImplementation.h | 4 + src/ifcgeom/IfcGeomShapes.cpp | 2 +- src/ifcgeom/IfcGeomTree.h | 16 +- src/ifcgeom/IfcRegister.cpp | 6 +- src/ifcgeom_schema_agnostic/IfcGeomIterator.h | 6 +- .../IteratorImplementation.cpp | 16 +- .../IteratorImplementation.h | 2 +- src/ifcgeom_schema_agnostic/Kernel.h | 6 + src/ifcopenshell-python/Makefile | 18 + src/ifcopenshell-python/README.md | 3 + .../ifcopenshell/alignment.py | 4 +- .../ifcopenshell/api/__init__.py | 50 + .../ifcopenshell/api/attribute/data.py | 20 +- .../ifcopenshell/api/classification/data.py | 2 +- .../api/classification/edit_classification.py | 5 +- .../api/classification/edit_reference.py | 5 +- .../ifcopenshell/api/constraint/add_metric.py | 25 + .../api/constraint/add_objective.py | 9 +- .../api/constraint/assign_constraint.py | 14 +- .../ifcopenshell/api/constraint/data.py | 39 + .../api/constraint/edit_metric.py | 10 + .../api/constraint/edit_objective.py | 5 +- .../api/constraint/remove_metric.py | 15 + .../ifcopenshell/api/context/add_context.py | 15 +- .../ifcopenshell/api/context/data.py | 5 +- .../api/cost/add_cost_item_quantity.py | 4 +- .../ifcopenshell/api/cost/add_cost_value.py | 4 + .../api/cost/assign_cost_item_product.py | 31 - .../assign_cost_item_product_quantities.py | 32 - .../api/cost/assign_cost_item_quantity.py | 58 + .../api/cost/assign_cost_value.py | 17 + .../calculate_cost_item_resource_value.py | 56 + .../ifcopenshell/api/cost/data.py | 272 +- .../ifcopenshell/api/cost/edit_cost_value.py | 17 +- .../api/cost/edit_cost_value_formula.py | 34 + .../api/cost/remove_cost_value.py | 23 + ...duct.py => unassign_cost_item_quantity.py} | 13 +- .../api/document/add_information.py | 8 +- .../api/document/add_reference.py | 5 +- .../api/document/assign_document.py | 13 +- .../api/document/edit_information.py | 5 +- .../api/document/edit_reference.py | 5 +- .../api/geometry/add_representation.py | 12 +- .../ifcopenshell/api/geometry/data.py | 2 +- .../api/geometry/edit_object_placement.py | 81 +- .../api/georeference/add_georeferencing.py | 17 +- .../api/georeference/edit_georeferencing.py | 35 - .../ifcopenshell/api/grid/create_grid_axis.py | 7 +- .../ifcopenshell/api/group/add_group.py | 13 +- .../ifcopenshell/api/group/assign_group.py | 15 +- .../ifcopenshell/api/group/data.py | 2 +- .../ifcopenshell/api/group/edit_group.py | 5 +- .../ifcopenshell/api/layer/add_layer.py | 4 +- .../ifcopenshell/api/layer/edit_layer.py | 5 +- .../ifcopenshell/api/material/add_layer.py | 7 +- .../ifcopenshell/api/material/add_material.py | 3 - .../api/material/assign_material.py | 15 +- .../api/material/copy_material.py | 30 + .../api/material/edit_assigned_material.py | 7 +- .../api/material/edit_constituent.py | 8 +- .../ifcopenshell/api/material/edit_layer.py | 8 +- .../api/material/edit_layer_usage.py | 10 + .../ifcopenshell/api/material/edit_profile.py | 10 +- .../api/material/edit_profile_usage.py | 43 +- .../api/material/remove_list_item.py | 5 +- .../api/material/remove_material.py | 1 + .../api/material/remove_profile.py | 1 + .../api/material/unassign_material.py | 22 +- .../ifcopenshell/api/owner/add_address.py | 8 +- .../ifcopenshell/api/owner/add_role.py | 4 +- .../ifcopenshell/api/owner/edit_address.py | 5 +- .../api/owner/edit_organisation.py | 5 +- .../ifcopenshell/api/owner/edit_person.py | 5 +- .../ifcopenshell/api/owner/edit_role.py | 5 +- .../ifcopenshell/api/owner/settings.py | 20 +- .../api/owner/update_owner_history.py | 27 +- .../ifcopenshell/api/profile/edit_profile.py | 10 + .../remove_profile.py} | 5 +- .../ifcopenshell/api/project/append_asset.py | 36 +- .../api/project/assign_declaration.py | 3 + .../ifcopenshell/api/project/create_file.py | 18 +- .../ifcopenshell/api/pset/add_pset.py | 12 + .../ifcopenshell/api/pset/data.py | 13 +- .../ifcopenshell/api/pset/edit_pset.py | 44 +- .../api/pset_template/add_prop_template.py | 17 +- .../api/pset_template/add_pset_template.py | 15 +- .../ifcopenshell/api/pset_template/data.py | 2 +- .../api/pset_template/remove_prop_template.py | 4 +- .../api/pset_template/remove_pset_template.py | 4 +- .../ifcopenshell/api/resource/add_resource.py | 11 +- .../api/resource/add_resource_quantity.py | 18 + .../api/resource/add_resource_time.py | 16 + .../api/resource/calculate_resource_work.py | 59 + .../ifcopenshell/api/resource/data.py | 51 +- .../api/resource/edit_resource_quantity.py | 10 + .../api/resource/edit_resource_time.py | 33 + .../api/resource/remove_resource_quantity.py | 15 + .../ifcopenshell/api/root/copy_class.py | 53 +- .../ifcopenshell/api/root/create_entity.py | 16 +- .../ifcopenshell/api/root/data.py | 2 +- .../ifcopenshell/api/root/remove_product.py | 5 + .../api/sequence/add_task_time.py | 1 + .../api/sequence/add_work_plan.py | 4 +- .../api/sequence/calculate_task_duration.py | 70 + .../api/sequence/cascade_schedule.py | 12 +- .../ifcopenshell/api/sequence/data.py | 18 +- .../api/sequence/edit_task_time.py | 15 +- .../api/sequence/remove_work_schedule.py | 6 +- .../api/sequence/unassign_lag_time.py | 4 +- .../api/spatial/assign_container.py | 1 + .../ifcopenshell/api/spatial/data.py | 2 +- .../api/spatial/remove_container.py | 17 +- .../assign_structural_analysis_model.py | 15 +- .../ifcopenshell/api/structural/data.py | 9 +- .../edit_structural_analysis_model.py | 5 +- .../edit_structural_boundary_condition.py | 5 +- .../edit_structural_connection_cs.py | 4 +- .../api/structural/edit_structural_load.py | 5 +- .../remove_structural_connection_condition.py | 1 - .../api/style/assign_material_style.py | 83 + .../assign_representation_styles.py} | 6 +- .../ifcopenshell/api/style/data.py | 14 + .../api/style/edit_presentation_style.py | 10 + .../ifcopenshell/api/style/edit_style.py | 67 - .../api/style/edit_surface_style.py | 45 + .../ifcopenshell/api/style/remove_style.py | 30 + .../ifcopenshell/api/system/add_system.py | 20 + .../ifcopenshell/api/system/assign_system.py | 30 + .../ifcopenshell/api/system/data.py | 24 + .../ifcopenshell/api/system/edit_system.py | 10 + .../ifcopenshell/api/system/remove_system.py | 11 + .../api/system/unassign_system.py | 25 + .../ifcopenshell/api/type/data.py | 26 +- .../ifcopenshell/api/type/unassign_type.py | 4 +- .../api/unit/add_context_dependent_unit.py | 14 + .../api/unit/add_monetary_unit.py | 9 + .../ifcopenshell/api/unit/add_si_unit.py | 9 + .../ifcopenshell/api/unit/assign_unit.py | 57 +- .../ifcopenshell/api/unit/data.py | 40 +- .../api/unit/edit_derived_unit.py | 10 + .../api/unit/edit_monetary_unit.py | 10 + .../ifcopenshell/api/unit/edit_named_unit.py | 18 + .../ifcopenshell/api/unit/remove_unit.py | 21 + .../ifcopenshell/api/unit/unassign_unit.py | 17 + .../ifcopenshell/api/void/add_filling.py | 20 +- .../ifcopenshell/api/void/add_opening.py | 22 +- .../ifcopenshell/api/void/data.py | 4 +- .../ifcopenshell/api/void/remove_opening.py | 15 +- .../ifcopenshell/entity_instance.py | 4 +- .../ifcopenshell/express/express_parser.py | 1397 +- .../ifcopenshell/express/implementation.py | 18 +- .../ifcopenshell/express/schema_class.py | 10 +- .../ifcopenshell/express/templates.py | 2 + src/ifcopenshell-python/ifcopenshell/file.py | 134 +- .../ifcopenshell/geom/app.py | 52 +- .../ifcopenshell/geom/main.py | 10 +- src/ifcopenshell-python/ifcopenshell/ids.py | 1197 +- src/ifcopenshell-python/ifcopenshell/ids.xsd | 232 + .../ifcopenshell/transition_curve.py | 42 +- .../ifcopenshell/util/attribute.py | 14 +- .../ifcopenshell/util/cost.py | 147 + .../ifcopenshell/util/date.py | 2 +- .../ifcopenshell/util/element.py | 41 +- .../ifcopenshell/util/fm.py | 107 + .../ifcopenshell/util/placement.py | 1 + .../ifcopenshell/util/pset.py | 12 +- .../ifcopenshell/util/schema.py | 19 +- .../ifcopenshell/util/selector.py | 78 +- .../ifcopenshell/util/type.py | 15 +- .../ifcopenshell/util/unit.py | 242 +- .../ifcopenshell/validate.py | 51 +- .../test/__init__.py} | 0 src/ifcopenshell-python/test/api/__init__.py | 0 .../test/api/geometry/__init__.py | 0 .../geometry/test_edit_object_placement.py | 338 + .../test/api/owner/__init__.py | 0 .../api/owner/test_update_owner_history.py | 128 + .../test/api/pset/__init__.py | 0 .../test/api/pset/test_edit_pset.py | 172 + .../test/api/resource/__init__.py | 0 .../resource/test_calculate_resource_work.py | 94 + .../test/api/root/__init__.py | 0 .../test/api/root/test_copy_class.py | 72 + .../test/api/sequence/__init__.py | 0 .../sequence/test_calculate_task_duration.py | 70 + .../test/api/spatial/__init__.py | 0 .../test/api/spatial/test_assign_container.py | 79 + .../test/api/style/test_edit_surface_style.py | 87 + .../test/api/void/__init__.py | 0 .../test/api/void/test_add_filling.py | 27 + .../test/api/void/test_add_opening.py | 27 + .../test/api/void/test_remove_opening.py | 31 + src/ifcopenshell-python/test/bootstrap.py | 18 + src/ifcopenshell-python/test/test_file.py | 211 + src/ifcopenshell-python/test/test_ids.py | 328 + src/ifcopenshell-python/test/util/__init__.py | 0 .../test/util/test_element.py | 271 + .../{ifcopenshell => test}/util/test_pset.py | 0 src/ifcparse/Ifc2x3.cpp | 328 + src/ifcparse/Ifc4.cpp | 414 + src/ifcparse/Ifc4x1.cpp | 420 + src/ifcparse/Ifc4x2.cpp | 434 + src/ifcparse/Ifc4x3_rc1.cpp | 484 + src/ifcparse/Ifc4x3_rc2.cpp | 490 + src/ifcparse/Ifc4x3_rc3-definitions.h | 4006 ++ src/ifcparse/Ifc4x3_rc3-schema.cpp | 17130 +++++ src/ifcparse/Ifc4x3_rc3.cpp | 24245 +++++++ src/ifcparse/Ifc4x3_rc3.h | 53345 +++++++++++++++ src/ifcparse/Ifc4x3_rc4-definitions.h | 4019 ++ src/ifcparse/Ifc4x3_rc4-schema.cpp | 17144 +++++ src/ifcparse/Ifc4x3_rc4.cpp | 24273 +++++++ src/ifcparse/Ifc4x3_rc4.h | 53362 ++++++++++++++++ src/ifcparse/IfcFile.h | 21 +- src/ifcparse/IfcHierarchyHelper.cpp | 368 +- src/ifcparse/IfcHierarchyHelper.h | 129 +- src/ifcparse/IfcParse.cpp | 128 +- src/ifcparse/IfcParse.h | 4 +- src/ifcparse/IfcSIPrefix.cpp | 18 + src/ifcparse/IfcSchema.cpp | 12 + src/ifcparse/IfcSchema.h | 4 +- src/ifcpatch/COPYING | 621 + src/ifcpatch/COPYING.LESSER | 165 + src/ifcpatch/README.md | 11 +- src/ifcpatch/bootstrap.py | 28 + src/ifcpatch/ifcpatch/__init__.py | 117 +- src/ifcpatch/ifcpatch/__main__.py | 29 +- src/ifcpatch/ifcpatch/make.py | 23 - .../ifcpatch/recipes/ConvertLengthUnit.py | 19 + .../recipes/ConvertPropertiesToQuantities.py | 19 + .../recipes/DowngradeIndexedPolyCurve.py | 52 + .../ifcpatch/recipes/ExtractElements.py | 31 +- .../ifcpatch/recipes/ExtractSpaces.py | 19 + .../recipes/MergeDuplicateTypesByTag.py | 40 + src/ifcpatch/ifcpatch/recipes/MergeProject.py | 19 + src/ifcpatch/ifcpatch/recipes/Migrate.py | 19 + .../recipes/OffsetObjectPlacements.py | 19 + .../recipes/OffsetStoreyElevations.py | 19 + src/ifcpatch/ifcpatch/recipes/Optimise.py | 19 + .../recipes/RecycleNonRootedElements.py | 19 + .../ifcpatch/recipes/RegenerateGlobalIds.py | 46 + .../recipes/RemoveSiteRepresentation.py | 19 + .../recipes/ResetAbsoluteCoordinates.py | 19 + .../recipes/ResetSpatialElementLocations.py | 19 + .../ifcpatch/recipes/SetRefElevation.py | 19 + .../ifcpatch/recipes/SplitByBuildingStorey.py | 19 + src/ifcpatch/ifcpatch/recipes/__init__.py | 19 + src/ifcpatch/make.py | 26 + src/ifcwrap/IfcGeomWrapper.i | 6 +- src/ifcwrap/IfcParseWrapper.i | 38 +- src/ifcwrap/IfcPython.i | 10 + src/opencdeserver/README.md | 27 + .../opencdeserver/bcf/project.json | 444 + src/opencdeserver/opencdeserver/bcf/routes.py | 650 + .../Collaboration/Action/comment_actions.json | 10 + .../Collaboration/Action/project_actions.json | 11 + .../Collaboration/Action/topic_actions.json | 16 + .../Action/viewpoint_actions.json | 7 + .../Collaboration/Comment/comment_GET.json | 47 + .../Collaboration/Comment/comment_POST.json | 18 + .../Collaboration/Comment/comment_PUT.json | 14 + .../Collaboration/Document/document_GET.json | 14 + .../document_reference_GET.json | 21 + .../document_reference_POST.json | 18 + .../document_reference_PUT.json | 15 + .../Events/comment_event_GET.json | 29 + .../Collaboration/Events/event_action.json | 14 + .../Collaboration/Events/topic_event_GET.json | 25 + .../schemas/Collaboration/File/file_GET.json | 28 + .../schemas/Collaboration/File/file_PUT.json | 26 + .../project_file_display_information.json | 16 + .../File/project_file_information.json | 13 + .../File/project_files_information_GET.json | 8 + .../RelatedTopic/related_topic_GET.json | 12 + .../RelatedTopic/related_topic_PUT.json | 12 + .../Collaboration/Topic/bim_snippet.json | 23 + .../Collaboration/Topic/topic_GET.json | 101 + .../Collaboration/Topic/topic_POST.json | 68 + .../Collaboration/Topic/topic_PUT.json | 64 + .../Collaboration/Viewpoint/bitmap_GET.json | 24 + .../Collaboration/Viewpoint/bitmap_POST.json | 25 + .../Viewpoint/clipping_plane.json | 13 + .../Collaboration/Viewpoint/coloring.json | 12 + .../Collaboration/Viewpoint/coloring_GET.json | 14 + .../Collaboration/Viewpoint/component.json | 15 + .../Viewpoint/component_list.json | 7 + .../Collaboration/Viewpoint/components.json | 18 + .../Collaboration/Viewpoint/direction.json | 15 + .../schemas/Collaboration/Viewpoint/line.json | 12 + .../Collaboration/Viewpoint/location.json | 16 + .../Viewpoint/orthogonal_camera.json | 22 + .../Viewpoint/perspective_camera.json | 22 + .../Collaboration/Viewpoint/point.json | 15 + .../Viewpoint/selection_GET.json | 11 + .../Collaboration/Viewpoint/snapshot_GET.json | 10 + .../Viewpoint/snapshot_POST.json | 14 + .../Viewpoint/view_setup_hints.json | 18 + .../Viewpoint/viewpoint_GET.json | 52 + .../Viewpoint/viewpoint_POST.json | 46 + .../Collaboration/Viewpoint/visibility.json | 16 + .../Viewpoint/visibility_GET.json | 12 + .../bcf/schemas/Project/extensions_GET.json | 78 + .../bcf/schemas/Project/project_GET.json | 25 + .../bcf/schemas/Project/project_PUT.json | 12 + .../bcf/schemas/User/user_GET.json | 14 + .../opencdeserver/bcf/schemas/error.json | 12 + .../opencdeserver/bcf/success.txt | 1 + .../opencdeserver/bcf/templates/base.html | 19 + .../opencdeserver/foundation/forms.py | 73 + .../opencdeserver/foundation/models.py | 88 + .../opencdeserver/foundation/oauth2.py | 121 + .../opencdeserver/foundation/routes.py | 321 + .../foundation/templates/base.html | 138 + .../foundation/templates/clientdata.html | 36 + .../foundation/templates/createclient.html | 47 + .../foundation/templates/index.html | 35 + .../foundation/templates/login.html | 46 + .../foundation/templates/oauth.html | 20 + .../foundation/templates/ologin.html | 47 + .../foundation/templates/register.html | 49 + src/opencdeserver/opencdeserver/run.py | 44 + src/opencdeserver/requirements.txt | 9 + src/serializers/SvgSerializer.cpp | 160 +- src/serializers/SvgSerializer.h | 15 +- win/build-deps.cmd | 4 +- 691 files changed, 245770 insertions(+), 8151 deletions(-) create mode 100644 src/bcf/COPYING create mode 100644 src/bcf/COPYING.LESSER create mode 100644 src/bcf/MANIFEST.in create mode 100644 src/bcf/pyproject.toml create mode 100644 src/bcf/setup.cfg create mode 100644 src/bcf/src/bcf/__init__.py rename src/bcf/{ => src}/bcf/bcfxml.py (50%) create mode 100644 src/bcf/src/bcf/v2/__init__.py rename src/bcf/{ => src}/bcf/v2/bcfxml.py (97%) rename src/bcf/{ => src}/bcf/v2/data.py (84%) rename src/bcf/{ => src}/bcf/v2/xsd/markup.xsd (100%) rename src/bcf/{ => src}/bcf/v2/xsd/project.xsd (100%) rename src/bcf/{ => src}/bcf/v2/xsd/version.xsd (100%) rename src/bcf/{ => src}/bcf/v2/xsd/visinfo.xsd (100%) create mode 100644 src/bcf/src/bcf/v3/__init__.py create mode 100644 src/bcf/src/bcf/v3/bcfapi.py rename src/bcf/{ => src}/bcf/v3/bcfxml.py (97%) rename src/bcf/{ => src}/bcf/v3/data.py (84%) rename src/bcf/{ => src}/bcf/v3/xsd/documents.xsd (100%) rename src/bcf/{ => src}/bcf/v3/xsd/extensions.xsd (100%) rename src/bcf/{ => src}/bcf/v3/xsd/markup.xsd (100%) rename src/bcf/{ => src}/bcf/v3/xsd/project.xsd (100%) rename src/bcf/{ => src}/bcf/v3/xsd/shared-types.xsd (100%) rename src/bcf/{ => src}/bcf/v3/xsd/version.xsd (100%) rename src/bcf/{ => src}/bcf/v3/xsd/visinfo.xsd (100%) create mode 100644 src/blenderbim/COPYING delete mode 100644 src/blenderbim/RenameObjects.py create mode 100644 src/blenderbim/blenderbim/bim/module/__init__.py create mode 100644 src/blenderbim/blenderbim/bim/module/lca/__init__.py create mode 100644 src/blenderbim/blenderbim/bim/module/lca/operator.py create mode 100644 src/blenderbim/blenderbim/bim/module/lca/prop.py create mode 100644 src/blenderbim/blenderbim/bim/module/lca/ui.py delete mode 100644 src/blenderbim/blenderbim/bim/module/model/column.py create mode 100644 src/blenderbim/blenderbim/bim/module/model/profile.py create mode 100644 src/blenderbim/blenderbim/bim/module/model/root.py create mode 100644 src/blenderbim/blenderbim/bim/module/model/task.py delete mode 100644 src/blenderbim/blenderbim/bim/module/parametric/__init__.py delete mode 100644 src/blenderbim/blenderbim/bim/module/parametric/operator.py delete mode 100644 src/blenderbim/blenderbim/bim/module/parametric/ui.py create mode 100644 src/blenderbim/blenderbim/bim/module/profile/__init__.py create mode 100644 src/blenderbim/blenderbim/bim/module/profile/operator.py create mode 100644 src/blenderbim/blenderbim/bim/module/profile/prop.py create mode 100644 src/blenderbim/blenderbim/bim/module/profile/ui.py create mode 100644 src/blenderbim/blenderbim/bim/module/style/prop.py create mode 100644 src/blenderbim/blenderbim/bim/module/system/__init__.py create mode 100644 src/blenderbim/blenderbim/bim/module/system/operator.py create mode 100644 src/blenderbim/blenderbim/bim/module/system/prop.py create mode 100644 src/blenderbim/blenderbim/bim/module/system/ui.py create mode 100644 src/blenderbim/blenderbim/bim/module/unit/prop.py rename src/{bcf/bcf/__init__.py => blenderbim/blenderbim/libs/site/packages/.gitignore} (100%) delete mode 100644 src/blenderbim/blenderbim/libs/site/packages/OCC.pth delete mode 100644 src/blenderbim/blenderbim/libs/site/packages/ifcopenshell.pth delete mode 100644 src/blenderbim/blenderbim/libs/site/packages/svgwrite.pth create mode 100644 src/blenderbim/demo-library.blend create mode 100644 src/blenderbim/docs/blenderbim/georeferencing.rst create mode 100644 src/blenderbim/generate_demo_library.py create mode 100644 src/blenderbim/generate_site_library.py create mode 100644 src/blenderbim/libraries/blenderbim-demo-library.ifc create mode 100644 src/blenderbim/libraries/blenderbim-site-library.ifc create mode 100644 src/blenderbim/setup_pytest.py create mode 100644 src/blenderbim/site-library.blend create mode 100644 src/blenderbim/test/__init__.py create mode 100644 src/blenderbim/test/bim/__init__.py create mode 100644 src/blenderbim/test/bim/bootstrap.py create mode 100644 src/blenderbim/test/bim/module/__init__.py create mode 100644 src/blenderbim/test/bim/module/bimtester/__init__.py create mode 100644 src/blenderbim/test/bim/module/bimtester/test_operator.py create mode 100644 src/blenderbim/test/bim/module/drawing/__init__.py rename src/blenderbim/{tests => test/bim/module/drawing}/test_segment_clipping.py (58%) create mode 100644 src/blenderbim/test/bim/module/patch/__init__.py create mode 100644 src/blenderbim/test/bim/module/patch/test_operator.py create mode 100644 src/blenderbim/test/bim/module/project/__init__.py create mode 100644 src/blenderbim/test/bim/module/project/test_operator.py create mode 100644 src/blenderbim/test/bim/module/root/__init__.py create mode 100644 src/blenderbim/test/bim/module/root/test_operator.py create mode 100644 src/blenderbim/test/bim/module/sequence/__init__.py create mode 100644 src/blenderbim/test/bim/module/sequence/test_operator.py create mode 100644 src/blenderbim/test/bim/module/void/__init__.py create mode 100644 src/blenderbim/test/bim/module/void/test_operator.py create mode 100644 src/blenderbim/test/files/basic.blend create mode 100644 src/blenderbim/test/files/basic.ifc create mode 100644 src/blenderbim/test/files/decomposition.ifc create mode 100644 src/blenderbim/test/files/manual-geolocation.ifc create mode 100644 src/blenderbim/test/files/sample-ids.xml create mode 100644 src/bsdd/COPYING create mode 100644 src/bsdd/COPYING.LESSER create mode 100644 src/ifc2ca/COPYING create mode 100644 src/ifc2ca/COPYING.LESSER create mode 100644 src/ifc4d/COPYING create mode 100644 src/ifc4d/COPYING.LESSER create mode 100644 src/ifc4d/README.md rename src/{ifcp6/ifcp6 => ifc4d/ifc4d}/msp2ifc.py (86%) rename src/{ifcp6/ifcp6 => ifc4d/ifc4d}/p62ifc.py (95%) create mode 100644 src/ifc5d/COPYING create mode 100644 src/ifc5d/COPYING.LESSER create mode 100644 src/ifc5d/README.md create mode 100644 src/ifc5d/ifc5d/csv2ifc.py create mode 100644 src/ifc5d/rates.csv create mode 100644 src/ifc5d/schedule.csv create mode 100644 src/ifcbimtester/COPYING create mode 100644 src/ifcbimtester/COPYING.LESSER create mode 100644 src/ifcbimtester/bimtester/features/steps/attributes_psets/de.py create mode 100644 src/ifcbimtester/bimtester/features/steps/attributes_psets/en.py delete mode 100644 src/ifcbimtester/examples/steps/attributes_psets.py delete mode 100644 src/ifcbimtester/examples/steps/attributes_psets_de.py delete mode 100644 src/ifcbimtester/examples/steps/attributes_psets_fr.py delete mode 100644 src/ifcbimtester/examples/steps/attributes_psets_methods.py create mode 100644 src/ifccityjson/COPYING create mode 100644 src/ifccityjson/COPYING.LESSER delete mode 100644 src/ifccityjson/example/3DBAG_example.json create mode 100644 src/ifccityjson/example/geometries.json create mode 100644 src/ifcclash/COPYING create mode 100644 src/ifcclash/COPYING.LESSER create mode 100644 src/ifcclash/README.md create mode 100644 src/ifcclash/bootstrap.py delete mode 100644 src/ifcclash/clashsets.json delete mode 100644 src/ifcclash/collision.py delete mode 100644 src/ifcclash/ifcclash.py create mode 100644 src/ifcclash/ifcclash/__main__.py create mode 100644 src/ifcclash/ifcclash/collider.py create mode 100644 src/ifcclash/ifcclash/ifcclash.py create mode 100644 src/ifcclash/make.py create mode 100644 src/ifccobie/COPYING create mode 100644 src/ifccobie/COPYING.LESSER create mode 100644 src/ifccsv/COPYING create mode 100644 src/ifccsv/COPYING.LESSER create mode 100644 src/ifcdiff/COPYING create mode 100644 src/ifcdiff/COPYING.LESSER create mode 100644 src/ifcfm/COPYING create mode 100644 src/ifcfm/COPYING.LESSER create mode 100644 src/ifcfm/README.md create mode 100644 src/ifcfm/ifcfm/parser.py create mode 100644 src/ifcfm/ifcfm/writer.py create mode 100644 src/ifcopenshell-python/Makefile create mode 100644 src/ifcopenshell-python/README.md create mode 100644 src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py delete mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_product.py delete mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_product_quantities.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_value.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value_formula.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py rename src/ifcopenshell-python/ifcopenshell/api/cost/{unassign_cost_item_product.py => unassign_cost_item_quantity.py} (66%) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/material/copy_material.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py rename src/ifcopenshell-python/ifcopenshell/api/{cost/remove_cost_item_value.py => profile/remove_profile.py} (61%) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_time.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_work.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource_quantity.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py rename src/ifcopenshell-python/ifcopenshell/api/{geometry/assign_styles.py => style/assign_representation_styles.py} (86%) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/data.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py delete mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/edit_style.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/system/add_system.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/system/data.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/system/remove_system.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/unit/add_context_dependent_unit.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/unit/add_monetary_unit.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/unit/remove_unit.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py create mode 100644 src/ifcopenshell-python/ifcopenshell/ids.xsd create mode 100644 src/ifcopenshell-python/ifcopenshell/util/cost.py create mode 100644 src/ifcopenshell-python/ifcopenshell/util/fm.py rename src/{bcf/bcf/v3/bcfapi.py => ifcopenshell-python/test/__init__.py} (100%) create mode 100644 src/ifcopenshell-python/test/api/__init__.py create mode 100644 src/ifcopenshell-python/test/api/geometry/__init__.py create mode 100644 src/ifcopenshell-python/test/api/geometry/test_edit_object_placement.py create mode 100644 src/ifcopenshell-python/test/api/owner/__init__.py create mode 100644 src/ifcopenshell-python/test/api/owner/test_update_owner_history.py create mode 100644 src/ifcopenshell-python/test/api/pset/__init__.py create mode 100644 src/ifcopenshell-python/test/api/pset/test_edit_pset.py create mode 100644 src/ifcopenshell-python/test/api/resource/__init__.py create mode 100644 src/ifcopenshell-python/test/api/resource/test_calculate_resource_work.py create mode 100644 src/ifcopenshell-python/test/api/root/__init__.py create mode 100644 src/ifcopenshell-python/test/api/root/test_copy_class.py create mode 100644 src/ifcopenshell-python/test/api/sequence/__init__.py create mode 100644 src/ifcopenshell-python/test/api/sequence/test_calculate_task_duration.py create mode 100644 src/ifcopenshell-python/test/api/spatial/__init__.py create mode 100644 src/ifcopenshell-python/test/api/spatial/test_assign_container.py create mode 100644 src/ifcopenshell-python/test/api/style/test_edit_surface_style.py create mode 100644 src/ifcopenshell-python/test/api/void/__init__.py create mode 100644 src/ifcopenshell-python/test/api/void/test_add_filling.py create mode 100644 src/ifcopenshell-python/test/api/void/test_add_opening.py create mode 100644 src/ifcopenshell-python/test/api/void/test_remove_opening.py create mode 100644 src/ifcopenshell-python/test/bootstrap.py create mode 100644 src/ifcopenshell-python/test/test_file.py create mode 100644 src/ifcopenshell-python/test/test_ids.py create mode 100644 src/ifcopenshell-python/test/util/__init__.py create mode 100644 src/ifcopenshell-python/test/util/test_element.py rename src/ifcopenshell-python/{ifcopenshell => test}/util/test_pset.py (100%) create mode 100644 src/ifcparse/Ifc4x3_rc3-definitions.h create mode 100644 src/ifcparse/Ifc4x3_rc3-schema.cpp create mode 100644 src/ifcparse/Ifc4x3_rc3.cpp create mode 100644 src/ifcparse/Ifc4x3_rc3.h create mode 100644 src/ifcparse/Ifc4x3_rc4-definitions.h create mode 100644 src/ifcparse/Ifc4x3_rc4-schema.cpp create mode 100644 src/ifcparse/Ifc4x3_rc4.cpp create mode 100644 src/ifcparse/Ifc4x3_rc4.h create mode 100644 src/ifcpatch/COPYING create mode 100644 src/ifcpatch/COPYING.LESSER create mode 100644 src/ifcpatch/bootstrap.py delete mode 100644 src/ifcpatch/ifcpatch/make.py create mode 100644 src/ifcpatch/ifcpatch/recipes/DowngradeIndexedPolyCurve.py create mode 100644 src/ifcpatch/ifcpatch/recipes/MergeDuplicateTypesByTag.py create mode 100644 src/ifcpatch/ifcpatch/recipes/RegenerateGlobalIds.py create mode 100644 src/ifcpatch/make.py create mode 100644 src/opencdeserver/README.md create mode 100644 src/opencdeserver/opencdeserver/bcf/project.json create mode 100644 src/opencdeserver/opencdeserver/bcf/routes.py create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/comment_actions.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/project_actions.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/topic_actions.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/viewpoint_actions.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_POST.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_PUT.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Document/document_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_POST.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_PUT.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/comment_event_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/event_action.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/topic_event_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/file_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/file_PUT.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_file_display_information.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_file_information.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_files_information_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/RelatedTopic/related_topic_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/RelatedTopic/related_topic_PUT.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/bim_snippet.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_POST.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_PUT.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/bitmap_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/bitmap_POST.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/clipping_plane.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/coloring.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/coloring_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/component.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/component_list.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/components.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/direction.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/line.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/location.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/orthogonal_camera.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/perspective_camera.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/point.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/selection_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/snapshot_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/snapshot_POST.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/view_setup_hints.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/viewpoint_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/viewpoint_POST.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/visibility.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/visibility_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Project/extensions_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Project/project_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/Project/project_PUT.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/User/user_GET.json create mode 100644 src/opencdeserver/opencdeserver/bcf/schemas/error.json create mode 100644 src/opencdeserver/opencdeserver/bcf/success.txt create mode 100644 src/opencdeserver/opencdeserver/bcf/templates/base.html create mode 100644 src/opencdeserver/opencdeserver/foundation/forms.py create mode 100644 src/opencdeserver/opencdeserver/foundation/models.py create mode 100644 src/opencdeserver/opencdeserver/foundation/oauth2.py create mode 100644 src/opencdeserver/opencdeserver/foundation/routes.py create mode 100644 src/opencdeserver/opencdeserver/foundation/templates/base.html create mode 100644 src/opencdeserver/opencdeserver/foundation/templates/clientdata.html create mode 100644 src/opencdeserver/opencdeserver/foundation/templates/createclient.html create mode 100644 src/opencdeserver/opencdeserver/foundation/templates/index.html create mode 100644 src/opencdeserver/opencdeserver/foundation/templates/login.html create mode 100644 src/opencdeserver/opencdeserver/foundation/templates/oauth.html create mode 100644 src/opencdeserver/opencdeserver/foundation/templates/ologin.html create mode 100644 src/opencdeserver/opencdeserver/foundation/templates/register.html create mode 100644 src/opencdeserver/opencdeserver/run.py create mode 100644 src/opencdeserver/requirements.txt diff --git a/.gitignore b/.gitignore index 2d0f1d3b67..82204797f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # Dependency and build folders created by the build scripts +/_build-vs2015-x64/ +/_deps-vs2015-x64-installed/ /_build-vs2017-x64/ /_deps-vs2017-x64-installed/ +/_build-vs2019-x64/ +/_deps-vs2019-x64-installed/ /_deps/ /deps*/ /build*/ @@ -28,7 +32,32 @@ Pipfile.lock # gettext binary translation files *.mo + # Vim *.swp +*.swo +# Flask +instance/* +!instance/.gitignore +.webassets-cache +.env +*.db +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +# pipenv +Pipfile +Pipfile.lock + +# Database +*.db + +# PyTest +htmlcov +.coverage + +# Blender +*.blend1 diff --git a/.travis.yml b/.travis.yml index 84b382fae9..00614ac0d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,10 +22,15 @@ addons: - nlohmann-json3-dev - opencollada-dev - python3-all-dev + - python3-pip - swig before_script: - if [ $TRAVIS_OS_NAME == "linux" ]; then ccache -z; fi + +install: + # for IDS + - python3 -m pip install xmlschema script: - pwd @@ -39,23 +44,24 @@ script: -DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/python3 \ -DPYTHON_INCLUDE_DIR:PATH=/usr/include/python3.8 \ -DPYTHON_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libpython3.8.so \ + "-DSCHEMA_VERSIONS=2x3;4" \ -DGLTF_SUPPORT=On \ -DJSON_INCLUDE_DIR=/usr/include \ ../cmake # TODO: Drop sudo - sudo make -j $(nproc) install - + - cd ../test - /usr/bin/python tests.py + - /usr/bin/python ../src/ifcopenshell-python/ifcopenshell/test_ids.py - cd input - + - /usr/local/bin/IfcConvert -yv acad2010_walls.ifc acad2010_walls.glb - # - gltf_validator acad2010_walls.glb - + - /usr/bin/python -c "from io import open; import ifcopenshell; f = ifcopenshell.open('encoding.ifc'); assert list(map(ord, f[1][0])) == [39, 97, 39, 32, 49, 109, 179, 32, 8804, 32, 53, 109, 179, 32, 8805, 32, 49, 48, 109, 179]" - + - | (for i in *.ifc; do \ echo $i | tee -a log; \ diff --git a/COPYING b/COPYING index e587591e14..ed990f8cd2 100644 --- a/COPYING +++ b/COPYING @@ -1,7 +1,7 @@ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. diff --git a/COPYING.LESSER b/COPYING.LESSER index 65c5ca88a6..0a041280bd 100644 --- a/COPYING.LESSER +++ b/COPYING.LESSER @@ -1,7 +1,7 @@ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. diff --git a/README.md b/README.md index 0cdd3b2653..a28b3a58df 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,44 @@ Usage examples >>> # Writing IFC-SPF files to disk: >>> f.write("out.ifc") -[LGPL]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/COPYING "LGPL" +Extra tools +----------- + +Also available are a series of utilities that are based on or related to IfcOpenShell. + +Those marked with an asterisk are part of IfcOpenShell. + +Name | License +--- | --- +bcf | LGPL-3.0-or-later +blenderbim | GPL-3.0-or-later +bsdd | LGPL-3.0-or-later +ifc2ca | LGPL-3.0-or-later +ifc4d | LGPL-3.0-or-later +ifc5d | LGPL-3.0-or-later +ifcbimtester | LGPL-3.0-or-later +ifcblender | LGPL-3.0-or-later\* +ifccityjson | LGPL-3.0-or-later +ifcclash | LGPL-3.0-or-later +ifccobie | LGPL-3.0-or-later +ifcconvert | LGPL-3.0-or-later\* +ifccsv | LGPL-3.0-or-later +ifcdiff | LGPL-3.0-or-later +ifcfm | LGPL-3.0-or-later +ifcgeom | LGPL-3.0-or-later\* +ifcgeom\_schema\_agnostic | LGPL-3.0-or-later\* +ifcgeomserver | LGPL-3.0-or-later\* +ifcjni | LGPL-3.0-or-later\* +ifcmax | LGPL-3.0-or-later\* +ifcopenshell-python | LGPL-3.0-or-later\* +ifcparse | LGPL-3.0-or-later\* +ifcpatch | LGPL-3.0-or-later +ifcsverchok | GPL-3.0-or-later +ifcwrap | LGPL-3.0-or-later\* +qtviewer | LGPL-3.0-or-later\* +serializers | LGPL-3.0-or-later\* + +[LGPL]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/COPYING.LESSER "LGPL-3.0-or-later" [IFC]: https://technical.buildingsmart.org/standards/ifc/ "IFC" [IFC2x3 TC1]: https://standards.buildingsmart.org/IFC/RELEASE/IFC2x3/TC1/HTML/ "IFC2x3 TC1" [IFC4 Add2 TC1]: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/ "IFC4 Add2 TC1" diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 7069ad6fab..2b4b9bbc97 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -494,7 +494,9 @@ function(files_for_ifc_version IFC_VERSION RESULT_NAME) ) endfunction() -set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2") +if(NOT SCHEMA_VERSIONS) + set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3_rc1" "4x3_rc2" "4x3_rc3" "4x3_rc4") +endif() foreach(s ${SCHEMA_VERSIONS}) add_definitions(-DHAS_SCHEMA_${s}) diff --git a/src/bcf/COPYING b/src/bcf/COPYING new file mode 100644 index 0000000000..810fce6e9b --- /dev/null +++ b/src/bcf/COPYING @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/src/bcf/COPYING.LESSER b/src/bcf/COPYING.LESSER new file mode 100644 index 0000000000..0a041280bd --- /dev/null +++ b/src/bcf/COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/src/bcf/MANIFEST.in b/src/bcf/MANIFEST.in new file mode 100644 index 0000000000..30feaf47bc --- /dev/null +++ b/src/bcf/MANIFEST.in @@ -0,0 +1,2 @@ +include src/bcf/v2/xsd/*.xsd +include src/bcf/v3/xsd/*.xsd \ No newline at end of file diff --git a/src/bcf/README.md b/src/bcf/README.md index 1ce991d97f..812c0d6f04 100644 --- a/src/bcf/README.md +++ b/src/bcf/README.md @@ -4,16 +4,16 @@ A simple Python implementation of BCF. The data model is described in `data.py`. Manipulation of BCF-XML is available via `bcfxml.py` and manipulation of BCF-API is available via `bcfapi.py`. - - BCF-XML version 2.1: Fully supported - - BCF-API version 2.1: Not supported, will probably tackle this after BCF-API v3.0 - - BCF-XML version 3.0: Almost fully supported, except for the documents module - - BCF-API version 3.0: Not supported, but work underway to support it +- BCF-XML version 2.1: Fully supported +- BCF-API version 2.1: Not supported, will probably tackle this after BCF-API v3.0 +- BCF-XML version 3.0: Almost fully supported, except for the documents module +- BCF-API version 3.0: Almost fully supported, except for two requests. ## bcfxml The `bcfxml` module lets you interact with the BCF-XML standard. -``` +```python from bcf import bcfxml @@ -60,3 +60,42 @@ topic = bcfxml.get_topic(guid) topic.title = "New title" bcfxml.edit_topic(topic) ``` + +## bcfapi + +The `bcfapi` module lets you interact with the BCF-API standard. + +```python +from bcf.v3.bcfapi import FoundationClient, BcfClient + +foundation_client = FoundationClient("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET", "OPENCDE_BASEURL") +auth_methods = foundation_client.get_auth_methods() + +# Our library currently only implements the authorization_code flow +if "authorization_code" in auth_methods: + foundation_client.login() + +bcf_client = BcfClient(foundation_client) + +versions = foundation_client.get_versions() +for version in versions: +if "3.0" in versions: + if version["api_id"] == "bcf" and version["version_id"] == "3.0": + bcf_client.set_version(version) + +data = bcf_client.get_projects() +print(data) +project_id = data[0]["project_id"] +print(project_id) +data = bcf_client.get_project(project_id) +print(data) +data = bcf_client.get_extensions(project_id) +print(data) +``` + +## Todo List + +The remaining work that needs to be completed in `bcfxml.py` and `bcfapi.py`. + +- For `bcfxml.py` two xsds support is remaining namely 'documents.xsd`and`extensions.xsd`. +- For `bcfapi.py` two requests that are `get_topics` and `get_comments` are remaining. diff --git a/src/bcf/pyproject.toml b/src/bcf/pyproject.toml new file mode 100644 index 0000000000..dadda3ed02 --- /dev/null +++ b/src/bcf/pyproject.toml @@ -0,0 +1,12 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 120 + +[tool.isort] +profile = "black" \ No newline at end of file diff --git a/src/bcf/setup.cfg b/src/bcf/setup.cfg new file mode 100644 index 0000000000..3eba915750 --- /dev/null +++ b/src/bcf/setup.cfg @@ -0,0 +1,37 @@ +[metadata] +name=bcf-client +version=0.0.1 +author = Ifcopenshell +description = A simple Python implementation of BCF +url = https://github.com/IfcOpenShell/IfcOpenShell +project_urls = + Code=https://github.com/IfcOpenShell/IfcOpenShell + Issues=https://github.com/IfcOpenShell/IfcOpenShell/issues +long_description = file: README.md +long_description_content_type = text/markdown +classifiers = + License :: OSI Approved :: GNU General Public License v3 (GPLv3) + Operating System :: OS Independent + Programming Language :: Python :: 3 + Topic :: Scientific/Engineering + Topic :: Utilities +keywords = + Python + file formats + engineering + +[options] +package_dir = + = src +packages = find: +python_requires = >=3 +install_requires= + xmlschema +include_package_data = True + +[options.packages.find] +where = src + +[flake8] +max-line-length = 120 +ignore = E24, E121, E123, E126, E203, E226, E704, E741, W503, W504 diff --git a/src/bcf/src/bcf/__init__.py b/src/bcf/src/bcf/__init__.py new file mode 100644 index 0000000000..a91684cdf7 --- /dev/null +++ b/src/bcf/src/bcf/__init__.py @@ -0,0 +1,17 @@ +# BCF - BCF Python library +# Copyright (C) 2021 Prabhat Singh +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . diff --git a/src/bcf/bcf/bcfxml.py b/src/bcf/src/bcf/bcfxml.py similarity index 50% rename from src/bcf/bcf/bcfxml.py rename to src/bcf/src/bcf/bcfxml.py index 28b25a8db1..e9bcde349f 100644 --- a/src/bcf/bcf/bcfxml.py +++ b/src/bcf/src/bcf/bcfxml.py @@ -1,3 +1,22 @@ +# BCF - BCF Python library +# Copyright (C) 2021 Prabhat Singh +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . + + import os.path import zipfile import tempfile @@ -9,18 +28,21 @@ def load(filepath): if os.path.isfile(os.path.join(filepath, "bcf.version")): version_path = os.path.join(filepath, "bcf.version") version_id = get_version(version_path) - if version_id == "2.1": + # TODO: we actually coded it for 2.1, let's check the difference between 2.0 and 2.1 + if version_id == "2.1" or version_id == "2.0": from bcf.v2.bcfxml import BcfXml bcfxml = BcfXml() bcfxml.filepath = filepath return bcfxml - else: + elif version_id == "3.0": from bcf.v3.bcfxml import BcfXml bcfxml = BcfXml() bcfxml.filepath = filepath return bcfxml + else: + raise Exception(f"Version {version_id} not supported.") def get_version(version_path): diff --git a/src/bcf/src/bcf/v2/__init__.py b/src/bcf/src/bcf/v2/__init__.py new file mode 100644 index 0000000000..adb79b4034 --- /dev/null +++ b/src/bcf/src/bcf/v2/__init__.py @@ -0,0 +1,19 @@ + +# BCF - BCF Python library +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . + diff --git a/src/bcf/bcf/v2/bcfxml.py b/src/bcf/src/bcf/v2/bcfxml.py similarity index 97% rename from src/bcf/bcf/v2/bcfxml.py rename to src/bcf/src/bcf/v2/bcfxml.py index 3b4dd722b0..76c094d69e 100644 --- a/src/bcf/bcf/v2/bcfxml.py +++ b/src/bcf/src/bcf/v2/bcfxml.py @@ -1,3 +1,22 @@ + +# BCF - BCF Python library +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . + import os import uuid import shutil diff --git a/src/bcf/bcf/v2/data.py b/src/bcf/src/bcf/v2/data.py similarity index 84% rename from src/bcf/bcf/v2/data.py rename to src/bcf/src/bcf/v2/data.py index 534aacaffc..e79352dbd0 100644 --- a/src/bcf/bcf/v2/data.py +++ b/src/bcf/src/bcf/v2/data.py @@ -1,3 +1,22 @@ + +# BCF - BCF Python library +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . + class Project: def __init__(self): self.project_id = "" diff --git a/src/bcf/bcf/v2/xsd/markup.xsd b/src/bcf/src/bcf/v2/xsd/markup.xsd similarity index 100% rename from src/bcf/bcf/v2/xsd/markup.xsd rename to src/bcf/src/bcf/v2/xsd/markup.xsd diff --git a/src/bcf/bcf/v2/xsd/project.xsd b/src/bcf/src/bcf/v2/xsd/project.xsd similarity index 100% rename from src/bcf/bcf/v2/xsd/project.xsd rename to src/bcf/src/bcf/v2/xsd/project.xsd diff --git a/src/bcf/bcf/v2/xsd/version.xsd b/src/bcf/src/bcf/v2/xsd/version.xsd similarity index 100% rename from src/bcf/bcf/v2/xsd/version.xsd rename to src/bcf/src/bcf/v2/xsd/version.xsd diff --git a/src/bcf/bcf/v2/xsd/visinfo.xsd b/src/bcf/src/bcf/v2/xsd/visinfo.xsd similarity index 100% rename from src/bcf/bcf/v2/xsd/visinfo.xsd rename to src/bcf/src/bcf/v2/xsd/visinfo.xsd diff --git a/src/bcf/src/bcf/v3/__init__.py b/src/bcf/src/bcf/v3/__init__.py new file mode 100644 index 0000000000..5828265521 --- /dev/null +++ b/src/bcf/src/bcf/v3/__init__.py @@ -0,0 +1,19 @@ + +# BCF - BCF Python library +# Copyright (C) 2021 Prabhat Singh +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . + diff --git a/src/bcf/src/bcf/v3/bcfapi.py b/src/bcf/src/bcf/v3/bcfapi.py new file mode 100644 index 0000000000..308f323eb8 --- /dev/null +++ b/src/bcf/src/bcf/v3/bcfapi.py @@ -0,0 +1,603 @@ + +# BCF - BCF Python library +# Copyright (C) 2021 Prabhat Singh +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . + +import uuid +import time +import json +import urllib +import requests +import webbrowser +import http.server +import base64 +import tempfile +import os + +client_id, client_secret = "", "" + + +class OAuthReceiver(http.server.BaseHTTPRequestHandler): + def do_GET(self): + query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) + self.server.auth_code = query.get("code", [""])[0] + self.server.auth_state = query.get("state", [""])[0] + self.send_response(200) + self.send_header("Content-type", "text/plain") + self.end_headers() + self.wfile.write("You have now authenticated :) You may now close this browser window.".encode("utf-8")) + + +class FoundationClient: + def __init__(self, client_id, client_secret, base_url=None, redirect_subdir=None): + self.baseurl = base_url + self.access_token = "" + self.refresh_token = "" + self.access_token_expires_on = time.time() + self.refresh_token_expires_on = float("inf") + self.auth_endpoint = None + self.token_endpoint = None + self.client_id = client_id + self.client_secret = client_secret + self.auth_method = None + self.redirect_subdir = redirect_subdir + + def get_access_token(self): + if self.access_token and self.access_token_expires_on > time.time(): + return self.access_token + elif self.refresh_token and self.refresh_token_expires_on > time.time(): + self.get_refresh_token() + else: + self.login() + return self.access_token + + def get_auth_methods(self): + resp = requests.get(f"{self.baseurl}foundation/1.0/auth") + return resp.json()["supported_oauth2_flows"] + + def get_versions(self): + resp = requests.get(f"{self.baseurl}foundation/versions") + return resp.json()["versions"] + + def login(self): + resp = requests.get(f"{self.baseurl}foundation/1.0/auth") + values = resp.json() + self.auth_endpoint = values["oauth2_auth_url"] + self.token_endpoint = values["oauth2_token_url"] + + with http.server.HTTPServer(("", 8080), OAuthReceiver) as server: + state = str(uuid.uuid4()) + query = urllib.parse.urlencode( + { + "client_id": self.client_id, + "response_type": "code", + "state": state, + "redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}", + } + ) + if "?" in self.auth_endpoint: + webbrowser.open(f"{self.auth_endpoint}&{query}") + else: + webbrowser.open(f"{self.auth_endpoint}?{query}") + server.timeout = 100 + server.state = state + server.handle_request() + if server.auth_code and server.auth_state == state: + data = { + "grant_type": "authorization_code", + "code": server.auth_code, + "redirect_uri": f"http://localhost:{server.server_address[1]}/{self.redirect_subdir}", + } + auth_string = f"{self.client_id}:{self.client_secret}" + header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8") + headers = {"Authorization": f"Basic {header_string}"} + self.set_tokens_from_response(requests.post(self.token_endpoint, data=data, headers=headers)) + + def get_refresh_token(self): + self.set_tokens_from_response( + requests.post( + self.token_endpoint, + params={ + "grant_type": "refresh_token", + "refresh_token": self.refresh_token, + }, + ).json() + ) + + def get_new_access_token(self): + auth_string = f"{self.client_id}:{self.client_secret}" + header_string = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8") + headers = {"Authorization": f"Basic {header_string}"} + self.set_tokens_from_response( + requests.post( + self.token_endpoint, + data={ + "grant_type": "refresh_token", + "refresh_token": self.refresh_token, + }, + headers=headers, + ).json() + ) + + def set_auth_method(self, method="authorization_code_grant"): + if method != "authorization_code_grant": + raise NotImplementedError(f"{method} not supported") + else: + self.auth_method = method + + def set_tokens_from_response(self, response): + response = response.json() + self.access_token = response["access_token"] + self.refresh_token = response["refresh_token"] + self.access_token_expires_on = time.time() + response["expires_in"] + if "refresh_token_expires_in" in response: + self.refresh_token_expires_on = time.time() + response["refresh_token_expires_in"] + + +class BcfClient: + def __init__(self, foundation_client): + self.foundation_client = foundation_client + self.version_id = None + self.baseurl = None + self.filepath = tempfile.mkdtemp() + + def set_version(self, version): + self.version_id = version["version_id"] + self.baseurl = version["api_base_url"] + + def get(self, endpoint, params=None, is_auth_required=False): + # TODO: handle error http status codes and raise exception. Follow error.json standard. + headers = {"Authorization": "Bearer " + self.foundation_client.get_access_token()} + response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None) + try: + response = requests.get(f"{self.baseurl}{endpoint}", headers=headers, params=params or None) + if response.status_code == 200: + return response.json() + response.raise_for_status() + except requests.exceptions.HTTPError as e: + print(f"message: {response.reason}' '{response.status_code}' '{ e }") + + def post(self, endpoint, data=None, params=None): + headers = { + "Authorization": "Bearer " + self.foundation_client.get_access_token(), + "Content-type": "application/json", + } + try: + response = requests.post( + f"{self.baseurl}{endpoint}", + headers=headers, + params=params or None, + data=data or None, + ) + if response.status_code == 201: + return response.status_code, response.text + response.raise_for_status() + except requests.exceptions.HTTPError as errh: + print(f"message: {response.reason}' '{response.status_code}, {errh}") + + def put(self, endpoint, data=None, params=None): + headers = { + "Authorization": "Bearer " + self.foundation_client.get_access_token(), + "Content-type": "application/json", + } + try: + response = requests.put( + f"{self.baseurl}{endpoint}", + headers=headers, + params=params or None, + data=data or None, + ) + if response.status_code == 200: + return response.status_code, response.text + response.raise_for_status() + except requests.exceptions.HTTPError as errh: + print(f"message: {response.reason}' '{response.status_code}, {errh}") + + def delete(self, endpoint, params=None): + headers = { + "Authorization": "Bearer " + self.foundation_client.get_access_token(), + "Content-type": "application/json", + } + try: + response = requests.delete( + f"{self.baseurl}{endpoint}", + headers=headers, + params=params or None, + ) + if response.status_code == 200: + return response.status_code, response.text + response.raise_for_status() + except requests.exceptions.HTTPError as errh: + print(f"message: {response.reason}' '{response.status_code}, {errh}") + + def get_projects(self) -> list: + return self.get( + f"/projects", + ) + + def get_project( + self, + project_id="", + ) -> dict: + return self.get( + f"/projects/{project_id}", + { + "project_id": project_id, + }, + ) + + def update_project(self, project_id="", data=None) -> dict: + url = f"{self.baseurl}/projects/{project_id}" + headers = {"Authorization": "Bearer " + self.foundation_client.get_access_token()} + resp = requests.put(url, headers=headers, data=data) + return resp.status_code, resp.text + + def get_extensions( + self, + project_id="", + ) -> dict: + return self.get( + f"/projects/{project_id}/extensions", + { + "project_id": project_id, + }, + ) + + def get_topics( + self, + project_id="", + topics="", + query_string=None, + ) -> list: + # return self.get( + # f"/projects/{project_id}/topics", + # { + # "project_id": project_id, + # "topics": topics, + # "query_string": query_string, + # }, + # ) + pass + + def get_topic(self, project_id="", topic_id="") -> dict: + return self.get( + f"/projects/{project_id}/topics/{topic_id}", + { + "project_id": project_id, + "topic_id": topic_id, + }, + ) + + def create_topic(self, project_id="", data=None): + return self.post(f"/projects/{project_id}/topics", data=data) + + def update_topic(self, project_id="", topic_id="", data=None) -> dict: + return self.put(f"/projects/{project_id}/topics/{topic_id}", data=data) + + def delete_topic(self, project_id="", topic_id=""): + return self.delete(f"/projects/{project_id}/topics/{topic_id}") + + def get_snippet(self, project_id="", topic_id="") -> str: + headers = { + "Authorization": "Bearer " + self.foundation_client.get_access_token(), + "Content-type": "application/octet-stream", + } + response = requests.get( + f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet", + headers=headers, + ) + # TODO: write to tmpdir + with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_snippet.txt"), "wb") as f: + f.write(response.content.decode("utf-8")) + return response.status_code, response.content + + def update_snippet(self, project_id="", topic_id="", files=None, data=None): + headers = { + "Authorization": "Bearer " + self.foundation_client.get_access_token(), + "Content-type": "application/octet-stream", + } + response = requests.put( + f"{self.baseurl}/projects/{project_id}/topics/{topic_id}/snippet", + headers=headers, + files=files, + ) + return response.status_code + + def get_files_information(self, project_id="") -> list: + return self.get( + f"/projects/{project_id}/files_information", + { + "project_id": project_id, + }, + ) + + def get_files(self, project_id="", topic_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/files", + { + "project_id": project_id, + "topic_id": topic_id, + }, + ) + + def update_files( + self, + project_id="", + topic_id="", + data=None, + params=None, + ): + return self.put( + f"/projects/{project_id}/topics/{topic_id}/files", + data=data, + ) + + def get_comments(self, project_id="", topic_id="") -> list: + pass + + def create_comments( + self, + project_id="", + topic_id="", + data=None, + params=None, + ): + return self.post( + f"/projects/{project_id}/topics/{topic_id}/comments", + data=data, + ) + + def get_comment(self, project_id="", topic_id="", comment_id="") -> dict: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", + { + "project_id": project_id, + "topic_id": topic_id, + "comment_id": comment_id, + }, + ) + + def delete_comment(self, project_id="", topic_id="", comment_id=""): + return self.delete(f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}") + + def update_comment( + self, + project_id="", + topic_id="", + comment_id="", + data=None, + ): + return self.put( + f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", + data=data, + ) + + def get_viewpoints(self, project_id="", topic_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/viewpoints", + { + "project_id": project_id, + "topic_id": topic_id, + }, + ) + + def create_viewpoints(self, project_id="", topic_id="", data=None): + return self.post( + f"/projects/{project_id}/topics/{topic_id}/viewpoints", + data=data, + ) + + def get_viewpoint(self, project_id="", topic_id="", viewpoint_id="") -> dict: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}", + { + "project_id": project_id, + "topic_id": topic_id, + "viewpoint_id": viewpoint_id, + }, + ) + + def delete_viewpoint( + self, + project_id="", + topic_id="", + viewpoint_id="", + ): + return self.delete( + f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}", + ) + + def get_snapshot(self, project_id="", topic_id="", viewpoint_id="") -> str: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot", + { + "project_id": project_id, + "topic_id": topic_id, + "viewpoint_id": viewpoint_id, + }, + ) + + def get_bitmap(self, project_id="", topic_id="", viewpoint_id="", bitmap_id="") -> str: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}", + { + "project_id": project_id, + "topic_id": topic_id, + "viewpoint_id": viewpoint_id, + "bitmap_id": bitmap_id, + }, + ) + + def get_selection(self, project_id="", topic_id="", viewpoint_id="") -> dict: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection", + { + "project_id": project_id, + "topic_id": topic_id, + "viewpoint_id": viewpoint_id, + }, + ) + + def get_coloring(self, project_id="", topic_id="", viewpoint_id="") -> dict: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring", + { + "project_id": project_id, + "topic_id": topic_id, + "viewpoint_id": viewpoint_id, + }, + ) + + def get_visibility(self, project_id="", topic_id="", viewpoint_id="") -> dict: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility", + { + "project_id": project_id, + "topic_id": topic_id, + "viewpoint_id": viewpoint_id, + }, + ) + + def get_related_topics(self, project_id="", topic_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/related_topics", + { + "project_id": project_id, + "topic_id": topic_id, + }, + ) + + def update_related_topics( + self, + project_id="", + topic_id="", + data=None, + ): + return self.put( + f"/projects/{project_id}/topics/{topic_id}/related_topics", + data=data, + ) + + def get_document_references(self, project_id="", topic_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/document_references", + { + "project_id": project_id, + "topic_id": topic_id, + }, + ) + + def create_document_reference( + self, + project_id="", + topic_id="", + data=None, + ): + return self.post( + f"/projects/{project_id}/topics/{topic_id}/document_references", + data=data, + ) + + def update_document_references( + self, + project_id="", + topic_id="", + document_reference_id="", + data=None, + ): + return self.put( + f"/projects/{project_id}/topics/{topic_id}/document_references/{document_reference_id}", + data=data, + ) + + def get_documents(self, project_id="", topic_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/documents", + { + "project_id": project_id, + "topic_id": topic_id, + }, + ) + + def create_document( + self, + project_id="", + topic_id="", + guid=None, + files=None, + data=None, + ): + headers = { + "Authorization": "Bearer " + self.foundation_client.get_access_token(), + "Content-type": "application/octet-stream", + } + response = requests.post( + f"/projects/{project_id}/topics/{topic_id}/documents", + data=data, + params={guid}, + files=files, + headers=headers, + ) + return response.status_code + + def get_document(self, project_id="", topic_id="", document_id="") -> str: + headers = { + "Authorization": "Bearer " + self.foundation_client.get_access_token(), + "Content-type": "application/octet-stream", + } + response = requests.get( + f"{self.baseurl}/projects/{project_id}/topics/documents/{document_id}", + headers=headers, + ) + with open(os.path.join(self.filepath, f"{project_id}_{topic_id}_{document_id}_document.txt"), "wb") as f: + f.write(response.content.decode("utf-8")) + return response.status_code, response.content + + def get_topics_events(self, project_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/events", + { + "project_id": project_id, + }, + ) + + def get_topic_events(self, project_id="", topic_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/events", + { + "project_id": project_id, + "topic_id": topic_id, + }, + ) + + def get_comments_events(self, project_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/comments/events", + { + "project_id": project_id, + }, + ) + + def get_comment_events(self, project_id="", topic_id="", comment_id="") -> list: + return self.get( + f"/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events", + { + "project_id": project_id, + "topic_id": topic_id, + "comment_id": comment_id, + }, + ) diff --git a/src/bcf/bcf/v3/bcfxml.py b/src/bcf/src/bcf/v3/bcfxml.py similarity index 97% rename from src/bcf/bcf/v3/bcfxml.py rename to src/bcf/src/bcf/v3/bcfxml.py index 3b3fcc8466..9d9578e576 100644 --- a/src/bcf/bcf/v3/bcfxml.py +++ b/src/bcf/src/bcf/v3/bcfxml.py @@ -1,3 +1,22 @@ + +# BCF - BCF Python library +# Copyright (C) 2021 Prabhat Singh +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . + import os import uuid import shutil @@ -151,10 +170,10 @@ class BcfXml: if value in data["Topic"]: setattr(topic, key, data["Topic"][value]) - if "ReferenceLinks" in data["Topic"]: + if data["Topic"].get("ReferenceLinks"): topic.reference_links.extend(data["Topic"]["ReferenceLinks"].get("ReferenceLink", [])) - if "Labels" in data["Topic"]: + if data["Topic"].get("Labels"): topic.labels.extend(data["Topic"]["Labels"].get("Label", [])) if "BimSnippet" in data["Topic"]: @@ -189,7 +208,7 @@ class BcfXml: related_topic = bcf.v3.data.RelatedTopic() related_topic.guid = item["@Guid"] topic.related_topics.append(related_topic) - return topic + return topic def add_topic(self, topic=None): if topic is None: diff --git a/src/bcf/bcf/v3/data.py b/src/bcf/src/bcf/v3/data.py similarity index 84% rename from src/bcf/bcf/v3/data.py rename to src/bcf/src/bcf/v3/data.py index b6f241e148..a166a3b1b2 100644 --- a/src/bcf/bcf/v3/data.py +++ b/src/bcf/src/bcf/v3/data.py @@ -1,3 +1,22 @@ + +# BCF - BCF Python library +# Copyright (C) 2021 Prabhat Singh +# +# This file is part of BCF. +# +# BCF is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BCF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BCF. If not, see . + class Project: def __init__(self): self.project_id = "" diff --git a/src/bcf/bcf/v3/xsd/documents.xsd b/src/bcf/src/bcf/v3/xsd/documents.xsd similarity index 100% rename from src/bcf/bcf/v3/xsd/documents.xsd rename to src/bcf/src/bcf/v3/xsd/documents.xsd diff --git a/src/bcf/bcf/v3/xsd/extensions.xsd b/src/bcf/src/bcf/v3/xsd/extensions.xsd similarity index 100% rename from src/bcf/bcf/v3/xsd/extensions.xsd rename to src/bcf/src/bcf/v3/xsd/extensions.xsd diff --git a/src/bcf/bcf/v3/xsd/markup.xsd b/src/bcf/src/bcf/v3/xsd/markup.xsd similarity index 100% rename from src/bcf/bcf/v3/xsd/markup.xsd rename to src/bcf/src/bcf/v3/xsd/markup.xsd diff --git a/src/bcf/bcf/v3/xsd/project.xsd b/src/bcf/src/bcf/v3/xsd/project.xsd similarity index 100% rename from src/bcf/bcf/v3/xsd/project.xsd rename to src/bcf/src/bcf/v3/xsd/project.xsd diff --git a/src/bcf/bcf/v3/xsd/shared-types.xsd b/src/bcf/src/bcf/v3/xsd/shared-types.xsd similarity index 100% rename from src/bcf/bcf/v3/xsd/shared-types.xsd rename to src/bcf/src/bcf/v3/xsd/shared-types.xsd diff --git a/src/bcf/bcf/v3/xsd/version.xsd b/src/bcf/src/bcf/v3/xsd/version.xsd similarity index 100% rename from src/bcf/bcf/v3/xsd/version.xsd rename to src/bcf/src/bcf/v3/xsd/version.xsd diff --git a/src/bcf/bcf/v3/xsd/visinfo.xsd b/src/bcf/src/bcf/v3/xsd/visinfo.xsd similarity index 100% rename from src/bcf/bcf/v3/xsd/visinfo.xsd rename to src/bcf/src/bcf/v3/xsd/visinfo.xsd diff --git a/src/blenderbim/COPYING b/src/blenderbim/COPYING new file mode 100644 index 0000000000..810fce6e9b --- /dev/null +++ b/src/blenderbim/COPYING @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 70af090dda..f331aec6d0 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -1,6 +1,87 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + VERSION:=`date '+%y%m%d'` PYVERSION:=py37 +ifeq ($(PYVERSION), py37) +PYLIBDIR:=python3.7 +PYNUMBER:=37 +endif +ifeq ($(PYVERSION), py39) +PYLIBDIR:=python3.9 +PYNUMBER:=39 +endif + +ifeq ($(PLATFORM), linux) +ifeq ($(PYVERSION), py37) +HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py37h5f1835d_0.tar.bz2 +EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py37h95e2c48_0.tar.bz2 +BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py37h0379df6_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/30/c0/d0526314971fc661b083ab135747dc68446a3022686da8c16d25fcf6ef07/lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl +endif +ifeq ($(PYVERSION), py39) +HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/linux-64/hpp-fcl-1.7.5-py39hbcdfc36_0.tar.bz2 +EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/linux-64/eigenpy-2.6.5-py39h5aed9d1_0.tar.bz2 +BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/linux-64/boost-1.74.0-py39h5472131_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/19/d9/a69c6aff5673554df48120565a14a50eaa41d29ae03b02faa0b023666318/lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl +endif +ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/linux-64/assimp-5.0.1-hedfc422_6.tar.bz2 +OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/linux-64/octomap-1.9.7-h4bd325d_0.tar.bz2 +ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/linux-64/zlib-1.2.11-h516909a_1010.tar.bz2 +endif + +ifeq ($(PLATFORM), macos) +ifeq ($(PYVERSION), py37) +HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py37h2d7f23a_0.tar.bz2 +EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py37h0695097_0.tar.bz2 +BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py37hd79e0ac_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/1e/3e/f0abc15d5dac50939bccc589aae336d5ead4c72e7ad1039a2e0f3630ea92/lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl +endif +ifeq ($(PYVERSION), py39) +HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/osx-64/hpp-fcl-1.7.5-py39h1e32b98_0.tar.bz2 +EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/osx-64/eigenpy-2.6.5-py39h5405915_0.tar.bz2 +BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/osx-64/boost-1.74.0-py39ha641261_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/b8/74/a71f7ad72e8db54ce899efab84507b801660750cbbfa6a39e6717557d36a/lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl +endif +ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/osx-64/assimp-5.0.1-h1224e73_6.tar.bz2 +OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/osx-64/octomap-1.9.7-h940c156_0.tar.bz2 +ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/osx-64/zlib-1.2.11-h7795811_1010.tar.bz2 +endif + +ifeq ($(PLATFORM), win) +ifeq ($(PYVERSION), py37) +HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py37h839d6b1_0.tar.bz2 +EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py37h2c32e34_0.tar.bz2 +BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py37h3b38789_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/9e/5e/171ee9d40a600f565fe691ec5bf7596247ec62cfb2edc00c91afe8ea837b/lxml-4.6.3-cp37-cp37m-win_amd64.whl +endif +ifeq ($(PYVERSION), py39) +HPPFCL_URL:=https://anaconda.org/conda-forge/hpp-fcl/1.7.5/download/win-64/hpp-fcl-1.7.5-py39h2e7c763_0.tar.bz2 +EIGENPY_URL:=https://anaconda.org/conda-forge/eigenpy/2.6.5/download/win-64/eigenpy-2.6.5-py39h3ce40e6_0.tar.bz2 +BOOST_URL:=https://anaconda.org/conda-forge/boost/1.74.0/download/win-64/boost-1.74.0-py39hefe7e4c_3.tar.bz2 +LXML_URL:=https://files.pythonhosted.org/packages/72/d4/426ecb8849c47c3e370c87aa0ac05d85768df917ffea27fcd6686a5e6495/lxml-4.6.3-cp39-cp39-win_amd64.whl +endif +ASSIMP_URL:=https://anaconda.org/conda-forge/assimp/5.0.1/download/win-64/assimp-5.0.1-hc2aa0de_6.tar.bz2 +OCTOMAP_URL:=https://anaconda.org/conda-forge/octomap/1.9.7/download/win-64/octomap-1.9.7-h5362a0b_0.tar.bz2 +ZLIB_URL:=https://anaconda.org/conda-forge/zlib/1.2.11/download/win-64/zlib-1.2.11-h62dcd97_1010.tar.bz2 +endif + .PHONY: dist dist: ifndef PLATFORM @@ -12,12 +93,7 @@ endif cp -r blenderbim/* dist/blenderbim/ # Provides IfcOpenShell Python functionality -ifeq ($(PYVERSION), py37) - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-2fd2b49-$(PLATFORM)64.zip -endif -ifeq ($(PYVERSION), py39) - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-2fd2b49-$(PLATFORM)64.zip -endif + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-$(PYNUMBER)-v0.6.0-721fe47-$(PLATFORM)64.zip cd dist/working && unzip ifcblender* cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/ @@ -44,10 +120,12 @@ endif # IfcOpenBot sometimes lags behind, so we hotfix the Python utilities cp -r dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/util/* dist/blenderbim/libs/site/packages/ifcopenshell/util/ cp -r dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/api/* dist/blenderbim/libs/site/packages/ifcopenshell/api/ + cp dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/entity_instance.py dist/blenderbim/libs/site/packages/ifcopenshell/ + cp dist/working/IfcOpenShell-0.6.0/src/ifcopenshell-python/ifcopenshell/file.py dist/blenderbim/libs/site/packages/ifcopenshell/ # Provides bcf functionality - cp -r dist/working/IfcOpenShell-0.6.0/src/bcf/bcf dist/blenderbim/libs/site/packages/ + cp -r dist/working/IfcOpenShell-0.6.0/src/bcf/src/bcf dist/blenderbim/libs/site/packages/ # Provides IFCClash functionality - cp -r dist/working/IfcOpenShell-0.6.0/src/ifcclash/* dist/blenderbim/libs/site/packages/ + cp -r dist/working/IfcOpenShell-0.6.0/src/ifcclash/ifcclash dist/blenderbim/libs/site/packages/ # Provides BIMTester functionality cd dist/working && python -m venv env cd dist/working && . env/bin/activate && pip install pybabel @@ -62,8 +140,10 @@ endif cp -r dist/working/IfcOpenShell-0.6.0/src/ifccsv/* dist/blenderbim/libs/site/packages/ # Provides IFCPatch functionality cp -r dist/working/IfcOpenShell-0.6.0/src/ifcpatch/ifcpatch dist/blenderbim/libs/site/packages/ - # Provides IFCP6 functionality - cp -r dist/working/IfcOpenShell-0.6.0/src/ifcp6/ifcp6 dist/blenderbim/libs/site/packages/ + # Provides IFC4D functionality + cp -r dist/working/IfcOpenShell-0.6.0/src/ifc4d/ifc4d dist/blenderbim/libs/site/packages/ + # Provides IFC5D functionality + cp -r dist/working/IfcOpenShell-0.6.0/src/ifc5d/ifc5d dist/blenderbim/libs/site/packages/ rm -rf dist/working # Provides Mustache templating in construction documentation @@ -172,20 +252,97 @@ endif rm -rf dist/working # Required by IFCClash -ifeq ($(PLATFORM), linux) mkdir dist/working - cd dist/working && wget https://files.pythonhosted.org/packages/0c/fa/00d85f893b02289e2942849d97f8818dde8e2111182e825fb3a735677791/python_fcl-0.0.12-cp37-cp37m-manylinux1_x86_64.whl - cd dist/working && unzip python_fcl* - cp -r dist/working/fcl dist/blenderbim/libs/site/packages/ - rm -rf dist/working + cd dist/working && wget $(HPPFCL_URL) + cd dist/working && tar -xf hpp-fcl* +ifeq ($(PLATFORM), linux) + cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/ + cp -r dist/working/lib/*.so* dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), macos) + cp -r dist/working/lib/$(PYLIBDIR)/site-packages/hppfcl dist/blenderbim/libs/site/packages/ + cp -r dist/working/lib/*.dylib dist/blenderbim/libs/ endif ifeq ($(PLATFORM), win) - mkdir dist/working - cd dist/working && wget https://files.pythonhosted.org/packages/19/e6/6e9f33fb59e8f27c0e1592bd26e644bc92b85c942b072b2d3854105d5887/python_fcl_win32-0.0.12.post3-py3-none-win_amd64.whl - cd dist/working && unzip python_fcl* - cp -r dist/working/fcl dist/blenderbim/libs/site/packages/ - rm -rf dist/working + cp -r dist/working/Lib/site-packages/hppfcl dist/blenderbim/libs/site/packages/ + cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/ endif + rm -rf dist/working + + # Required by hpp-fcl + mkdir dist/working + cd dist/working && wget $(ASSIMP_URL) + cd dist/working && tar -xf assimp* +ifeq ($(PLATFORM), linux) + cp -r dist/working/lib/*.so* dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), macos) + cp -r dist/working/lib/*.dylib dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), win) + cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/ +endif + rm -rf dist/working + + # Required by hpp-fcl + mkdir dist/working + cd dist/working && wget $(EIGENPY_URL) + cd dist/working && tar -xf eigenpy* +ifeq ($(PLATFORM), linux) + cp -r dist/working/lib/*.so* dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), macos) + cp -r dist/working/lib/*.dylib dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), win) + cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/ +endif + rm -rf dist/working + + # Required by hpp-fcl + mkdir dist/working + cd dist/working && wget $(OCTOMAP_URL) + cd dist/working && tar -xf octomap* +ifeq ($(PLATFORM), linux) + cp -r dist/working/lib/*.so* dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), macos) + cp -r dist/working/lib/*.dylib dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), win) + # Uh, do nothing, apparently? No DLLs are shipped. +endif + rm -rf dist/working + + # Required by hpp-fcl + mkdir dist/working + cd dist/working && wget $(BOOST_URL) + cd dist/working && tar -xf boost* +ifeq ($(PLATFORM), linux) + cp -r dist/working/lib/*.so* dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), macos) + cp -r dist/working/lib/*.dylib dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), win) + cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/ +endif + rm -rf dist/working + + # Required by hpp-fcl + mkdir dist/working + cd dist/working && wget $(ZLIB_URL) + cd dist/working && tar -xf zlib* +ifeq ($(PLATFORM), linux) + cp -r dist/working/lib/*.so* dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), macos) + cp -r dist/working/lib/*.dylib dist/blenderbim/libs/ +endif +ifeq ($(PLATFORM), win) + cp -r dist/working/Library/bin/*.dll dist/blenderbim/libs/site/packages/hppfcl/ +endif + rm -rf dist/working # Required by BIMTester mkdir dist/working @@ -199,6 +356,13 @@ endif cd dist/working/ && patch ../blenderbim/libs/site/packages/behave/runner_util.py < runner_util.patch rm -rf dist/working + # Required by ids + mkdir dist/working + cd dist/working && wget $(LXML_URL) + cd dist/working && cp *.whl lxml.zip && unzip lxml.zip + cp -r dist/working/lxml dist/blenderbim/libs/site/packages/ + rm -rf dist/working + # Required by behave mkdir dist/working cd dist/working && wget https://files.pythonhosted.org/packages/f4/65/220bb4075fddb09d5b3ea2c1c1fa66c1c72be9361ec187aab50fa161e576/parse-1.15.0.tar.gz @@ -269,10 +433,50 @@ endif cp -r dist/working/IFC2JSON_python-master/file_converters/ifcjson dist/blenderbim/libs/site/packages/ rm -rf dist/working + # Provides OpenLCA fucntionality + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/85/33/f91b96e9e8608ff65a003b692e8a9cdd60f2178f60617e5b1d21334c009c/olca-ipc-0.0.10.tar.gz + cd dist/working && tar -xzvf olca-ipc* + cd dist/working/olca-ipc-0.0.10/ && cp -r olca ../../blenderbim/libs/site/packages/ + rm -rf dist/working + cd dist/blenderbim && sed -i "s/999999/$(VERSION)/" __init__.py - cd dist && zip -r blender28-bim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./* + cd dist && zip -r blenderbim-$(VERSION)-$(PYVERSION)-$(PLATFORM).zip ./* rm -rf dist/blenderbim +.PHONY: test +test: + make test-core + make test-bim + +.PHONY: test-core +test-core: + pytest -p no:pytest-blender test/core + +.PHONY: test-bim +test-bim: + pytest test/bim + +.PHONY: qa +qa: + black . + pylint ./* --output-format=colorized --disable all --enable E --disable import-error + +.PHONY: coverage +coverage: + coverage run --source blenderbim.core -m pytest -p no:pytest-blender test/core + coverage html + xdg-open htmlcov/index.html + +.PHONY: license +license: + copyright-header --license GPL3 --copyright-holder "Dion Moult " --copyright-year "2021" --copyright-software "BlenderBIM Add-on" --copyright-software-description "OpenBIM Blender Add-on" -a ./ -o ./ + .PHONY: clean clean: rm -rf dist + rm -rf htmlcov + +.PHONY: dev +dev: + blender -p setup_pytest.py diff --git a/src/blenderbim/RenameObjects.py b/src/blenderbim/RenameObjects.py deleted file mode 100644 index f3e38daafc..0000000000 --- a/src/blenderbim/RenameObjects.py +++ /dev/null @@ -1,119 +0,0 @@ -import bpy -from bpy.types import Operator - - -class Object_OT_RenameObjects(Operator): - bl_idname = "object.renameobjects" - bl_label = "Rename Object(s)" - - # Multi Object rename UI - BNameCB: bpy.props.BoolProperty(name="Base Name:") - BaseName: bpy.props.StringProperty(name="") - PreFixCB: bpy.props.BoolProperty(name="Prefix:") - PreFix: bpy.props.StringProperty(name="") - RemFrst: bpy.props.BoolProperty(name="Remove First") - DgtFrst: bpy.props.IntProperty(name="Digits") - SuffixCB: bpy.props.BoolProperty(name="Suffix") - Suffix: bpy.props.StringProperty(name="") - RemLast: bpy.props.BoolProperty(name="Remove Last") - DgtLast: bpy.props.IntProperty(name="Digits") - NumbredCB: bpy.props.BoolProperty(name="Numbred") - BaseNum: bpy.props.IntProperty(name="Base Number") - Step: bpy.props.IntProperty(name="Step", default=1) - findCB: bpy.props.BoolProperty(name="Replace") - find: bpy.props.StringProperty(name="") - replace: bpy.props.StringProperty(name="") - # Single rename UI - Name: bpy.props.StringProperty(name="Name") - - def draw(self, ctx): - SelCount = len(bpy.context.selected_objects) - if SelCount > 1: - box = self.layout.box() - row = box.row() - row.prop(self, "BNameCB") - row.prop(self, "BaseName") - row = box.row() - row.prop(self, "PreFixCB") - row.prop(self, "PreFix") - row = box.row() - row.prop(self, "RemFrst") - row.prop(self, "DgtFrst") - row = box.row() - row.prop(self, "SuffixCB") - row.prop(self, "Suffix") - row = box.row() - row.prop(self, "RemLast") - row.prop(self, "DgtLast") - row = box.row(align=True) - row.prop(self, "NumbredCB") - row.prop(self, "BaseNum") - row.prop(self, "Step") - row = box.row(align=True) - row.prop(self, "findCB") - row.prop(self, "find") - row.prop(self, "replace") - if SelCount == 1: - box = self.layout.box() - row = box.row() - row.prop(self, "Name") - if SelCount == 0: - box = self.layout.box() - row = box.row() - row.label("No Selected Object") - - def __init__(self): - if len(bpy.context.selected_objects) == 1: - self.Name = bpy.context.selected_objects[0].name - - def execute(self, context): - SelCount = len(bpy.context.selected_objects) - if SelCount > 1: - SelObj = bpy.context.selected_objects - Index = self.BaseNum - for i in range(0, SelCount): - # Get Object Original Name # - NewName = SelObj[i].name - # Set the Base name # - if self.BNameCB: - NewName = self.BaseName - # Remove First characters # - if self.RemFrst: - NewName = NewName[self.DgtFrst : self.DgtFrst + len(NewName)] - # Remove Last Characters # - if self.RemLast: - NewName = NewName[1 : len(NewName) - self.DgtLast] - # Add Prefix to the new name # - if self.PreFixCB: - NewName = self.PreFix + NewName - # Add Suffix to the new name # - if self.SuffixCB: - NewName = NewName + self.Suffix - # Add Digits to end of new name # - if self.NumbredCB: - NewName += str(Index) - Index += self.Step - # Find and Replace # - if self.findCB: - NewName = NewName.replace(self.find, self.replace) - # Set the new name to the object # - SelObj[i].name = NewName - elif SelCount == 1: - bpy.context.selected_objects[0].name = self.Name - return {"FINISHED"} - - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_props_dialog(self) - - -def register(): - bpy.utils.register_class(Object_OT_RenameObjects) - - -def unregister(): - bpy.utils.unregister_class(Object_OT_RenameObjects) - - -if __name__ == "__main__": - register() diff --git a/src/blenderbim/blenderbim/__init__.py b/src/blenderbim/blenderbim/__init__.py index 3251fdd873..728b563351 100644 --- a/src/blenderbim/blenderbim/__init__.py +++ b/src/blenderbim/blenderbim/__init__.py @@ -1,7 +1,29 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import os +import sys +import site + bl_info = { "name": "BlenderBIM", - "description": "Author, import, and export files in the " "Industry Foundation Classes (.ifc) file format", - "author": "Dion Moult, IfcOpenShell", + "description": "Author, import, and export data using the Industry Foundation Classes schema", + "author": "IfcOpenShell Contributors", "blender": (2, 80, 0), "version": (0, 0, 999999), "location": "File > Export, File > Import, Scene / Object / Material / Mesh Properties", @@ -9,25 +31,15 @@ bl_info = { "category": "Import-Export", } -import os -import site +if sys.modules.get("bpy", None): + # Process *.pth in /libs/site/packages to setup globally importable modules + # This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda + site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages")) + import blenderbim.bim -# process *.pth in /libs/site/packages to setup globally importable modules -# 3 levels deep required by occ static ../../ path -# TODO: 3 levels deep is no longer required as we no longer bundle OCC -cwd = os.path.dirname(os.path.realpath(__file__)) -site.addsitedir(os.path.join(cwd, "libs", "site", "packages")) + def register(): + blenderbim.bim.register() - -# main import -from .bim import * - - -# Explicitely expose bim.xx when imported with from blenderbim import * -# Other bim still are importable using explicit from blenderbim.bim import xxx -__all__ = ["export_ifc", "import_ifc"] - - -if __name__ == "__main__": - register() + def unregister(): + blenderbim.bim.unregister() diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 952e46b1a7..8ecb59a8ee 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -1,156 +1,167 @@ -# Check if we are running in Blender before loading, to allow for multiprocessing -import sys +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . -bpy = sys.modules.get("bpy") +import bpy +import importlib +from . import handler, ui, prop, operator -if bpy is not None: - import bpy - import importlib - from . import handler, ui, prop, operator +modules = { + "project": None, + "search": None, + "bcf": None, + "root": None, + "unit": None, + "model": None, + "georeference": None, + "context": None, + "drawing": None, + "attribute": None, + "type": None, + "spatial": None, + "void": None, + "aggregate": None, + "geometry": None, + "cobie": None, + "resource": None, + "cost": None, + "sequence": None, + "group": None, + "system": None, + "structural": None, + "boundary": None, + "profile": None, + "material": None, + "style": None, + "layer": None, + "owner": None, + "pset": None, + "qto": None, + "classification": None, + "constraint": None, + "document": None, + "pset_template": None, + "clash": None, + "lca": None, + "csv": None, + "bimtester": None, + "diff": None, + "patch": None, + "covetool": None, + "augin": None, + "debug": None, +} - modules = { - "project": None, - "parametric": None, - "search": None, - "bcf": None, - "root": None, - "unit": None, - "model": None, - "georeference": None, - "context": None, - "drawing": None, - "attribute": None, - "type": None, - "spatial": None, - "void": None, - "aggregate": None, - "geometry": None, - "cobie": None, - "resource": None, - "cost": None, - "sequence": None, - "group": None, - "structural": None, - "boundary": None, - "material": None, - "style": None, - "layer": None, - "owner": None, - "pset": None, - "qto": None, - "classification": None, - "constraint": None, - "document": None, - "pset_template": None, - "clash": None, - "csv": None, - "bimtester": None, - "diff": None, - "patch": None, - "covetool": None, - "augin": None, - "debug": None, - } +for name in modules.keys(): + modules[name] = importlib.import_module(f"blenderbim.bim.module.{name}") - for name in modules.keys(): - modules[name] = importlib.import_module(f"blenderbim.bim.module.{name}") +classes = [ + operator.OpenUri, + operator.SelectDataDir, + operator.SelectSchemaDir, + operator.SelectIfcFile, + operator.ExportIFC, + operator.ImportIFC, + operator.OpenUpstream, + operator.AddSectionPlane, + operator.RemoveSectionPlane, + operator.ReloadIfcFile, + operator.AddIfcFile, + operator.RemoveIfcFile, + operator.SetOverrideColour, + operator.SetViewportShadowFromSun, + operator.SnapSpacesTogether, + operator.OverrideDelete, + prop.StrProperty, + prop.Attribute, + prop.BIMProperties, + prop.IfcParameter, + prop.PsetQto, + prop.GlobalId, + prop.BIMObjectProperties, + prop.BIMMaterialProperties, + prop.BIMMeshProperties, + ui.BIM_PT_section_plane, + ui.BIM_UL_generic, + ui.BIM_UL_topics, + ui.BIM_ADDON_preferences, +] - classes = [ - operator.OpenUri, - operator.SelectDataDir, - operator.SelectSchemaDir, - operator.SelectIfcFile, - operator.ExportIFC, - operator.ImportIFC, - operator.SelectExternalMaterialDir, - operator.FetchExternalMaterial, - operator.FetchObjectPassport, - operator.OpenUpstream, - operator.AddSectionPlane, - operator.RemoveSectionPlane, - operator.ReloadIfcFile, - operator.AddIfcFile, - operator.RemoveIfcFile, - operator.SetOverrideColour, - operator.SetViewportShadowFromSun, - operator.CopyPropertyToSelection, - operator.CopyAttributeToSelection, - operator.LinkIfc, - operator.SnapSpacesTogether, - prop.StrProperty, - prop.Attribute, - prop.BIMProperties, - prop.IfcParameter, - prop.PsetQto, - prop.GlobalId, - prop.BIMObjectProperties, - prop.BIMMaterialProperties, - prop.ItemSlotMap, - prop.BIMMeshProperties, - ui.BIM_PT_section_plane, - ui.BIM_UL_generic, - ui.BIM_UL_topics, - ui.BIM_ADDON_preferences, - ] +for mod in modules.values(): + classes.extend(mod.classes) - for module in modules.values(): - classes.extend(module.classes) - def menu_func_export(self, context): - self.layout.operator(operator.ExportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcjson)") +def menu_func_export(self, context): + self.layout.operator(operator.ExportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcjson)") - def menu_func_import(self, context): - self.layout.operator(operator.ImportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcxml)") - def on_register(scene): - handler.setDefaultProperties(scene) - bpy.app.handlers.depsgraph_update_post.remove(on_register) +def menu_func_import(self, context): + self.layout.operator(operator.ImportIFC.bl_idname, text="Industry Foundation Classes (.ifc/.ifczip/.ifcxml)") - def register(): - for cls in classes: - bpy.utils.register_class(cls) - bpy.app.handlers.depsgraph_update_post.append(on_register) - bpy.app.handlers.undo_post.append(handler.undo_post) - bpy.app.handlers.redo_post.append(handler.redo_post) - bpy.app.handlers.load_post.append(handler.setDefaultProperties) - bpy.app.handlers.load_post.append(handler.loadIfcStore) - bpy.app.handlers.save_pre.append(handler.ensureIfcExported) - bpy.types.TOPBAR_MT_file_export.append(menu_func_export) - bpy.types.TOPBAR_MT_file_import.append(menu_func_import) - bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) - bpy.types.Object.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties) - bpy.types.Material.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties) - bpy.types.Collection.BIMObjectProperties = bpy.props.PointerProperty( - type=prop.BIMObjectProperties - ) # Check if we need this - bpy.types.Material.BIMMaterialProperties = bpy.props.PointerProperty(type=prop.BIMMaterialProperties) - bpy.types.Mesh.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) - bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) - bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) - bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) - bpy.types.SCENE_PT_unit.append(ui.ifc_units) - for module in modules.values(): - module.register() +def on_register(scene): + handler.setDefaultProperties(scene) + bpy.app.handlers.depsgraph_update_post.remove(on_register) - def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) - bpy.app.handlers.load_post.remove(handler.setDefaultProperties) - bpy.app.handlers.load_post.remove(handler.loadIfcStore) - bpy.app.handlers.save_pre.remove(handler.ensureIfcExported) - bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) - bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) - del bpy.types.Scene.BIMProperties - del bpy.types.Object.BIMObjectProperties - del bpy.types.Material.BIMObjectProperties - del bpy.types.Collection.BIMObjectProperties # Check if we need this - del bpy.types.Material.BIMMaterialProperties - del bpy.types.Mesh.BIMMeshProperties - del bpy.types.Curve.BIMMeshProperties - del bpy.types.Camera.BIMMeshProperties - del bpy.types.PointLight.BIMMeshProperties - bpy.types.SCENE_PT_unit.remove(ui.ifc_units) - for module in reversed(list(modules.values())): - module.unregister() +def register(): + for cls in classes: + bpy.utils.register_class(cls) + bpy.app.handlers.depsgraph_update_post.append(on_register) + bpy.app.handlers.undo_pre.append(handler.undo_pre) + bpy.app.handlers.undo_post.append(handler.undo_post) + bpy.app.handlers.redo_pre.append(handler.redo_pre) + bpy.app.handlers.redo_post.append(handler.redo_post) + bpy.app.handlers.load_post.append(handler.setDefaultProperties) + bpy.app.handlers.load_post.append(handler.loadIfcStore) + bpy.app.handlers.save_pre.append(handler.ensureIfcExported) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + bpy.types.Scene.BIMProperties = bpy.props.PointerProperty(type=prop.BIMProperties) + bpy.types.Object.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties) + bpy.types.Material.BIMObjectProperties = bpy.props.PointerProperty(type=prop.BIMObjectProperties) + bpy.types.Material.BIMMaterialProperties = bpy.props.PointerProperty(type=prop.BIMMaterialProperties) + bpy.types.Mesh.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) + bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) + bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) + bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) + bpy.types.SCENE_PT_unit.append(ui.ifc_units) + + for mod in modules.values(): + mod.register() + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + bpy.app.handlers.load_post.remove(handler.setDefaultProperties) + bpy.app.handlers.load_post.remove(handler.loadIfcStore) + bpy.app.handlers.save_pre.remove(handler.ensureIfcExported) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + del bpy.types.Scene.BIMProperties + del bpy.types.Object.BIMObjectProperties + del bpy.types.Material.BIMObjectProperties + del bpy.types.Material.BIMMaterialProperties + del bpy.types.Mesh.BIMMeshProperties + del bpy.types.Curve.BIMMeshProperties + del bpy.types.Camera.BIMMeshProperties + del bpy.types.PointLight.BIMMeshProperties + bpy.types.SCENE_PT_unit.remove(ui.ifc_units) + + for mod in reversed(list(modules.values())): + mod.unregister() diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index dd1dd54d03..7a115d0ea9 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import json @@ -5,12 +23,11 @@ import numpy as np import datetime import zipfile import tempfile -import ifcopenshell -import ifcopenshell.util.placement -import ifcopenshell.api -from ifcopenshell.api.spatial.data import Data as SpatialData -from blenderbim.bim.ifc import IfcStore import addon_utils +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.placement +from blenderbim.bim.ifc import IfcStore class IfcExporter: @@ -68,6 +85,8 @@ class IfcExporter: for ifc_definition_id, obj in IfcStore.id_map.items(): try: + if isinstance(obj, bpy.types.Material): + continue self.sync_object_placement(obj) self.sync_object_container(ifc_definition_id, obj) except ReferenceError: @@ -75,8 +94,6 @@ class IfcExporter: if self.should_delete(obj): to_delete.append(ifc_definition_id) - SpatialData.purge() - for ifc_definition_id in to_delete: product = self.file.by_id(ifc_definition_id) IfcStore.unlink_element(product) @@ -87,13 +104,16 @@ class IfcExporter: if not obj: continue try: - bpy.ops.bim.update_representation(obj=obj.name) + if isinstance(obj, bpy.types.Material): + bpy.ops.bim.update_style_colours(material=obj.name) + else: + bpy.ops.bim.update_representation(obj=obj.name) except ReferenceError: pass # The object is likely deleted IfcStore.edited_objs.clear() def sync_object_placement(self, obj): - blender_matrix = np.matrix(obj.matrix_world) + blender_matrix = np.array(obj.matrix_world) element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) if not hasattr(element, "ObjectPlacement"): return @@ -103,7 +123,7 @@ class IfcExporter: ifc_matrix[2][3] *= self.unit_scale props = bpy.context.scene.BIMGeoreferenceProperties - if props.has_blender_offset and props.blender_offset_type == "OBJECT_PLACEMENT": + if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "OBJECT_PLACEMENT": ifc_matrix = ifcopenshell.util.geolocation.global2local( ifc_matrix, float(props.blender_eastings) * self.unit_scale, @@ -137,7 +157,6 @@ class IfcExporter: else: parent_collection = obj.users_collection[0] - parent_obj = bpy.data.objects.get(parent_collection.name) if not parent_obj or not parent_obj.BIMObjectProperties.ifc_definition_id: return @@ -153,6 +172,7 @@ class IfcExporter: try: # This will throw an exception if the Blender object no longer exists foo = obj.name + foo return False except: return True @@ -180,7 +200,6 @@ class IfcExportSettings: @staticmethod def factory(context, output_file, logger): - scene_bim = context.scene.BIMProperties settings = IfcExportSettings() settings.output_file = output_file settings.logger = logger diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index 32723669e0..cc249506fd 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -1,10 +1,32 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import addon_utils import ifcopenshell.api.owner.settings +from blenderbim.bim.module.drawing.prop import RasterStyleProperty from bpy.app.handlers import persistent from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.owner.prop import getPersons, getOrganisations from ifcopenshell.api.attribute.data import Data as AttributeData +from ifcopenshell.api.material.data import Data as MaterialData +from ifcopenshell.api.style.data import Data as StyleData from ifcopenshell.api.type.data import Data as TypeData @@ -12,6 +34,8 @@ global_subscription_owner = object() def mode_callback(obj, data): + if not bpy.context.scene.BIMProjectProperties.is_authoring: + return objects = bpy.context.selected_objects if bpy.context.active_object: objects += [bpy.context.active_object] @@ -21,9 +45,8 @@ def mode_callback(obj, data): or not obj.data or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve)) or not obj.BIMObjectProperties.ifc_definition_id - or not bpy.context.scene.BIMProjectProperties.is_authoring ): - return + continue if obj.data.BIMMeshProperties.ifc_definition_id: representation = IfcStore.get_file().by_id(obj.data.BIMMeshProperties.ifc_definition_id) if representation.RepresentationType in ["Tessellation", "Brep", "Annotation2D"]: @@ -33,12 +56,26 @@ def mode_callback(obj, data): def name_callback(obj, data): + # TODO Do we still need this, now that we are monitoring the undo redo objects? try: - oby.type + obj.name except: return # In case the object RNA is gone during an undo / redo operation - # Blender material names are up to 63 UTF-8 bytes - if not obj.BIMObjectProperties.ifc_definition_id or "/" not in obj.name or len(bytes(obj.name, "utf-8")) >= 63: + # Blender names are up to 63 UTF-8 bytes + if len(bytes(obj.name, "utf-8")) >= 63: + return + + if isinstance(obj, bpy.types.Material): + if obj.BIMObjectProperties.ifc_definition_id: + IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id).Name = obj.name + AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + MaterialData.load_materials() + if obj.BIMMaterialProperties.ifc_style_id: + IfcStore.get_file().by_id(obj.BIMMaterialProperties.ifc_style_id).Name = obj.name + StyleData.load(IfcStore.get_file(), obj.BIMMaterialProperties.ifc_style_id) + return + + if not obj.BIMObjectProperties.ifc_definition_id or "/" not in obj.name: return element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) if not element.is_a("IfcRoot"): @@ -62,6 +99,11 @@ def name_callback(obj, data): AttributeData.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) +def color_callback(obj, data): + if obj.BIMMaterialProperties.ifc_style_id: + IfcStore.edited_objs.add(obj) + + def active_object_callback(): obj = bpy.context.active_object for obj in bpy.context.selected_objects: @@ -73,7 +115,10 @@ def active_object_callback(): def subscribe_to(object, data_path, callback): - subscribe_to = object.path_resolve(data_path, False) + try: + subscribe_to = object.path_resolve(data_path, False) + except: + return bpy.msgbus.subscribe_rna( key=subscribe_to, owner=object, @@ -106,12 +151,16 @@ def purge_module_data(): @persistent def loadIfcStore(scene): IfcStore.purge() - ifc_file = IfcStore.get_file() - if not ifc_file: + purge_module_data() + if not IfcStore.get_file(): return IfcStore.get_schema() IfcStore.reload_linked_elements() - purge_module_data() + + +@persistent +def undo_pre(scene): + IfcStore.update_undo_redo_stack_objects() @persistent @@ -119,7 +168,14 @@ def undo_post(scene): if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction: IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction IfcStore.undo() - IfcStore.reload_linked_elements(should_reload_selected=True) + purge_module_data() + IfcStore.update_undo_redo_stack_objects() + IfcStore.reload_linked_elements(objects=[bpy.data.objects.get(o) for o in IfcStore.undo_redo_stack_objects]) + + +@persistent +def redo_pre(scene): + IfcStore.update_undo_redo_stack_objects() @persistent @@ -127,7 +183,9 @@ def redo_post(scene): if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction: IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction IfcStore.redo() - IfcStore.reload_linked_elements(should_reload_selected=True) + purge_module_data() + IfcStore.update_undo_redo_stack_objects() + IfcStore.reload_linked_elements(objects=[bpy.data.objects.get(o) for o in IfcStore.undo_redo_stack_objects]) @persistent @@ -172,12 +230,12 @@ def setDefaultProperties(scene): ) ifcopenshell.api.owner.settings.get_person = ( lambda ifc: ifc.by_id(int(bpy.context.scene.BIMOwnerProperties.user_person)) - if bpy.context.scene.BIMOwnerProperties.user_person + if getPersons(None, None) and bpy.context.scene.BIMOwnerProperties.user_person else None ) ifcopenshell.api.owner.settings.get_organisation = ( lambda ifc: ifc.by_id(int(bpy.context.scene.BIMOwnerProperties.user_organisation)) - if bpy.context.scene.BIMOwnerProperties.user_organisation + if getOrganisations(None, None) and bpy.context.scene.BIMOwnerProperties.user_organisation else None ) ifcopenshell.api.owner.settings.get_application = get_application @@ -187,30 +245,30 @@ def setDefaultProperties(scene): drawing_style.render_type = "VIEWPORT" drawing_style.raster_style = json.dumps( { - "bpy.data.worlds[0].color": (1, 1, 1), - "bpy.context.scene.render.engine": "BLENDER_WORKBENCH", - "bpy.context.scene.render.film_transparent": False, - "bpy.context.scene.display.shading.show_object_outline": True, - "bpy.context.scene.display.shading.show_cavity": False, - "bpy.context.scene.display.shading.cavity_type": "BOTH", - "bpy.context.scene.display.shading.curvature_ridge_factor": 1, - "bpy.context.scene.display.shading.curvature_valley_factor": 1, - "bpy.context.scene.view_settings.view_transform": "Standard", - "bpy.context.scene.display.shading.light": "FLAT", - "bpy.context.scene.display.shading.color_type": "SINGLE", - "bpy.context.scene.display.shading.single_color": (1, 1, 1), - "bpy.context.scene.display.shading.show_shadows": False, - "bpy.context.scene.display.shading.shadow_intensity": 0.5, - "bpy.context.scene.display.light_direction": (0.5, 0.5, 0.5), - "bpy.context.scene.view_settings.use_curve_mapping": False, - "space.overlay.show_wireframes": True, - "space.overlay.wireframe_threshold": 0, - "space.overlay.show_floor": False, - "space.overlay.show_axis_x": False, - "space.overlay.show_axis_y": False, - "space.overlay.show_axis_z": False, - "space.overlay.show_object_origins": False, - "space.overlay.show_relationship_lines": False, + RasterStyleProperty.WORLD_COLOR.value: (1, 1, 1), + RasterStyleProperty.RENDER_ENGINE.value: "BLENDER_WORKBENCH", + RasterStyleProperty.RENDER_TRANSPARENT.value: False, + RasterStyleProperty.SHADING_SHOW_OBJECT_OUTLINE.value: True, + RasterStyleProperty.SHADING_SHOW_CAVITY.value: False, + RasterStyleProperty.SHADING_CAVITY_TYPE.value: "BOTH", + RasterStyleProperty.SHADING_CURVATURE_RIDGE_FACTOR.value: 1, + RasterStyleProperty.SHADING_CURVATURE_VALLEY_FACTOR.value: 1, + RasterStyleProperty.VIEW_TRANSFORM.value: "Standard", + RasterStyleProperty.SHADING_LIGHT.value: "FLAT", + RasterStyleProperty.SHADING_COLOR_TYPE.value: "SINGLE", + RasterStyleProperty.SHADING_SINGLE_COLOR.value: (1, 1, 1), + RasterStyleProperty.SHADING_SHOW_SHADOWS.value: False, + RasterStyleProperty.SHADING_SHADOW_INTENSITY.value: 0.5, + RasterStyleProperty.DISPLAY_LIGHT_DIRECTION.value: (0.5, 0.5, 0.5), + RasterStyleProperty.VIEW_USE_CURVE_MAPPING.value: False, + RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: True, + RasterStyleProperty.OVERLAY_WIREFRAME_THRESHOLD.value: 0, + RasterStyleProperty.OVERLAY_SHOW_FLOOR.value: False, + RasterStyleProperty.OVERLAY_SHOW_AXIS_X.value: False, + RasterStyleProperty.OVERLAY_SHOW_AXIS_Y.value: False, + RasterStyleProperty.OVERLAY_SHOW_AXIS_Z.value: False, + RasterStyleProperty.OVERLAY_SHOW_OBJECT_ORIGINS.value: False, + RasterStyleProperty.OVERLAY_SHOW_RELATIONSHIP_LINES.value: False, } ) drawing_style = bpy.context.scene.DocProperties.drawing_styles.add() @@ -218,30 +276,30 @@ def setDefaultProperties(scene): drawing_style.render_type = "VIEWPORT" drawing_style.raster_style = json.dumps( { - "bpy.data.worlds[0].color": (1, 1, 1), - "bpy.context.scene.render.engine": "BLENDER_WORKBENCH", - "bpy.context.scene.render.film_transparent": False, - "bpy.context.scene.display.shading.show_object_outline": True, - "bpy.context.scene.display.shading.show_cavity": True, - "bpy.context.scene.display.shading.cavity_type": "BOTH", - "bpy.context.scene.display.shading.curvature_ridge_factor": 1, - "bpy.context.scene.display.shading.curvature_valley_factor": 1, - "bpy.context.scene.view_settings.view_transform": "Standard", - "bpy.context.scene.display.shading.light": "STUDIO", - "bpy.context.scene.display.shading.color_type": "MATERIAL", - "bpy.context.scene.display.shading.single_color": (1, 1, 1), - "bpy.context.scene.display.shading.show_shadows": True, - "bpy.context.scene.display.shading.shadow_intensity": 0.5, - "bpy.context.scene.display.light_direction": (0.5, 0.5, 0.5), - "bpy.context.scene.view_settings.use_curve_mapping": False, - "space.overlay.show_wireframes": True, - "space.overlay.wireframe_threshold": 0, - "space.overlay.show_floor": False, - "space.overlay.show_axis_x": False, - "space.overlay.show_axis_y": False, - "space.overlay.show_axis_z": False, - "space.overlay.show_object_origins": False, - "space.overlay.show_relationship_lines": False, + RasterStyleProperty.WORLD_COLOR.value: (1, 1, 1), + RasterStyleProperty.RENDER_ENGINE.value: "BLENDER_WORKBENCH", + RasterStyleProperty.RENDER_TRANSPARENT.value: False, + RasterStyleProperty.SHADING_SHOW_OBJECT_OUTLINE.value: True, + RasterStyleProperty.SHADING_SHOW_CAVITY.value: True, + RasterStyleProperty.SHADING_CAVITY_TYPE.value: "BOTH", + RasterStyleProperty.SHADING_CURVATURE_RIDGE_FACTOR.value: 1, + RasterStyleProperty.SHADING_CURVATURE_VALLEY_FACTOR.value: 1, + RasterStyleProperty.VIEW_TRANSFORM.value: "Standard", + RasterStyleProperty.SHADING_LIGHT.value: "STUDIO", + RasterStyleProperty.SHADING_COLOR_TYPE.value: "MATERIAL", + RasterStyleProperty.SHADING_SINGLE_COLOR.value: (1, 1, 1), + RasterStyleProperty.SHADING_SHOW_SHADOWS.value: True, + RasterStyleProperty.SHADING_SHADOW_INTENSITY.value: 0.5, + RasterStyleProperty.DISPLAY_LIGHT_DIRECTION.value: (0.5, 0.5, 0.5), + RasterStyleProperty.VIEW_USE_CURVE_MAPPING.value: False, + RasterStyleProperty.OVERLAY_SHOW_WIREFRAMES.value: True, + RasterStyleProperty.OVERLAY_WIREFRAME_THRESHOLD.value: 0, + RasterStyleProperty.OVERLAY_SHOW_FLOOR.value: False, + RasterStyleProperty.OVERLAY_SHOW_AXIS_X.value: False, + RasterStyleProperty.OVERLAY_SHOW_AXIS_Y.value: False, + RasterStyleProperty.OVERLAY_SHOW_AXIS_Z.value: False, + RasterStyleProperty.OVERLAY_SHOW_OBJECT_ORIGINS.value: False, + RasterStyleProperty.OVERLAY_SHOW_RELATIONSHIP_LINES.value: False, } ) drawing_style = bpy.context.scene.DocProperties.drawing_styles.add() diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 63e0a75112..e6eea8757d 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import math @@ -11,33 +29,31 @@ from blenderbim.bim.ifc import IfcStore def draw_attributes(props, layout, copy_operator=None): for attribute in props: row = layout.row(align=True) - value = None - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - value = attribute.string_value - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - value = attribute.bool_value - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - value = attribute.int_value - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - value = attribute.float_value - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - value = attribute.enum_value - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - if copy_operator: - op = row.operator(f"{copy_operator}", text="", icon="COPYDOWN") - op.data = json.dumps({"name": attribute.name, "value": value, "is_null": attribute.is_null}) + draw_attribute(attribute, row, copy_operator) + + +def draw_attribute(attribute, layout, copy_operator=None): + value_name = attribute.get_value_name() + if not value_name: + layout.label(text=attribute.name) + return + layout.prop( + attribute, + value_name, + text=attribute.name, + ) + if attribute.is_optional: + layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + if copy_operator: + op = layout.operator(f"{copy_operator}", text="", icon="COPYDOWN") + op.data = json.dumps({"name": attribute.name, "value": attribute.get_value(), "is_null": attribute.is_null}) def import_attributes(ifc_class, props, data, callback=None): for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity" or (isinstance(data_type, tuple) and "entity" in ".".join(data_type)): + if isinstance(data_type, tuple) or data_type == "entity": + callback(attribute.name(), None, data) if callback else None continue new = props.add() new.name = attribute.name() @@ -65,20 +81,9 @@ def import_attributes(ifc_class, props, data, callback=None): def export_attributes(props, callback=None): attributes = {} - for attribute in props: - is_handled_by_callback = callback(attributes, attribute) if callback else False - if attribute.is_null: - attributes[attribute.name] = None - elif is_handled_by_callback: - pass # Our job is done - elif attribute.data_type == "string": - attributes[attribute.name] = attribute.string_value - elif attribute.data_type == "boolean": - attributes[attribute.name] = attribute.bool_value - elif attribute.data_type == "integer": - attributes[attribute.name] = attribute.int_value - elif attribute.data_type == "float": - attributes[attribute.name] = attribute.float_value - elif attribute.data_type == "enum": - attributes[attribute.name] = attribute.enum_value + for prop in props: + is_handled_by_callback = callback(attributes, prop) if callback else False + if is_handled_by_callback: + continue # Our job is done + attributes[prop.name] = prop.get_value() return attributes diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index acc3b71817..b5879fce39 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -1,7 +1,28 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import uuid +import zipfile +import tempfile import ifcopenshell import blenderbim.bim.handler +from pathlib import Path class IfcStore: @@ -16,6 +37,7 @@ class IfcStore: library_path = "" library_file = None element_listeners = set() + undo_redo_stack_objects = set() current_transaction = "" last_transaction = "" history = [] @@ -40,11 +62,26 @@ class IfcStore: IfcStore.path = bpy.context.scene.BIMProperties.ifc_file if IfcStore.path: try: - IfcStore.file = ifcopenshell.open(IfcStore.path) + IfcStore.load_file(IfcStore.path) except: - IfcStore.file + pass return IfcStore.file + @staticmethod + def load_file(path): + extension = path.split(".")[-1] + if extension.lower() == "ifczip": + with tempfile.TemporaryDirectory() as unzipped_path: + with zipfile.ZipFile(path, "r") as zip_ref: + zip_ref.extractall(unzipped_path) + for filename in Path(unzipped_path).glob("**/*.ifc"): + IfcStore.file = ifcopenshell.open(filename) + return + elif extension.lower() == "ifcxml": + IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path)) + elif extension.lower() == "ifc": + IfcStore.file = ifcopenshell.open(path) + @staticmethod def get_schema(): if IfcStore.file is None: @@ -61,7 +98,7 @@ class IfcStore: map_object = IfcStore.guid_map try: obj = map_object[id_or_guid] - obj.type # In case the object has been deleted, this triggers an exception + obj.name # In case the object has been deleted, this triggers an exception except: return return obj @@ -71,18 +108,38 @@ class IfcStore: IfcStore.element_listeners.add(callback) @staticmethod - def reload_linked_elements(should_reload_selected=False): + def update_undo_redo_stack_objects(): + """Keeps track of selected object names, typically during undo and redo + + When any Blender object is stored outside a Blender PointerProperty, such as + in a regular Python list, there is the likely probability that the object + will be invalidated when undo or redo occurs. Object invalidation seems to + only occur for selected objects either pre/post undo/redo event, including + selected objects for consecutive undo/redos, and all children. + + So if I first select o1, then o2, then o3, then press undo, o3 will be + invalidated. If instead I press undo twice, o3 and o2 will be invalidated. + """ + if bpy.context.active_object: + objects = set([o.name for o in bpy.context.selected_objects + [bpy.context.active_object]]) + objects.update([o.name for o in bpy.context.active_object.children]) + else: + objects = set([o.name for o in bpy.context.selected_objects]) + for obj in bpy.context.selected_objects: + objects.update([o.name for o in obj.children]) + IfcStore.undo_redo_stack_objects |= objects + + @staticmethod + def reload_linked_elements(objects=None): file = IfcStore.get_file() if not file: return - if should_reload_selected: - objects = bpy.context.selected_objects - if bpy.context.active_object: - objects += [bpy.context.active_object] - else: + if objects is None: objects = bpy.data.objects for obj in objects: + if not obj: + continue if not obj.BIMObjectProperties.ifc_definition_id: continue element = file.by_id(obj.BIMObjectProperties.ifc_definition_id) @@ -96,9 +153,19 @@ class IfcStore: IfcStore.id_map[element.id()] = obj if hasattr(element, "GlobalId"): IfcStore.guid_map[element.GlobalId] = obj - obj.BIMObjectProperties.ifc_definition_id = element.id() - blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) + + if element.is_a("IfcSurfaceStyle"): + obj.BIMMaterialProperties.ifc_style_id = element.id() + else: + obj.BIMObjectProperties.ifc_definition_id = element.id() + blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback) + + if isinstance(obj, bpy.types.Material): + blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) + elif isinstance(obj, bpy.types.Object): + blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) + for listener in IfcStore.element_listeners: listener(element, obj) @@ -118,10 +185,14 @@ class IfcStore: def commit_link_element(data): obj = bpy.data.objects.get(data["obj"]) IfcStore.id_map[data["id"]] = obj - if data["guid"]: + if "guid" in data: IfcStore.guid_map[data["guid"]] = obj blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback) + if isinstance(obj, bpy.types.Material): + blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) + # TODO Listeners are not re-registered. Does this cause nasty problems to debug later on? + # TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems. @staticmethod def unlink_element(element=None, obj=None): @@ -165,24 +236,15 @@ class IfcStore: if is_top_level_operator: IfcStore.get_file().end_transaction() IfcStore.add_transaction_operation( - operator, rollback=IfcStore.rollback_ifc_operator, commit=IfcStore.commit_ifc_operator + operator, rollback=lambda d: IfcStore.get_file().undo(), commit=lambda d: IfcStore.get_file().redo() ) IfcStore.end_transaction(operator) return result - @staticmethod - def rollback_ifc_operator(data): - IfcStore.get_file().undo() - blenderbim.bim.handler.purge_module_data() - - @staticmethod - def commit_ifc_operator(data): - IfcStore.get_file().redo() - blenderbim.bim.handler.purge_module_data() - @staticmethod def begin_transaction(operator): + IfcStore.undo_redo_stack_objects = set() IfcStore.current_transaction = str(uuid.uuid4()) operator.transaction_key = IfcStore.current_transaction diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 640feb95a8..d829c6f184 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -1,28 +1,38 @@ -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.util.geolocation -import ifcopenshell.util.selector -import ifcopenshell.util.element -import ifcopenshell.util.unit -import bpy -import bmesh -import os +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import re +import bpy +import time +import bmesh import shutil import threading -import json -import time import mathutils -import math -import multiprocessing -import zipfile -import tempfile import numpy as np -from blenderbim.bim.module.drawing.prop import getDiagramScales -from pathlib import Path -from itertools import cycle -from datetime import datetime +import multiprocessing +import ifcopenshell +import ifcopenshell.geom +import ifcopenshell.util.unit +import ifcopenshell.util.element +import ifcopenshell.util.selector +import ifcopenshell.util.geolocation from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.module.drawing.prop import get_diagram_scales class FileCopy(threading.Thread): @@ -54,9 +64,35 @@ class MaterialCreator: if not self.mesh or self.mesh.name in self.parsed_meshes: return self.parsed_meshes.add(self.mesh.name) + self.add_default_material(element) if self.parse_representations(element): self.assign_material_slots_to_faces() + def add_default_material(self, element): + element_material = ifcopenshell.util.element.get_material(element) + if not element_material: + return + for material in [m for m in self.ifc_importer.file.traverse(element_material) if m.is_a("IfcMaterial")]: + if not material.HasRepresentation: + continue + surface_style = [ + s for s in self.ifc_importer.file.traverse(material.HasRepresentation[0]) if s.is_a("IfcSurfaceStyle") + ] + if surface_style: + self.mesh.materials.append(self.styles[surface_style[0].id()]) + return + # For authoring convenience, we choose to assign a material, even if it has no surface style. See #1585. + for material in [m for m in self.ifc_importer.file.traverse(element_material) if m.is_a("IfcMaterial")]: + self.mesh.materials.append(self.materials[material.id()]) + return + + def load_existing_materials(self): + for material in bpy.data.materials: + if material.BIMObjectProperties.ifc_definition_id: + self.materials[material.BIMObjectProperties.ifc_definition_id] = material + if material.BIMMaterialProperties.ifc_style_id: + self.styles[material.BIMMaterialProperties.ifc_style_id] = material + def parse_representations(self, element): has_parsed = False if hasattr(element, "Representation"): @@ -127,12 +163,9 @@ class IfcImporter: self.settings_native.set(self.settings_native.INCLUDE_CURVES, True) self.settings_2d = ifcopenshell.geom.settings() self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True) - self.filter_mode = "BLACKLIST" - self.include_elements = set() - self.exclude_elements = set() self.project = None - self.spatial_structure_elements = {} - self.elements = {} + self.collections = {} + self.elements = set() self.type_collection = None self.type_products = {} self.openings = {} @@ -143,7 +176,6 @@ class IfcImporter: self.added_data = {} self.native_elements = set() self.native_data = {} - self.aggregates = {} self.material_creator = MaterialCreator(ifc_import_settings, self) @@ -152,17 +184,19 @@ class IfcImporter: self.time = time.time() print("{} :: {:.2f}".format(message, time.time() - self.time)) self.time = time.time() + self.update_progress(self.progress + 1) + + def update_progress(self, progress): + if progress <= 100: + self.progress = progress + bpy.context.window_manager.progress_update(self.progress) def execute(self): + bpy.context.window_manager.progress_begin(0, 100) + self.progress = 0 self.profile_code("Starting import process") - self.load_diff() - self.profile_code("Load diff") - self.purge_diff() - self.profile_code("Purge diffs") self.load_file() self.profile_code("Loading file") - self.set_ifc_file() - self.profile_code("Setting file") self.calculate_unit_scale() self.profile_code("Calculate unit scale") self.calculate_model_offset() @@ -171,47 +205,41 @@ class IfcImporter: self.profile_code("Set units") self.create_project() self.profile_code("Create project") - self.create_spatial_hierarchy() - self.profile_code("Create spatial hierarchy") - self.create_aggregates() - self.profile_code("Create aggregates") - self.create_aggregate_tree() - self.profile_code("Create aggregate tree") + self.process_element_filter() + self.profile_code("Process element filter") + self.create_collections() + self.profile_code("Create collections") self.create_openings_collection() self.profile_code("Create opening collection") self.create_materials() self.profile_code("Create materials") self.create_styles() self.profile_code("Create styles") - self.process_element_filter() - self.profile_code("Process element filter") - self.parse_native_elements() - self.profile_code("Parsing native elements") - self.create_grids() - self.profile_code("Create grids") - self.create_native_products() - self.profile_code("Create native products") - self.create_products() - self.profile_code("Create products") - self.create_empty_products() - self.profile_code("Create empty products") - self.create_type_products() - self.profile_code("Create type products") self.create_annotation() self.profile_code("Create annotation") - self.create_structural_elements() - self.profile_code("Create structural elements") - self.place_objects_in_spatial_tree() - self.profile_code("Placing objects in spatial tree") + self.parse_native_elements() + self.profile_code("Parsing native elements") + self.create_native_elements() + self.profile_code("Create native elements") + self.create_elements() + self.profile_code("Create elements") + self.create_grids() + self.profile_code("Create grids") + self.create_spatial_elements() + self.profile_code("Create spatial elements") + self.create_structural_items() + self.profile_code("Create structural items") + self.create_type_products() + self.profile_code("Create type products") + self.place_objects_in_collections() + self.profile_code("Place objects in collections") if self.ifc_import_settings.should_merge_by_class: self.merge_by_class() self.profile_code("Merging by class") elif self.ifc_import_settings.should_merge_by_material: self.merge_by_material() self.profile_code("Merging by material") - if self.ifc_import_settings.should_merge_materials_by_colour or ( - self.ifc_import_settings.should_auto_set_workarounds and len(self.material_creator.materials) > 300 - ): + if self.ifc_import_settings.should_merge_materials_by_colour or len(self.material_creator.materials) > 300: self.merge_materials_by_colour() self.profile_code("Merging by colour") self.add_project_to_scene() @@ -221,43 +249,50 @@ class IfcImporter: self.profile_code("Mesh cleaning") self.set_default_context() self.profile_code("Setting default context") + self.update_progress(100) + bpy.context.window_manager.progress_end() - def is_element_far_away(self, element): + def is_element_far_away(self, element, is_meters=True): try: - return self.is_point_far_away(element.ObjectPlacement.RelativePlacement.Location) + return self.is_point_far_away(element.ObjectPlacement.RelativePlacement.Location, is_meters=is_meters) except: pass - def is_point_far_away(self, point): - # Arbitrary threshold based on experience + def is_point_far_away(self, point, is_meters=True): + # Locations greater than 1km are not considered "small sites" according to the georeferencing guide + limit = 1000 if is_meters else (1000 / self.unit_scale) coords = point if hasattr(point, "Coordinates"): coords = point.Coordinates - return abs(coords[0]) > 1000000 or abs(coords[1]) > 1000000 or abs(coords[2]) > 1000000 + return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit def process_element_filter(self): - if not self.ifc_import_settings.ifc_selector: - return - selector = ifcopenshell.util.selector.Selector() - elements = selector.parse(self.file, self.ifc_import_settings.ifc_selector) - if self.ifc_import_settings.ifc_import_filter == "WHITELIST": - self.filter_mode = "WHITELIST" - self.include_elements = set(elements) - elif self.ifc_import_settings.ifc_import_filter == "BLACKLIST": - self.exclude_elements = set(elements) - self.filter_mode = "BLACKLIST" + if self.ifc_import_settings.has_filter: + self.elements = set(self.ifc_import_settings.elements) + self.spatial_elements = self.get_spatial_elements_filtered_by_elements(self.elements) + else: + self.elements = set(self.file.by_type("IfcElement")) + if self.file.schema == "IFC2X3": + self.spatial_elements = set(self.file.by_type("IfcSpatialStructureElement")) + else: + self.spatial_elements = set(self.file.by_type("IfcSpatialElement")) + + def get_spatial_elements_filtered_by_elements(self, elements): + leaf_spatial_elements = set([ifcopenshell.util.element.get_container(e) for e in elements]) + results = set() + for spatial_element in leaf_spatial_elements: + while True: + results.add(spatial_element) + spatial_element = ifcopenshell.util.element.get_aggregate(spatial_element) + if not spatial_element or spatial_element.is_a("IfcContext"): + break + return results def parse_native_elements(self): - if self.filter_mode == "WHITELIST": - for element in self.include_elements: - if self.is_native(element): - self.native_elements[element.GlobalId] = element - self.include_elements -= self.native_elements - elif self.filter_mode == "BLACKLIST": - for element in set(self.file.by_type("IfcElement")) - self.exclude_elements: - if self.is_native(element): - self.native_elements.add(element) - self.exclude_elements |= self.native_elements + for element in self.elements: + if self.is_native(element): + self.native_elements.add(element) + self.elements -= self.native_elements def is_native(self, element): if ( @@ -312,12 +347,15 @@ class IfcImporter: props = bpy.context.scene.BIMGeoreferenceProperties if props.has_blender_offset: return - project = self.file.by_type("IfcProject")[0] + if self.file.schema == "IFC2X3": + project = self.file.by_type("IfcProject")[0] + else: + project = self.file.by_type("IfcContext")[0] site = self.find_decomposed_ifc_class(project, "IfcSite") - if site and self.is_element_far_away(site[0]): + if site and self.is_element_far_away(site[0], is_meters=False): return self.guess_georeferencing(site[0]) building = self.find_decomposed_ifc_class(project, "IfcBuilding") - if building and self.is_element_far_away(building[0]): + if building and self.is_element_far_away(building[0], is_meters=False): return self.guess_georeferencing(building[0]) return self.guess_absolute_coordinate() @@ -333,7 +371,6 @@ class IfcImporter: props.blender_x_axis_abscissa = str(placement.RefDirection.DirectionRatios[0]) props.blender_x_axis_ordinate = str(placement.RefDirection.DirectionRatios[1]) props.has_blender_offset = True - props.blender_offset_type = "OBJECT_PLACEMENT" def guess_absolute_coordinate(self): # Civil BIM applications like to work in absolute coordinates, where the ObjectPlacement is 0,0,0 but each @@ -346,10 +383,8 @@ class IfcImporter: props.blender_northings = str(offset_point[1]) props.blender_orthogonal_height = str(offset_point[2]) props.has_blender_offset = True - props.blender_offset_type = "CARTESIAN_POINT" def get_offset_point(self): - offset_point = None elements_checked = 0 # If more than these points aren't far away, the file probably isn't absolutely positioned element_checking_threshold = 100 @@ -363,7 +398,7 @@ class IfcImporter: if elements_checked > element_checking_threshold: return for i, point in enumerate(point_list.CoordList): - if len(point) == 3 and self.is_point_far_away(point): + if len(point) == 3 and self.is_point_far_away(point, is_meters=False): return point for point in self.file.by_type("IfcCartesianPoint"): @@ -377,35 +412,41 @@ class IfcImporter: elements_checked += 1 if elements_checked > element_checking_threshold: return - if len(point.Coordinates) == 3 and self.is_point_far_away(point): + if len(point.Coordinates) == 3 and self.is_point_far_away(point, is_meters=False): return point.Coordinates - def apply_blender_offset_to_matrix(self, matrix): + def apply_blender_offset_to_matrix_world(self, obj, matrix): props = bpy.context.scene.BIMGeoreferenceProperties - if props.has_blender_offset and props.blender_offset_type == "OBJECT_PLACEMENT": - return mathutils.Matrix( - ifcopenshell.util.geolocation.global2local( + if props.has_blender_offset: + if self.is_point_far_away((matrix[0, 3], matrix[1, 3], matrix[2, 3])): + obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" + matrix = ifcopenshell.util.geolocation.global2local( matrix, float(props.blender_eastings) * self.unit_scale, float(props.blender_northings) * self.unit_scale, float(props.blender_orthogonal_height) * self.unit_scale, float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate), - ).tolist() - ) + ) + else: + obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + + if self.ifc_import_settings.should_offset_model: + matrix[0, 3] += self.ifc_import_settings.model_offset_coordinates[0] + matrix[1, 3] += self.ifc_import_settings.model_offset_coordinates[1] + matrix[2, 3] += self.ifc_import_settings.model_offset_coordinates[2] + return mathutils.Matrix(matrix.tolist()) def find_decomposed_ifc_class(self, element, ifc_class): - results = [] + if element.is_a(ifc_class): + return element rel_aggregates = element.IsDecomposedBy - if not rel_aggregates: - return results for rel_aggregate in rel_aggregates: for part in rel_aggregate.RelatedObjects: - if part.is_a(ifc_class): - results.append(part) - results.extend(self.find_decomposed_ifc_class(part, ifc_class)) - return results + result = self.find_decomposed_ifc_class(part, ifc_class) + if result: + return result def create_grids(self): grids = self.file.by_type("IfcGrid") @@ -419,40 +460,36 @@ class IfcImporter: shape = ifcopenshell.geom.create_shape(self.settings_2d, grid) grid_obj = self.create_product(grid, shape) collection = bpy.data.collections.new(self.get_name(grid)) - element_matrix = self.get_local_placement(grid.ObjectPlacement) - element_matrix[0][3] *= self.unit_scale - element_matrix[1][3] *= self.unit_scale - element_matrix[2][3] *= self.unit_scale u_axes = bpy.data.collections.new("UAxes") collection.children.link(u_axes) v_axes = bpy.data.collections.new("VAxes") collection.children.link(v_axes) - self.create_grid_axes(grid.UAxes, u_axes, element_matrix) - self.create_grid_axes(grid.VAxes, v_axes, element_matrix) + self.create_grid_axes(grid.UAxes, u_axes, grid_obj) + self.create_grid_axes(grid.VAxes, v_axes, grid_obj) if grid.WAxes: w_axes = bpy.data.collections.new("WAxes") collection.children.link(w_axes) - self.create_grid_axes(grid.WAxes, w_axes, element_matrix) + self.create_grid_axes(grid.WAxes, w_axes, grid_obj) - def create_grid_axes(self, axes, grid, matrix_world): + def create_grid_axes(self, axes, grid_collection, grid_obj): for axis in axes: shape = ifcopenshell.geom.create_shape(self.settings_2d, axis.AxisCurve) mesh = self.create_mesh(axis, shape) obj = bpy.data.objects.new(f"IfcGridAxis/{axis.AxisTag}", mesh) obj.BIMObjectProperties.ifc_definition_id = axis.id() - obj.matrix_world = matrix_world - grid.objects.link(obj) + obj.matrix_world = grid_obj.matrix_world + grid_collection.objects.link(obj) def create_type_products(self): - type_products = self.file.by_type("IfcTypeProduct") - for collection in self.project["blender"].children: - if collection.name == "Types": - self.type_collection = collection - break - if not self.type_collection: - self.type_collection = bpy.data.collections.new("Types") - self.project["blender"].children.link(self.type_collection) + # TODO allow filtering of spatial elements too + if self.ifc_import_settings.has_filter: + type_products = set([ifcopenshell.util.element.get_type(e) for e in self.elements]) + else: + type_products = self.file.by_type("IfcTypeProduct") + for type_product in type_products: + if not type_product: + continue self.create_type_product(type_product) def create_type_product(self, element): @@ -487,7 +524,7 @@ class IfcImporter: ): return representation_map - def create_native_products(self): + def create_native_elements(self): total = 0 checkpoint = time.time() bm = bmesh.new() @@ -520,32 +557,51 @@ class IfcImporter: bm.to_mesh(mesh) bm.clear() bm.free() - print("Done creating geometry") - def create_products(self): + def create_spatial_elements(self): + products = self.create_products(self.spatial_elements) + self.spatial_elements -= products + products = self.create_curve_products(self.spatial_elements) + self.spatial_elements -= products + for element in self.spatial_elements: + self.create_product(element) + + def create_elements(self): + products = self.create_products(self.elements) + self.elements -= products + products = self.create_curve_products(self.elements) + self.elements -= products + for element in self.elements: + self.create_product(element) + + def create_products(self, products): + results = set() + if not products: + return results if self.ifc_import_settings.should_use_cpu_multiprocessing: iterator = ifcopenshell.geom.iterator( - self.settings, - self.file, - multiprocessing.cpu_count(), - include=self.include_elements or None, - exclude=self.exclude_elements or None, + self.settings, self.file, multiprocessing.cpu_count(), include=products ) else: - iterator = ifcopenshell.geom.iterator( - self.settings, self.file, include=self.include_elements or None, exclude=self.exclude_elements or None - ) + iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=products) valid_file = iterator.initialize() if not valid_file: - return False + return results checkpoint = time.time() total = 0 + start_progress = self.progress + progress_range = 85 - start_progress while True: total += 1 if total % 250 == 0: - print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint)) + print( + "{} ({}%) elements processed in {:.2f}s ...".format( + total, iterator.progress(), time.time() - checkpoint + ) + ) checkpoint = time.time() + #self.update_progress(((total / approx_total_products) * progress_range) + start_progress) shape = iterator.get() if shape: product = self.file.by_id(shape.guid) @@ -554,28 +610,18 @@ class IfcImporter: if shape.context not in ["Body", "Facetation"] and IfcStore.get_element(shape.guid): # We only load a single context, and we prioritise the Body context. See #1290. pass - elif product.is_a("IfcAnnotation") and product.ObjectType == "DRAWING": - # We have already processed this during the create_annotation step - pass else: self.create_product(product, shape) + results.add(product) if not iterator.next(): break print("Done creating geometry") - - def create_empty_products(self): - for element in self.file.by_type("IfcProduct"): - if element.id() in self.added_data: - continue - if element.is_a("IfcPort"): - continue - if not element.Representation: - self.create_product(element) + return results def create_annotation(self): self.create_curve_products(self.file.by_type("IfcAnnotation")) - def create_structural_elements(self): + def create_structural_items(self): # Create structural collections self.structural_member_collection = bpy.data.collections.new("Members") self.structural_connection_collection = bpy.data.collections.new("Connections") @@ -610,10 +656,13 @@ class IfcImporter: mesh.from_pydata([mathutils.Vector(vertex) * self.unit_scale], [], []) obj = bpy.data.objects.new("{}/{}".format(product.is_a(), product.Name), mesh) - obj.matrix_world = mathutils.Matrix(placement_matrix.tolist()) + obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, placement_matrix) self.link_element(product, obj) def create_curve_products(self, products): + results = set() + if not products: + return results if self.ifc_import_settings.should_use_cpu_multiprocessing: iterator = ifcopenshell.geom.iterator( self.settings_2d, self.file, multiprocessing.cpu_count(), include=products @@ -622,7 +671,7 @@ class IfcImporter: iterator = ifcopenshell.geom.iterator(self.settings_2d, self.file, include=products) valid_file = iterator.initialize() if not valid_file: - return False + return results checkpoint = time.time() total = 0 while True: @@ -632,10 +681,13 @@ class IfcImporter: checkpoint = time.time() shape = iterator.get() if shape: + product = self.file.by_id(shape.guid) self.create_product(self.file.by_id(shape.guid), shape) + results.add(product) if not iterator.next(): break print("Done creating geometry") + return results def create_product(self, element, shape=None, mesh=None): if element is None: @@ -666,16 +718,16 @@ class IfcImporter: if shape: m = shape.transformation.matrix.data # We use numpy here because Blender mathutils.Matrix is not accurate enough - mat = np.matrix( + mat = np.array( ([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]) ) - obj.matrix_world = self.apply_blender_offset_to_matrix(mat) + obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, mat) self.material_creator.create(element, obj, mesh) elif mesh: - obj.matrix_world = self.apply_blender_offset_to_matrix(self.get_element_matrix(element)) + obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)) self.material_creator.create(element, obj, mesh) elif hasattr(element, "ObjectPlacement"): - obj.matrix_world = self.apply_blender_offset_to_matrix(self.get_element_matrix(element)) + obj.matrix_world = self.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element)) self.add_opening_relation(element, obj) @@ -686,7 +738,7 @@ class IfcImporter: def get_representation_item_material_name(self, item): if not item.StyledByItem: return - style_ids = [e.id() for e in self.ifc_importer.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")] + style_ids = [e.id() for e in self.file.traverse(item.StyledByItem[0]) if e.is_a("IfcSurfaceStyle")] return style_ids[0] if style_ids else None def create_native_faceted_brep(self, element, mesh_name): @@ -794,6 +846,8 @@ class IfcImporter: def merge_by_class(self): merge_set = {} for obj in self.added_data.values(): + if not isinstance(obj, bpy.types.Object): + continue if "/" not in obj.name or "IfcRelAggregates" in obj.users_collection[0].name: continue merge_set.setdefault(obj.name.split("/")[0], []).append(obj) @@ -802,6 +856,8 @@ class IfcImporter: def merge_by_material(self): merge_set = {} for obj in self.added_data.values(): + if not isinstance(obj, bpy.types.Object): + continue if "/" not in obj.name or "IfcRelAggregates" in obj.users_collection[0].name: continue if not obj.material_slots: @@ -828,6 +884,8 @@ class IfcImporter: cleaned_material["material"].diffuse_color = cleaned_material["diffuse_color"] for obj in self.added_data.values(): + if not isinstance(obj, bpy.types.Object): + continue if not hasattr(obj, "material_slots") or not obj.material_slots: continue for slot in obj.material_slots: @@ -852,6 +910,8 @@ class IfcImporter: obj = None last_obj = None for obj in self.added_data.values(): + if not isinstance(obj, bpy.types.Object): + continue if obj.type == "MESH": obj.select_set(True) last_obj = obj @@ -871,34 +931,11 @@ class IfcImporter: return self.openings[element.GlobalId] = obj - def load_diff(self): - if not self.ifc_import_settings.diff_file: - return - with open(self.ifc_import_settings.diff_file, "r") as file: - self.diff = json.load(file) - def load_file(self): self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file) - extension = self.ifc_import_settings.input_file.split(".")[-1] - if extension.lower() == "ifczip": - with tempfile.TemporaryDirectory() as unzipped_path: - with zipfile.ZipFile(self.ifc_import_settings.input_file, "r") as zip_ref: - zip_ref.extractall(unzipped_path) - for filename in Path(unzipped_path).glob("**/*.ifc"): - self.file = ifcopenshell.open(filename) - break - elif extension.lower() == "ifcxml": - self.file = ifcopenshell.file( - ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(self.ifc_import_settings.input_file) - ) - elif extension.lower() == "ifc": - self.file = ifcopenshell.open(self.ifc_import_settings.input_file) - IfcStore.file = self.file - - def set_ifc_file(self): - bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file - IfcStore.file = self.file - IfcStore.path = self.ifc_import_settings.input_file + if not bpy.context.scene.BIMProperties.ifc_file: + bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file + self.file = IfcStore.get_file() def calculate_unit_scale(self): self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) @@ -934,62 +971,76 @@ class IfcImporter: def create_project(self): self.project = {"ifc": self.file.by_type("IfcProject")[0]} - self.project["blender"] = bpy.data.collections.new("IfcProject/{}".format(self.project["ifc"].Name)) + self.project["blender"] = bpy.data.collections.new( + "{}/{}".format(self.project["ifc"].is_a(), self.project["ifc"].Name) + ) obj = self.create_product(self.project["ifc"]) if obj: self.project["blender"].objects.link(obj) - def create_spatial_hierarchy(self): - if self.project["ifc"].IsDecomposedBy: - for rel_aggregate in self.project["ifc"].IsDecomposedBy: - self.add_related_objects(self.project["blender"], rel_aggregate.RelatedObjects) + def create_collections(self): + if self.ifc_import_settings.collection_mode == "DECOMPOSITION": + self.create_decomposition_collections() + elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION": + self.create_spatial_decomposition_collections() - def add_related_objects(self, parent, related_objects): + def create_decomposition_collections(self): + self.create_spatial_decomposition_collections() + self.create_aggregate_collections() + + def create_spatial_decomposition_collections(self): + for rel_aggregate in self.project["ifc"].IsDecomposedBy or []: + self.create_spatial_decomposition_collection(self.project["blender"], rel_aggregate.RelatedObjects) + self.create_type_collection() + + def create_type_collection(self): + for collection in self.project["blender"].children: + if collection.name == "Types": + self.type_collection = collection + break + if not self.type_collection: + self.type_collection = bpy.data.collections.new("Types") + self.project["blender"].children.link(self.type_collection) + + def create_spatial_decomposition_collection(self, parent, related_objects): for element in related_objects: + if element not in self.spatial_elements: + continue global_id = element.GlobalId collection = bpy.data.collections.new(self.get_name(element)) - self.spatial_structure_elements[global_id] = {"blender": collection} + self.collections[global_id] = collection parent.children.link(collection) - obj = self.create_product(element) - if obj: - self.spatial_structure_elements[global_id]["blender_obj"] = obj - collection.objects.link(obj) if element.IsDecomposedBy: for rel_aggregate in element.IsDecomposedBy: - self.add_related_objects(collection, rel_aggregate.RelatedObjects) + self.create_spatial_decomposition_collection(collection, rel_aggregate.RelatedObjects) - def create_aggregates(self): - rel_aggregates = [a for a in self.file.by_type("IfcRelAggregates") if a.RelatingObject.is_a("IfcElement")] + def create_aggregate_collections(self): + if self.ifc_import_settings.has_filter: + rel_aggregates = [e.IsDecomposedBy[0] for e in self.elements if e.IsDecomposedBy] + else: + rel_aggregates = [a for a in self.file.by_type("IfcRelAggregates") if a.RelatingObject.is_a("IfcElement")] + + if len(rel_aggregates) > 10000: + # More than 10,000 collections makes Blender unhappy + print("Skipping aggregate collections for performance.") + self.ifc_import_settings.collection_mode = "SPATIAL_DECOMPOSITION" + return + + aggregates = {} for rel_aggregate in rel_aggregates: - self.create_aggregate(rel_aggregate) + element = rel_aggregate.RelatingObject + collection = bpy.data.collections.new(self.get_name(element)) + aggregates[element.GlobalId] = {"element": element, "collection": collection} + self.collections[element.GlobalId] = collection - def create_aggregate_tree(self): - for aggregate in self.aggregates.values(): - if aggregate["container"].is_a("IfcSpatialStructureElement"): - self.spatial_structure_elements[aggregate["container"].GlobalId]["blender"].children.link( - aggregate["blender"] - ) - else: - self.aggregates[aggregate["container"].GlobalId]["blender"].children.link(aggregate["blender"]) - - def create_aggregate(self, rel_aggregate): - element = rel_aggregate.RelatingObject - obj = bpy.data.objects.new("{}/{}".format(element.is_a(), element.Name), None) - self.link_element(element, obj) - collection = bpy.data.collections.new(obj.name) - collection.objects.link(obj) - self.aggregates[element.GlobalId] = { - "blender": collection, - "blender_obj": obj, - "container": self.get_aggregate_container(element), - } - - def get_aggregate_container(self, element): - if hasattr(element, "ContainedInStructure") and element.ContainedInStructure: - container = element.ContainedInStructure[0].RelatingStructure - elif hasattr(element, "Decomposes") and element.Decomposes: - container = element.Decomposes[0].RelatingObject - return container + for global_id, aggregate in aggregates.items(): + parent = ifcopenshell.util.element.get_aggregate(aggregate["element"]) + if parent: + self.collections[parent.GlobalId].children.link(aggregate["collection"]) + continue + parent = ifcopenshell.util.element.get_container(aggregate["element"]) + if parent: + self.collections[parent.GlobalId].children.link(aggregate["collection"]) def create_openings_collection(self): self.opening_collection = bpy.data.collections.new("IfcOpeningElements") @@ -997,10 +1048,14 @@ class IfcImporter: def create_materials(self): for material in self.file.by_type("IfcMaterial"): - blender_material = bpy.data.materials.new(material.Name) - blender_material.BIMObjectProperties.ifc_definition_id = material.id() - self.material_creator.materials[material.id()] = blender_material - blender_material.use_fake_user = True + self.create_material(material) + + def create_material(self, material): + blender_material = bpy.data.materials.new(material.Name) + self.link_element(material, blender_material) + self.material_creator.materials[material.id()] = blender_material + blender_material.use_fake_user = True + return blender_material def create_styles(self): parsed_styles = set() @@ -1016,11 +1071,18 @@ class IfcImporter: for style in self.file.by_type("IfcSurfaceStyle"): if style.id() in parsed_styles: continue + self.create_style(style) + + def create_style(self, style, blender_material=None): + if not blender_material: name = style.Name or str(style.id()) blender_material = bpy.data.materials.new(name) - self.create_style(style, blender_material) - def create_style(self, style, blender_material): + old_definition_id = blender_material.BIMObjectProperties.ifc_definition_id + if not old_definition_id: + self.link_element(style, blender_material) + blender_material.BIMObjectProperties.ifc_definition_id = old_definition_id + blender_material.BIMMaterialProperties.ifc_style_id = style.id() self.material_creator.styles[style.id()] = blender_material for surface_style in style.Styles: @@ -1039,78 +1101,41 @@ class IfcImporter: def get_name(self, element): return "{}/{}".format(element.is_a(), element.Name) - def purge_diff(self): - if not self.diff: - return - objects_to_purge = [] - for obj in bpy.data.objects: - if "GlobalId" not in obj.BIMObjectProperties.attributes: - continue - global_id = obj.BIMObjectProperties.attributes["GlobalId"].string_value - if global_id in self.diff["deleted"] or global_id in self.diff["changed"].keys(): - objects_to_purge.append(obj) - bpy.ops.object.delete({"selected_objects": objects_to_purge}) - - def place_objects_in_spatial_tree(self): + def place_objects_in_collections(self): for ifc_definition_id, obj in self.added_data.items(): - self.place_object_in_spatial_tree(self.file.by_id(ifc_definition_id), obj) + if isinstance(obj, bpy.types.Object): + self.place_object_in_collection(self.file.by_id(ifc_definition_id), obj) - def place_object_in_spatial_tree(self, element, obj): + def place_object_in_collection(self, element, obj): + if self.ifc_import_settings.collection_mode == "DECOMPOSITION": + self.place_object_in_decomposition_collection(element, obj) + elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION": + self.place_object_in_spatial_decomposition_collection(element, obj) + + def place_object_in_decomposition_collection(self, element, obj): if element.is_a("IfcProject"): return - elif element.is_a("IfcTypeObject"): - self.type_collection.objects.link(obj) - elif element.GlobalId in self.aggregates: + elif element.GlobalId in self.collections: + return self.collections[element.GlobalId].objects.link(obj) + elif getattr(element, "Decomposes", None): + aggregate = ifcopenshell.util.element.get_aggregate(element) + return self.collections[aggregate.GlobalId].objects.link(obj) + else: + return self.place_object_in_spatial_decomposition_collection(element, obj) + + def place_object_in_spatial_decomposition_collection(self, element, obj): + if element.is_a("IfcProject"): return - elif element.GlobalId in self.spatial_structure_elements: - if not obj.data: - return - # Since spatial structure elements are generated as empties, we'll replace it with the representation - spatial_obj = self.spatial_structure_elements[element.GlobalId]["blender_obj"] - spatial_collection = self.spatial_structure_elements[element.GlobalId]["blender"] - spatial_name = spatial_obj.name - spatial_collection.objects.link(obj) - bpy.data.objects.remove(spatial_obj) - obj.name = spatial_name - elif ( - hasattr(element, "ContainedInStructure") - and element.ContainedInStructure - and element.ContainedInStructure[0].RelatingStructure - ): - container = element.ContainedInStructure[0].RelatingStructure - if element.is_a("IfcGrid"): - grid_collection = bpy.data.collections.get(obj.name) - if grid_collection: # Just in case we ran into invalid grids from Revit - self.spatial_structure_elements[container.GlobalId]["blender"].children.link(grid_collection) - grid_collection.objects.link(obj) - else: - self.spatial_structure_elements[container.GlobalId]["blender"].objects.link(obj) - elif hasattr(element, "Decomposes") and element.Decomposes: - collection = None - if element.Decomposes[0].RelatingObject.is_a("IfcProject"): - collection = self.project["blender"] - elif element.Decomposes[0].RelatingObject.is_a("IfcSpatialStructureElement"): - if element.is_a("IfcSpatialStructureElement"): - global_id = element.GlobalId - if global_id in self.spatial_structure_elements: - if ( - element.is_a("IfcSpatialStructureElement") - and "blender_obj" in self.spatial_structure_elements[global_id] - ): - bpy.data.objects.remove(self.spatial_structure_elements[global_id]["blender_obj"]) - collection = self.spatial_structure_elements[global_id]["blender"] - else: - collection = self.aggregates[element.Decomposes[0].RelatingObject.GlobalId]["blender"] - if collection: - collection.objects.link(obj) - else: - self.ifc_import_settings.logger.error("An element could not be placed in the spatial tree %s", element) + elif element.GlobalId in self.collections: + return self.collections[element.GlobalId].objects.link(obj) + elif element.is_a("IfcTypeObject"): + return self.type_collection.objects.link(obj) elif element.is_a("IfcOpeningElement"): - self.opening_collection.objects.link(obj) + return self.opening_collection.objects.link(obj) elif element.is_a("IfcStructuralMember"): - self.structural_member_collection.objects.link(obj) + return self.structural_member_collection.objects.link(obj) elif element.is_a("IfcStructuralConnection"): - self.structural_connection_collection.objects.link(obj) + return self.structural_connection_collection.objects.link(obj) elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": view_collection = bpy.data.collections.get("Views") if not view_collection: @@ -1120,11 +1145,23 @@ class IfcImporter: drawing_collection = bpy.data.collections.new("IfcGroup/" + group.Name) view_collection.children.link(drawing_collection) drawing_collection.objects.link(obj) + return + + container = ifcopenshell.util.element.get_container(element) + if container: + if element.is_a("IfcGrid"): # TODO: refactor into a more holistic collection mode feature + grid_collection = bpy.data.collections.get(obj.name) + if grid_collection: # Just in case we run into invalid grids from Revit + self.collections[container.GlobalId].children.link(grid_collection) + grid_collection.objects.link(obj) + else: + self.collections[container.GlobalId].objects.link(obj) + else: self.ifc_import_settings.logger.warning("Warning: this object is outside the spatial hierarchy %s", element) bpy.context.scene.collection.objects.link(obj) - def get_element_matrix(self, element, mesh_name=None): + def get_element_matrix(self, element): result = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) result[0][3] *= self.unit_scale result[1][3] *= self.unit_scale @@ -1240,7 +1277,9 @@ class IfcImporter: if "TargetView" in pset: camera.BIMCameraProperties.target_view = pset["TargetView"] if "Scale" in pset: - valid_scales = [i[0] for i in getDiagramScales(None, None) if pset["Scale"] == i[0].split("|")[-1]] + valid_scales = [ + i[0] for i in get_diagram_scales(None, bpy.context) if pset["Scale"] == i[0].split("|")[-1] + ] if valid_scales: camera.BIMCameraProperties.diagram_scale = valid_scales[0] else: @@ -1258,7 +1297,11 @@ class IfcImporter: mesh = bpy.data.meshes.new(self.get_mesh_name(geometry)) props = bpy.context.scene.BIMGeoreferenceProperties - if props.has_blender_offset and props.blender_offset_type == "CARTESIAN_POINT": + if ( + props.has_blender_offset + and geometry.verts + and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2])) + ): ordinate_index = 0 verts = [None] * len(geometry.verts) offset_point = ( @@ -1283,13 +1326,7 @@ class IfcImporter: num_vertex_indices = len(geometry.faces) mesh.vertices.add(num_vertices) - if self.ifc_import_settings.should_offset_model: - # Potentially, there is a smarter way to do this. See #1047 - v_index = cycle((0, 1, 2)) - verts = [v + self.ifc_import_settings.model_offset_coordinates[next(v_index)] for v in verts] - mesh.vertices.foreach_set("co", verts) - else: - mesh.vertices.foreach_set("co", verts) + mesh.vertices.foreach_set("co", verts) mesh.loops.add(num_vertex_indices) mesh.loops.foreach_set("vertex_index", geometry.faces) mesh.polygons.add(num_loops) @@ -1364,7 +1401,6 @@ class IfcImportSettings: self.logger = None self.input_file = None self.diff_file = None - self.should_auto_set_workarounds = True self.should_use_cpu_multiprocessing = True self.should_merge_by_class = False self.should_merge_by_material = False @@ -1374,12 +1410,12 @@ class IfcImportSettings: self.angular_tolerance = 0.5 self.should_offset_model = False self.model_offset_coordinates = (0, 0, 0) - self.ifc_import_filter = "NONE" - self.ifc_selector = "" + self.has_filter = None + self.elements = "" + self.collection_mode = "DECOMPOSITION" @staticmethod def factory(context, input_file, logger): - scene_bim = context.scene.BIMProperties scene_diff = context.scene.DiffProperties settings = IfcImportSettings() settings.input_file = input_file diff --git a/src/blenderbim/blenderbim/bim/module/__init__.py b/src/blenderbim/blenderbim/bim/module/__init__.py new file mode 100644 index 0000000000..1498d0279b --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/__init__.py @@ -0,0 +1,17 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py index d70cecd453..bd7f6946ea 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, operator diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index fe85844eb4..b9b830df3b 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api from blenderbim.bim.ifc import IfcStore @@ -17,7 +35,7 @@ class AssignObject(bpy.types.Operator): def _execute(self, context): self.file = IfcStore.get_file() related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects ) relating_object = bpy.data.objects.get(self.relating_object) if not relating_object or not relating_object.BIMObjectProperties.ifc_definition_id: @@ -40,7 +58,7 @@ class AssignObject(bpy.types.Operator): spatial_collection = bpy.data.collections.get(related_object.name) relating_collection = bpy.data.collections.get(relating_object.name) if spatial_collection: - self.remove_collection(bpy.context.scene.collection, spatial_collection) + self.remove_collection(context.scene.collection, spatial_collection) for collection in bpy.data.collections: if collection == relating_collection: if not collection.children.get(spatial_collection.name): @@ -67,8 +85,8 @@ class EnableEditingAggregate(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.context.active_object.BIMObjectProperties.relating_object = None - bpy.context.active_object.BIMObjectProperties.is_editing_aggregate = True + context.active_object.BIMObjectProperties.relating_object = None + context.active_object.BIMObjectProperties.is_editing_aggregate = True return {"FINISHED"} @@ -79,7 +97,7 @@ class DisableEditingAggregate(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object obj.BIMObjectProperties.is_editing_aggregate = False return {"FINISHED"} @@ -94,9 +112,9 @@ class AddAggregate(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object aggregate_collection = bpy.data.collections.new("IfcElementAssembly/Assembly") - bpy.context.scene.collection.children.link(aggregate_collection) + context.scene.collection.children.link(aggregate_collection) aggregate = bpy.data.objects.new("Assembly", None) aggregate_collection.objects.link(aggregate) bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class="IfcElementAssembly") diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index d136bc9afc..3ce391dc8e 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel from ifcopenshell.api.aggregate.data import Data from blenderbim.bim.ifc import IfcStore @@ -37,8 +55,10 @@ class BIM_PT_aggregate(Panel): row = self.layout.row(align=True) row.prop(props, "relating_object", text="") if props.relating_object: - row.operator("bim.assign_object", icon="CHECKMARK", text="").relating_object = props.relating_object.name - row.operator("bim.disable_editing_aggregate", icon="X", text="") + row.operator( + "bim.assign_object", icon="CHECKMARK", text="" + ).relating_object = props.relating_object.name + row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="") else: row = self.layout.row(align=True) name = "{}/{}".format( diff --git a/src/blenderbim/blenderbim/bim/module/attribute/__init__.py b/src/blenderbim/blenderbim/bim/module/attribute/__init__.py index f7c565111c..d91a5ad409 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/attribute/operator.py b/src/blenderbim/blenderbim/bim/module/attribute/operator.py index 8d0432da77..e29bc69628 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/operator.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell @@ -21,8 +39,7 @@ class EnableEditingAttributes(bpy.types.Operator): obj = bpy.data.materials.get(self.obj) oprops = obj.BIMObjectProperties props = obj.BIMAttributeProperties - while len(props.attributes) > 0: - props.attributes.remove(0) + props.attributes.clear() for attribute in Data.products[oprops.ifc_definition_id]: new = props.attributes.add() if attribute["type"] == "entity" or (attribute["type"] == "list" and attribute["list_type"] == "entity"): @@ -101,15 +118,7 @@ class EditAttributes(bpy.types.Operator): elif attribute["type"] == "enum": attributes[attribute["name"]] = blender_attribute.enum_value product = self.file.by_id(oprops.ifc_definition_id) - ifcopenshell.api.run( - "attribute.edit_attributes", self.file, **{"product": product, "attributes": attributes} - ) - if "Name" in attributes: - new_name = "{}/{}".format(product.is_a(), product.Name or "Unnamed") - collection = bpy.data.collections.get(obj.name) - if collection: - collection.name = new_name - obj.name = new_name + ifcopenshell.api.run("attribute.edit_attributes", self.file, **{"product": product, "attributes": attributes}) Data.load(IfcStore.get_file(), oprops.ifc_definition_id) bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type) return {"FINISHED"} @@ -121,11 +130,11 @@ class GenerateGlobalId(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - index = bpy.context.active_object.BIMAttributeProperties.attributes.find("GlobalId") + index = context.active_object.BIMAttributeProperties.attributes.find("GlobalId") if index >= 0: - global_id = bpy.context.active_object.BIMAttributeProperties.attributes[index] + global_id = context.active_object.BIMAttributeProperties.attributes[index] else: - global_id = bpy.context.active_object.BIMAttributeProperties.attributes.add() + global_id = context.active_object.BIMAttributeProperties.attributes.add() global_id.name = "GlobalId" global_id.data_type = "string" global_id.string_value = ifcopenshell.guid.new() diff --git a/src/blenderbim/blenderbim/bim/module/attribute/prop.py b/src/blenderbim/blenderbim/bim/module/attribute/prop.py index 28701a4600..35391fc321 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/prop.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.ifc import IfcStore from blenderbim.bim.prop import StrProperty, Attribute @@ -13,6 +31,7 @@ from bpy.props import ( CollectionProperty, ) + class BIMAttributeProperties(PropertyGroup): attributes: CollectionProperty(name="Attributes", type=Attribute) is_editing_attributes: BoolProperty(name="Is Editing Attributes") diff --git a/src/blenderbim/blenderbim/bim/module/attribute/ui.py b/src/blenderbim/blenderbim/bim/module/attribute/ui.py index 2a12aa343d..e6d5c5fa1b 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/ui.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.attribute.data import Data @@ -42,10 +60,6 @@ def draw_ui(context, layout, obj_type): icon="RADIOBUT_OFF" if blender_attribute.is_null else "RADIOBUT_ON", text="", ) - # TODO: reimplement, see #1222 - # op = row.operator("bim.copy_attribute_to_selection", icon="COPYDOWN", text="") - # op.attribute_name = attribute.name - # op.attribute_value = attribute.string_value else: row = layout.row() op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit") @@ -87,7 +101,7 @@ class BIM_PT_object_attributes(Panel): class BIM_PT_material_attributes(Panel): - bl_label = "IFC Attributes" + bl_label = "IFC Material Attributes" bl_idname = "BIM_PT_material_attributes" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -95,6 +109,8 @@ class BIM_PT_material_attributes(Panel): @classmethod def poll(cls, context): + if not IfcStore.get_file(): + return False try: return bool(context.active_object.active_material.BIMObjectProperties.ifc_definition_id) except: diff --git a/src/blenderbim/blenderbim/bim/module/augin/__init__.py b/src/blenderbim/blenderbim/bim/module/augin/__init__.py index 9bfda2e096..d34dd212ef 100644 --- a/src/blenderbim/blenderbim/bim/module/augin/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/augin/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/augin/operator.py b/src/blenderbim/blenderbim/bim/module/augin/operator.py index 32d6b390e7..62f5c3b09d 100644 --- a/src/blenderbim/blenderbim/bim/module/augin/operator.py +++ b/src/blenderbim/blenderbim/bim/module/augin/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import json @@ -13,9 +31,10 @@ import addon_utils class AuginLogin(bpy.types.Operator): bl_idname = "bim.augin_login" bl_label = "Login to Augin" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = bpy.context.scene.AuginProperties + props = context.scene.AuginProperties url = "https://server.auge.pro.br/API/v3/augin_rest.php/user_login" payload = {"email": props.username, "password": props.password} @@ -32,9 +51,10 @@ class AuginLogin(bpy.types.Operator): class AuginReset(bpy.types.Operator): bl_idname = "bim.augin_reset" bl_label = "Upload Another Project" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = bpy.context.scene.AuginProperties + props = context.scene.AuginProperties props.is_success = False return {"FINISHED"} @@ -42,11 +62,13 @@ class AuginReset(bpy.types.Operator): class AuginCreateNewModel(bpy.types.Operator): bl_idname = "bim.augin_create_new_model" bl_label = "Create New Augin Model" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): import boto3 from botocore.config import Config - props = bpy.context.scene.AuginProperties + + props = context.scene.AuginProperties # Create project url = "https://server.auge.pro.br/API/v3/augin_rest.php/new_model" @@ -110,16 +132,15 @@ class AuginCreateNewModel(bpy.types.Operator): context.scene.render.image_settings.file_format = old_file_format context.scene.render.filepath = old_filepath - client.upload_file(bpy.context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"]) + client.upload_file(context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"]) client.upload_file(thumb_path, result["s3_bucket"], result["thumb_path"]) - # Notify done url = "https://server.auge.pro.br/API/v3/augin_rest.php/files_uploaded" payload = { "user_token": props.token, - "ifc_filesize": os.path.getsize(bpy.context.scene.BIMProperties.ifc_file), - "model_filesize": os.path.getsize(bpy.context.scene.BIMProperties.ifc_file), + "ifc_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file), + "model_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file), "thumb_filesize": os.path.getsize(thumb_path), "model_upload_path": result["model_path"], "thumb_upload_path": result["thumb_path"], diff --git a/src/blenderbim/blenderbim/bim/module/augin/prop.py b/src/blenderbim/blenderbim/bim/module/augin/prop.py index 35087503dc..3424ac09b8 100644 --- a/src/blenderbim/blenderbim/bim/module/augin/prop.py +++ b/src/blenderbim/blenderbim/bim/module/augin/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy.props import bpy.types diff --git a/src/blenderbim/blenderbim/bim/module/augin/ui.py b/src/blenderbim/blenderbim/bim/module/augin/ui.py index b6f5697f59..8349cd8c23 100644 --- a/src/blenderbim/blenderbim/bim/module/augin/ui.py +++ b/src/blenderbim/blenderbim/bim/module/augin/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy.types @@ -28,7 +46,7 @@ class BIM_PT_augin(bpy.types.Panel): row = layout.row() row.label(text="Logged in as " + props.username) - if not bpy.context.scene.BIMProperties.ifc_file: + if not context.scene.BIMProperties.ifc_file: row = layout.row() row.label(text="No IFC Found") return diff --git a/src/blenderbim/blenderbim/bim/module/bcf/__init__.py b/src/blenderbim/blenderbim/bim/module/bcf/__init__.py index ab9af36d50..a6cde8cf30 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -20,6 +38,7 @@ classes = ( operator.AddBcfLabel, operator.AddBcfRelatedTopic, operator.ViewBcfTopic, + operator.RemoveBcfTopic, operator.RemoveBcfComment, operator.RemoveBcfBimSnippet, operator.RemoveBcfReferenceLink, diff --git a/src/blenderbim/blenderbim/bim/module/bcf/bcfstore.py b/src/blenderbim/blenderbim/bim/module/bcf/bcfstore.py index 853b0c928b..d6b829339c 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/bcfstore.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/bcfstore.py @@ -1,6 +1,25 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bcf import bcf.v2.bcfxml + class BcfStore: bcfxml = None diff --git a/src/blenderbim/blenderbim/bim/module/bcf/operator.py b/src/blenderbim/blenderbim/bim/module/bcf/operator.py index 352f1d9409..1ce425c590 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/operator.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/operator.py @@ -1,40 +1,66 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import bcf import bcf.bcfxml import bcf.v2.data +import numpy as np +import ifcopenshell +import ifcopenshell.util.unit from . import bcfstore +import blenderbim.bim.module.bcf.prop as bcf_prop from blenderbim.bim.ifc import IfcStore from math import radians, degrees, atan, tan, cos, sin from mathutils import Vector, Matrix, Euler, geometry + class NewBcfProject(bpy.types.Operator): bl_idname = "bim.new_bcf_project" bl_label = "New BCF Project" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.context.scene.BCFProperties.is_loaded = False + context.scene.BCFProperties.is_loaded = False bcfxml = bcfstore.BcfStore.get_bcfxml() bcfxml.new_project() bpy.ops.bim.load_bcf_project() - bpy.context.scene.BCFProperties.is_loaded = True + context.scene.BCFProperties.is_loaded = True return {"FINISHED"} class LoadBcfProject(bpy.types.Operator): bl_idname = "bim.load_bcf_project" bl_label = "Load BCF Project" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) def execute(self, context): - bpy.context.scene.BCFProperties.is_loaded = False + context.scene.BCFProperties.is_loaded = False if self.filepath: bcfstore.BcfStore.bcfxml = bcf.bcfxml.load(self.filepath) bcfxml = bcfstore.BcfStore.get_bcfxml() bcfxml.get_project() - bpy.context.scene.BCFProperties.name = bcfxml.project.name + context.scene.BCFProperties.name = bcfxml.project.name bpy.ops.bim.load_bcf_topics() - bpy.context.scene.BCFProperties.is_loaded = True + context.scene.BCFProperties.is_loaded = True return {"FINISHED"} def invoke(self, context, event): @@ -45,30 +71,29 @@ class LoadBcfProject(bpy.types.Operator): class LoadBcfTopics(bpy.types.Operator): bl_idname = "bim.load_bcf_topics" bl_label = "Load BCF Topics" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() bcfxml.get_topics() - while len(bpy.context.scene.BCFProperties.topics) > 0: - bpy.context.scene.BCFProperties.topics.remove(0) - index = 0 - for topic_guid in bcfxml.topics.keys(): - new = bpy.context.scene.BCFProperties.topics.add() - bpy.ops.bim.load_bcf_topic(topic_guid = topic_guid, topic_index = index) - index += 1 + context.scene.BCFProperties.topics.clear() + for index, topic_guid in enumerate(bcfxml.topics.keys()): + new = context.scene.BCFProperties.topics.add() + bpy.ops.bim.load_bcf_topic(topic_guid=topic_guid, topic_index=index) return {"FINISHED"} class LoadBcfTopic(bpy.types.Operator): bl_idname = "bim.load_bcf_topic" bl_label = "Load BCF Topics" + bl_options = {"REGISTER", "UNDO"} topic_guid: bpy.props.StringProperty() topic_index: bpy.props.IntProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() topic = bcfxml.get_topic(self.topic_guid) - new = bpy.context.scene.BCFProperties.topics[self.topic_index] + new = context.scene.BCFProperties.topics[self.topic_index] data_map = { "name": topic.guid, "title": topic.title, @@ -82,64 +107,66 @@ class LoadBcfTopic(bpy.types.Operator): "modified_author": topic.modified_author, "assigned_to": topic.assigned_to, "due_date": topic.due_date, - "description": topic.description + "description": topic.description, } for key, value in data_map.items(): if value is not None: setattr(new, key, str(value)) - while len(new.reference_links) > 0: - new.reference_links.remove(0) + + new.reference_links.clear() for reference_link in topic.reference_links: - new2 = new.reference_links.add() - new2.name = reference_link - while len(new.labels) > 0: - new.labels.remove(0) + new_reference_link = new.reference_links.add() + new_reference_link.name = reference_link + + new.labels.clear() for label in topic.labels: - new2 = new.labels.add() - new2.name = label + new_label = new.labels.add() + new_label.name = label + if topic.bim_snippet: data_map = { "type": topic.bim_snippet.snippet_type, "is_external": topic.bim_snippet.is_external, "reference": topic.bim_snippet.reference, - "schema": topic.bim_snippet.reference_schema + "schema": topic.bim_snippet.reference_schema, } for key, value in data_map.items(): if value is not None: setattr(new.bim_snippet, key, value) - while len(new.document_references) > 0: - new.document_references.remove(0) + + new.document_references.clear() for doc in topic.document_references: - new2 = new.document_references.add() + new_document_references = new.document_references.add() data_map = { "reference": doc.referenced_document, "description": doc.description, "guid": doc.guid, - "is_external": doc.is_external + "is_external": doc.is_external, } for key, value in data_map.items(): if value is not None: - setattr(new2, key, value) - while len(new.related_topics) > 0: - new.related_topics.remove(0) + setattr(new_document_references, key, value) + + new.related_topics.clear() for related_topic in topic.related_topics: - new2 = new.related_topics.add() - new2.name = related_topic.guid - bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) + new_related_topic = new.related_topics.add() + new_related_topic.name = related_topic.guid + + bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid) return {"FINISHED"} class LoadBcfComments(bpy.types.Operator): bl_idname = "bim.load_bcf_comments" bl_label = "Load BCF Comments" + bl_options = {"REGISTER", "UNDO"} topic_guid: bpy.props.StringProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() bcfxml.get_comments(self.topic_guid) - blender_topic = bpy.context.scene.BCFProperties.topics.get(self.topic_guid) - while len(blender_topic.comments) > 0: - blender_topic.comments.remove(0) + blender_topic = context.scene.BCFProperties.topics.get(self.topic_guid) + blender_topic.comments.clear() for comment in bcfxml.topics[self.topic_guid].comments.values(): new = blender_topic.comments.add() data_map = { @@ -160,10 +187,11 @@ class LoadBcfComments(bpy.types.Operator): class EditBcfProjectName(bpy.types.Operator): bl_idname = "bim.edit_bcf_project_name" bl_label = "Edit BCF Project Name" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.project.name = bpy.context.scene.BCFProperties.name + bcfxml.project.name = context.scene.BCFProperties.name bcfxml.edit_project() return {"FINISHED"} @@ -171,20 +199,22 @@ class EditBcfProjectName(bpy.types.Operator): class EditBcfAuthor(bpy.types.Operator): bl_idname = "bim.edit_bcf_author" bl_label = "Edit BCF Author" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - bcfxml.author = bpy.context.scene.BCFProperties.author + bcfxml.author = context.scene.BCFProperties.author return {"FINISHED"} class EditBcfTopicName(bpy.types.Operator): bl_idname = "bim.edit_bcf_topic_name" bl_label = "Edit BCF Topic Name" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic bcfxml = bcfstore.BcfStore.get_bcfxml() topic = bcfxml.topics[blender_topic.name] topic.title = blender_topic.title @@ -195,10 +225,11 @@ class EditBcfTopicName(bpy.types.Operator): class EditBcfTopic(bpy.types.Operator): bl_idname = "bim.edit_bcf_topic" bl_label = "Edit BCF Topic" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic bcfxml = bcfstore.BcfStore.get_bcfxml() topic = bcfxml.topics[blender_topic.name] @@ -212,14 +243,16 @@ class EditBcfTopic(bpy.types.Operator): topic.topic_type = blender_topic.type or None bcfxml.edit_topic(topic) - props.active_topic_index = props.active_topic_index # Refreshes the BCF Topic + props.refresh_topic(context) return {"FINISHED"} class SaveBcfProject(bpy.types.Operator): bl_idname = "bim.save_bcf_project" bl_label = "Save BCF Project" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() @@ -234,12 +267,15 @@ class SaveBcfProject(bpy.types.Operator): class AddBcfTopic(bpy.types.Operator): bl_idname = "bim.add_bcf_topic" bl_label = "Add BCF Topic" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.scene.BCFProperties.author def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() bcfxml.add_topic() - new = bpy.context.scene.BCFProperties.topics.add() - new.name = "New Topic" bpy.ops.bim.load_bcf_topics() return {"FINISHED"} @@ -247,221 +283,300 @@ class AddBcfTopic(bpy.types.Operator): class AddBcfBimSnippet(bpy.types.Operator): bl_idname = "bim.add_bcf_bim_snippet" bl_label = "Add BCF BIM Snippet" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return all( + ( + getattr(context.scene.BCFProperties, attr, False) + for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type") + ) + ) def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] bim_snippet = bcf.v2.data.BimSnippet() - bim_snippet.reference = blender_topic.bim_snippet_reference - bim_snippet.reference_schema = blender_topic.bim_snippet_schema - bim_snippet.snippet_type = blender_topic.bim_snippet_type + bim_snippet.reference = props.bim_snippet_reference + bim_snippet.reference_schema = props.bim_snippet_schema + bim_snippet.snippet_type = props.bim_snippet_type bcfxml.add_bim_snippet(topic, bim_snippet) - bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index) + bim_snippet.reference = "" + bim_snippet.reference_schema = "" + bim_snippet.snippet_type = "" return {"FINISHED"} class AddBcfRelatedTopic(bpy.types.Operator): bl_idname = "bim.add_bcf_related_topic" bl_label = "Add BCF Related Topic" + bl_options = {"REGISTER", "UNDO"} - def execute(self, context): + @classmethod + def poll(cls, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic + if not props.related_topic: + return False + if props.related_topic == blender_topic.title: + # Prevent adding self as related topic + return False related_topic = None for topic in bcfxml.topics.values(): - if topic.title == blender_topic.related_topic: + if topic.title == props.related_topic: related_topic = bcf.v2.data.RelatedTopic() related_topic.guid = topic.guid break if not related_topic: - return {"FINISHED"} + return False + if str(related_topic.guid) in [t.name for t in blender_topic.related_topics]: + # Prevent adding the same related topic more than once + return False + return True + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = context.scene.BCFProperties + blender_topic = props.active_topic + related_topic = bcf.v2.data.RelatedTopic() + related_topic.guid = next((t for t in bcfxml.topics.values() if t.title == props.related_topic)).guid topic = bcfxml.topics[blender_topic.name] topic.related_topics.append(related_topic) bcfxml.edit_topic(topic) - bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index) + props.related_topic = "" return {"FINISHED"} class AddBcfHeaderFile(bpy.types.Operator): bl_idname = "bim.add_bcf_header_file" bl_label = "Add BCF Header File" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.scene.BCFProperties.file_reference def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] header_file = bcf.v2.data.HeaderFile() - header_file.reference = blender_topic.file_reference + header_file.reference = props.file_reference if not os.path.exists(header_file.reference): header_file.filename = header_file.reference - if len(blender_topic.file_ifc_project) == 22: - header_file.ifc_project = blender_topic.file_ifc_project - if len(blender_topic.file_ifc_spatial_structure_element) == 22: - header_file.ifc_spatial_structure_element = blender_topic.file_ifc_spatial_structure_element + if len(props.file_ifc_project) == 22: + header_file.ifc_project = props.file_ifc_project + if len(props.file_ifc_spatial_structure_element) == 22: + header_file.ifc_spatial_structure_element = props.file_ifc_spatial_structure_element bcfxml.add_file(topic, header_file) - props.active_topic_index = props.active_topic_index # refreshes the BCF Topic + props.refresh_topic(context) return {"FINISHED"} class ViewBcfTopic(bpy.types.Operator): bl_idname = "bim.view_bcf_topic" bl_label = "Get BCF Topic" + bl_options = {"REGISTER", "UNDO"} topic_guid: bpy.props.StringProperty() def execute(self, context): - for index, topic in enumerate(bpy.context.scene.BCFProperties.topics): - if topic.guid.lower() == self.topic_guid.lower(): - bpy.context.scene.BCFProperties.active_topic_index = index + for index, topic in enumerate(context.scene.BCFProperties.topics): + if topic.name.lower() == self.topic_guid.lower(): + context.scene.BCFProperties.active_topic_index = index + break return {"FINISHED"} class AddBcfViewpoint(bpy.types.Operator): bl_idname = "bim.add_bcf_viewpoint" bl_label = "Add BCF Viewpoint" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.scene.camera def execute(self, context): - if not bpy.context.scene.camera: - return {"FINISHED"} bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + blender_camera = context.scene.camera + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] viewpoint = bcf.v2.data.Viewpoint() - if bpy.context.scene.camera.data.type == "ORTHO": + if blender_camera.data.type == "ORTHO": camera = bcf.v2.data.OrthogonalCamera() - camera.view_to_world_scale = bpy.context.scene.camera.data.ortho_scale + camera.view_to_world_scale = blender_camera.data.ortho_scale viewpoint.orthogonal_camera = camera - elif bpy.context.scene.camera.data.type == "PERSP": + elif blender_camera.data.type == "PERSP": camera = bcf.v2.data.PerspectiveCamera() - camera.field_of_view = degrees(bpy.context.scene.camera.data.angle) + camera.field_of_view = degrees(blender_camera.data.angle) viewpoint.perspective_camera = camera - camera.camera_view_point.x = bpy.context.scene.camera.location.x - camera.camera_view_point.y = bpy.context.scene.camera.location.y - camera.camera_view_point.z = bpy.context.scene.camera.location.z - direction = bpy.context.scene.camera.matrix_world.to_quaternion() @ Vector((0.0, 0.0, -1.0)) + camera.camera_view_point.x = blender_camera.location.x + camera.camera_view_point.y = blender_camera.location.y + camera.camera_view_point.z = blender_camera.location.z + direction = blender_camera.matrix_world.to_quaternion() @ Vector((0.0, 0.0, -1.0)) camera.camera_direction.x = direction.x camera.camera_direction.y = direction.y camera.camera_direction.z = direction.z - up = bpy.context.scene.camera.matrix_world.to_quaternion() @ Vector((0.0, 1.0, 0.0)) + up = blender_camera.matrix_world.to_quaternion() @ Vector((0.0, 1.0, 0.0)) camera.camera_up_vector.x = up.x camera.camera_up_vector.y = up.y camera.camera_up_vector.z = up.z - old_file_format = bpy.context.scene.render.image_settings.file_format - bpy.context.scene.render.image_settings.file_format = "PNG" - old_filepath = bpy.context.scene.render.filepath - bpy.context.scene.render.filepath = os.path.join(bpy.context.scene.BIMProperties.data_dir, "snapshot.png") + blender_render = context.scene.render + old_file_format = blender_render.image_settings.file_format + blender_render.image_settings.file_format = "PNG" + old_filepath = blender_render.filepath + blender_render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png") bpy.ops.render.opengl(write_still=True) - viewpoint.snapshot = bpy.context.scene.render.filepath + viewpoint.snapshot = blender_render.filepath bcfxml.add_viewpoint(topic, viewpoint) - bpy.context.scene.render.filepath = old_filepath - bpy.context.scene.render.image_settings.file_format = old_file_format - props.active_topic_index = props.active_topic_index # refreshes the BCF Topic + blender_render.filepath = old_filepath + blender_render.image_settings.file_format = old_file_format + props.refresh_topic(context) return {"FINISHED"} class RemoveBcfViewpoint(bpy.types.Operator): bl_idname = "bim.remove_bcf_viewpoint" bl_label = "Remove BCF Viewpoint" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return bcf_prop.getBcfViewpoints(None, context) def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic viewpoint_guid = blender_topic.viewpoints topic = bcfxml.topics[blender_topic.name] bcfxml.delete_viewpoint(viewpoint_guid, topic) - props.active_topic_index = props.active_topic_index # Refreshes the BCF Topic + props.refresh_topic(context) return {"FINISHED"} class RemoveBcfFile(bpy.types.Operator): bl_idname = "bim.remove_bcf_file" bl_label = "Remove BCF File" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] bcfxml.delete_file(topic, self.index) - props.active_topic_index = props.active_topic_index # Refreshes the BCF Topic + props.refresh_topic(context) + return {"FINISHED"} + + +class RemoveBcfTopic(bpy.types.Operator): + bl_idname = "bim.remove_bcf_topic" + bl_label = "Remove BCF Topic" + bl_options = {"REGISTER", "UNDO"} + guid: bpy.props.StringProperty() + + @classmethod + def poll(cls, context): + return context.scene.BCFProperties.topics + + def execute(self, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = context.scene.BCFProperties + topic_to_delete = props.active_topic + bcfxml.delete_topic(topic_to_delete.name) + bpy.ops.bim.load_bcf_topics() return {"FINISHED"} class AddBcfReferenceLink(bpy.types.Operator): bl_idname = "bim.add_bcf_reference_link" bl_label = "Add BCF Reference Link" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.scene.BCFProperties.reference_link def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] - if not blender_topic.reference_link: - return {"FINISHED"} - topic.reference_links.append(blender_topic.reference_link) + topic.reference_links.append(props.reference_link) bcfxml.edit_topic(topic) - bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) - blender_topic.reference_link = "" + bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index) + props.reference_link = "" return {"FINISHED"} class AddBcfDocumentReference(bpy.types.Operator): bl_idname = "bim.add_bcf_document_reference" bl_label = "Add BCF Document Reference" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.scene.BCFProperties.document_reference def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] - if not blender_topic.document_reference: - return {"FINISHED"} document_reference = bcf.v2.data.DocumentReference() - document_reference.referenced_document = blender_topic.document_reference - document_reference.description = blender_topic.document_reference_description or None + document_reference.referenced_document = props.document_reference + document_reference.description = props.document_reference_description or None bcfxml.add_document_reference(topic, document_reference) - bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) - blender_topic.document_reference = "" - blender_topic.document_reference_description = "" + bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index) + props.document_reference = "" + props.document_reference_description = "" return {"FINISHED"} class AddBcfLabel(bpy.types.Operator): bl_idname = "bim.add_bcf_label" bl_label = "Add BCF Label" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.scene.BCFProperties.label def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] - if not blender_topic.label: - return {"FINISHED"} new = blender_topic.labels.add() - new.name = blender_topic.label - topic.labels.append(blender_topic.label) + new.name = props.label + topic.labels.append(props.label) bcfxml.edit_topic(topic) - blender_topic.label = "" + props.label = "" return {"FINISHED"} class EditBcfReferenceLinks(bpy.types.Operator): bl_idname = "bim.edit_bcf_reference_links" bl_label = "Edit BCF Reference Link" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] for index, reference_link in enumerate(topic.reference_links): if reference_link == blender_topic.reference_links[index].name: @@ -474,13 +589,16 @@ class EditBcfReferenceLinks(bpy.types.Operator): class EditBcfLabels(bpy.types.Operator): bl_idname = "bim.edit_bcf_labels" bl_label = "Edit BCF Labels" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] for index, label in enumerate(blender_topic.labels): + if index >= len(topic.labels): + break if label.name == topic.labels[index]: continue topic.labels[index] = blender_topic.labels[index].name @@ -491,12 +609,13 @@ class EditBcfLabels(bpy.types.Operator): class RemoveBcfReferenceLink(bpy.types.Operator): bl_idname = "bim.remove_bcf_reference_link" bl_label = "Remove BCF Reference Link" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] del topic.reference_links[self.index] bcfxml.edit_topic(topic) @@ -507,12 +626,13 @@ class RemoveBcfReferenceLink(bpy.types.Operator): class RemoveBcfLabel(bpy.types.Operator): bl_idname = "bim.remove_bcf_label" bl_label = "Remove BCF Label" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] del topic.labels[self.index] bcfxml.edit_topic(topic) @@ -523,11 +643,12 @@ class RemoveBcfLabel(bpy.types.Operator): class RemoveBcfBimSnippet(bpy.types.Operator): bl_idname = "bim.remove_bcf_bim_snippet" bl_label = "Remove BCF BIM Snippet" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] bcfxml.delete_bim_snippet(topic) blender_topic.bim_snippet.schema = "" @@ -539,123 +660,140 @@ class RemoveBcfBimSnippet(bpy.types.Operator): class RemoveBcfDocumentReference(bpy.types.Operator): bl_idname = "bim.remove_bcf_document_reference" bl_label = "Remove BCF Document Reference" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] bcfxml.delete_document_reference(topic, self.index) - bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index) return {"FINISHED"} class RemoveBcfRelatedTopic(bpy.types.Operator): bl_idname = "bim.remove_bcf_related_topic" bl_label = "Remove BCF Related Topic" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] del topic.related_topics[self.index] bcfxml.edit_topic(topic) - bpy.ops.bim.load_bcf_topic(topic_guid = topic.guid, topic_index = props.active_topic_index) + bpy.ops.bim.load_bcf_topic(topic_guid=topic.guid, topic_index=props.active_topic_index) return {"FINISHED"} class RemoveBcfComment(bpy.types.Operator): bl_idname = "bim.remove_bcf_comment" bl_label = "Remove BCF Comment" + bl_options = {"REGISTER", "UNDO"} comment_guid: bpy.props.StringProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] bcfxml.delete_comment(self.comment_guid, topic) - bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) + bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid) return {"FINISHED"} class EditBcfComment(bpy.types.Operator): bl_idname = "bim.edit_bcf_comment" bl_label = "Edit BCF Comment" + bl_options = {"REGISTER", "UNDO"} comment_guid: bpy.props.StringProperty() def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic blender_comment = blender_topic.comments.get(self.comment_guid) topic = bcfxml.topics[blender_topic.name] comment = topic.comments[self.comment_guid] comment.comment = blender_comment.comment bcfxml.edit_comment(comment, topic) - bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) + bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid) return {"FINISHED"} class AddBcfComment(bpy.types.Operator): bl_idname = "bim.add_bcf_comment" bl_label = "Add BCF Comment" + bl_options = {"REGISTER", "UNDO"} comment_guid: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + props = context.scene.BCFProperties + if not props.comment: + return False + if props.has_related_viewpoint and not bcf_prop.getBcfViewpoints(None, context): + return False + return True + def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] - if not blender_topic.comment: - return {"FINISHED"} comment = bcf.v2.data.Comment() - comment.comment = blender_topic.comment - if blender_topic.has_related_viewpoint and blender_topic.viewpoints: + comment.comment = props.comment + if props.has_related_viewpoint: comment.viewpoint = bcf.v2.data.Viewpoint() comment.viewpoint.guid = blender_topic.viewpoints bcfxml.add_comment(topic, comment) - bpy.ops.bim.load_bcf_comments(topic_guid = topic.guid) - print(bcfxml.filepath) + bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid) + props.comment = "" + props.has_related_viewpoint = False return {"FINISHED"} class ActivateBcfViewpoint(bpy.types.Operator): bl_idname = "bim.activate_bcf_viewpoint" bl_label = "Activate BCF Viewpoint" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + bcfxml = bcfstore.BcfStore.get_bcfxml() + props = context.scene.BCFProperties + blender_topic = props.active_topic + topic = bcfxml.topics[blender_topic.name] + return topic.viewpoints def execute(self, context): self.file = IfcStore.get_file() bcfxml = bcfstore.BcfStore.get_bcfxml() - props = bpy.context.scene.BCFProperties - blender_topic = props.topics[props.active_topic_index] + props = context.scene.BCFProperties + blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] - if not topic.viewpoints: - return {"FINISHED"} viewpoint_guid = blender_topic.viewpoints viewpoint = topic.viewpoints[viewpoint_guid] obj = bpy.data.objects.get("Viewpoint") if not obj: obj = bpy.data.objects.new("Viewpoint", bpy.data.cameras.new("Viewpoint")) - bpy.context.scene.collection.objects.link(obj) - bpy.context.scene.camera = obj + context.scene.collection.objects.link(obj) + context.scene.camera = obj - cam_width = bpy.context.scene.render.resolution_x - cam_height = bpy.context.scene.render.resolution_y + cam_width = context.scene.render.resolution_x + cam_height = context.scene.render.resolution_y cam_aspect = cam_width / cam_height if viewpoint.snapshot: obj.data.show_background_images = True - while len(obj.data.background_images) > 0: - obj.data.background_images.remove(obj.data.background_images[0]) + obj.data.background_images.clear() background = obj.data.background_images.new() - background.image = bpy.data.images.load( - os.path.join(bcfxml.filepath, topic.guid, viewpoint.snapshot) - ) + background.image = bpy.data.images.load(os.path.join(bcfxml.filepath, topic.guid, viewpoint.snapshot)) src_width = background.image.size[0] src_height = background.image.size[1] src_aspect = src_width / src_height @@ -665,9 +803,30 @@ class ActivateBcfViewpoint(bpy.types.Operator): else: background.frame_method = "CROP" background.display_depth = "FRONT" - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area = next(area for area in context.screen.areas if area.type == "VIEW_3D") area.spaces[0].region_3d.view_perspective = "CAMERA" + if self.file: + self.set_viewpoint_components(viewpoint, context) + + gp = bpy.data.grease_pencils.get("BCF") + if gp: + bpy.data.grease_pencils.remove(gp) + if viewpoint.lines: + self.draw_lines(viewpoint, context) + + self.delete_clipping_planes(context) + if viewpoint.clipping_planes: + self.create_clipping_planes(viewpoint) + + self.delete_bitmaps(context) + if viewpoint.bitmaps: + self.create_bitmaps(bcfxml, viewpoint, topic) + + self.setup_camera(viewpoint, obj, cam_aspect, context) + return {"FINISHED"} + + def setup_camera(self, viewpoint, obj, cam_aspect, context): if viewpoint.orthogonal_camera: camera = viewpoint.orthogonal_camera obj.data.type = "ORTHO" @@ -679,74 +838,123 @@ class ActivateBcfViewpoint(bpy.types.Operator): obj.data.angle = radians(camera.field_of_view) else: # https://blender.stackexchange.com/questions/23431/how-to-set-camera-horizontal-and-vertical-fov - obj.data.angle = 2 * atan((0.5 * cam_height) / (0.5 * cam_width / tan(radians(camera.field_of_view) / 2))) + obj.data.angle = 2 * atan( + (0.5 * cam_height) / (0.5 * cam_width / tan(radians(camera.field_of_view) / 2)) + ) - self.set_viewpoint_components(viewpoint) - - gp = bpy.data.grease_pencils.get("BCF") - if gp: - bpy.data.grease_pencils.remove(gp) - if viewpoint.lines: - self.draw_lines(viewpoint) - - self.delete_clipping_planes() - if viewpoint.clipping_planes: - self.create_clipping_planes(viewpoint) - - self.delete_bitmaps() - if viewpoint.bitmaps: - self.create_bitmaps(bcfxml, viewpoint, topic) - - z_axis = Vector((-camera.camera_direction.x, -camera.camera_direction.y, -camera.camera_direction.z)).normalized() + z_axis = Vector( + (-camera.camera_direction.x, -camera.camera_direction.y, -camera.camera_direction.z) + ).normalized() y_axis = Vector((camera.camera_up_vector.x, camera.camera_up_vector.y, camera.camera_up_vector.z)).normalized() x_axis = y_axis.cross(z_axis).normalized() rotation = Matrix((x_axis, y_axis, z_axis)) rotation.invert() - location = Vector((camera.camera_view_point.x, camera.camera_view_point.y, camera.camera_view_point.z)) - obj.matrix_world = rotation.to_4x4() - obj.location = location - return {"FINISHED"} + matrix = np.matrix( + ( + [x_axis[0], y_axis[0], z_axis[0], camera.camera_view_point.x], + [x_axis[1], y_axis[1], z_axis[1], camera.camera_view_point.y], + [x_axis[2], y_axis[2], z_axis[2], camera.camera_view_point.z], + [0, 0, 0, 1], + ) + ) + props = context.scene.BIMGeoreferenceProperties + if props.has_blender_offset: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + matrix = ifcopenshell.util.geolocation.global2local( + matrix, + float(props.blender_eastings) * unit_scale, + float(props.blender_northings) * unit_scale, + float(props.blender_orthogonal_height) * unit_scale, + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + ) + obj.matrix_world = Matrix(matrix.tolist()) - def set_viewpoint_components(self, viewpoint): + def set_viewpoint_components(self, viewpoint, context): if not viewpoint.components: return - selected_global_ids = [s.ifc_guid for s in viewpoint.components.selection] + + # Operators with context overrides are used because they are + # significantly faster than looping through all objects + exception_global_ids = [v.ifc_guid for v in viewpoint.components.visibility.exceptions] + + if viewpoint.components.visibility.default_visibility: + old = context.area.type + context.area.type = "VIEW_3D" + bpy.ops.object.hide_view_clear() + context.area.type = old + for global_id in exception_global_ids: + obj = IfcStore.get_element(global_id) + if obj: + obj.hide_set(True) + else: + objs = [] + for global_id in exception_global_ids: + obj = IfcStore.get_element(global_id) + if obj: + objs.append(obj) + if objs: + old = context.area.type + context.area.type = "VIEW_3D" + context_override = {} + context_override["object"] = context_override["active_object"] = objs[0] + context_override["selected_objects"] = context_override["selected_editable_objects"] = objs + bpy.ops.object.hide_view_set(context_override, unselected=True) + context.area.type = old + + if viewpoint.components.view_setup_hints: + if not viewpoint.components.view_setup_hints.spaces_visible: + self.hide_spaces(context) + if viewpoint.components.view_setup_hints.openings_visible is not None: + self.set_openings_visibility(viewpoint.components.view_setup_hints.openings_visible, context) + else: + self.hide_spaces(context) + self.set_openings_visibility(False, context) + + self.set_selection(viewpoint) + self.set_colours(viewpoint) + + def hide_spaces(self, context): + old = context.area.type + context.area.type = "VIEW_3D" + bpy.ops.object.select_pattern(pattern="IfcSpace/*") + bpy.ops.object.hide_view_set({}) + context.area.type = old + + def set_openings_visibility(self, is_visible, context): + for collection in self.get_opening_collections(context): + collection.hide_viewport = not is_visible + + def set_selection(self, viewpoint): + selected_global_ids = [s.ifc_guid for s in viewpoint.components.selection] + bpy.ops.object.select_all(action="DESELECT") + for global_id in selected_global_ids: + obj = IfcStore.get_element(global_id) + if obj: + obj.select_set(True) + + def set_colours(self, viewpoint): global_id_colours = {} for coloring in viewpoint.components.coloring: for component in coloring.components: global_id_colours.setdefault(component.ifc_guid, coloring.color) + for global_id, color in global_id_colours.items(): + obj = IfcStore.get_element(global_id) + if obj: + obj.color = self.hex_to_rgb(color) - for obj in bpy.data.objects: - if not obj.BIMObjectProperties.ifc_definition_id: - continue - global_id = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId - is_visible = viewpoint.components.visibility.default_visibility - if global_id in exception_global_ids: - is_visible = not is_visible - if not is_visible: - obj.hide_set(True) - continue - if "IfcSpace" in obj.name: - if viewpoint.components.view_setup_hints: - is_visible = viewpoint.components.view_setup_hints.spaces_visible - else: - is_visible = False - elif "IfcOpeningElement" in obj.name: - if viewpoint.components.view_setup_hints: - is_visible = viewpoint.components.view_setup_hints.openings_visible - else: - is_visible = False - obj.hide_set(not is_visible) - if not is_visible: - continue - obj.select_set(global_id in selected_global_ids) - if global_id in global_id_colours: - obj.color = self.hex_to_rgb(global_id_colours[global_id]) + def get_opening_collections(self, context): + collections = [] + for collection in context.view_layer.layer_collection.children: + opening_collection = collection.children.get("IfcOpeningElements") + if opening_collection: + collections.append(opening_collection) + return collections - def draw_lines(self, viewpoint): + def draw_lines(self, viewpoint, context): gp = bpy.data.grease_pencils.new("BCF") - scene = bpy.context.scene + scene = context.scene scene.grease_pencil = gp scene.frame_set(1) layer = gp.layers.new("BCF Annotation", set_active=True) @@ -758,7 +966,9 @@ class ActivateBcfViewpoint(bpy.types.Operator): stroke.points.add(len(viewpoint.lines) * 2) coords = [] for l in viewpoint.lines: - coords.extend([l.start_point.x, l.start_point.y, l.start_point.z, l.end_point.x, l.end_point.y, l.end_point.z]) + coords.extend( + [l.start_point.x, l.start_point.y, l.start_point.z, l.end_point.x, l.end_point.y, l.end_point.z] + ) stroke.points.foreach_set("co", coords) def create_clipping_planes(self, viewpoint): @@ -776,19 +986,19 @@ class ActivateBcfViewpoint(bpy.types.Operator): ) n += 1 - def delete_clipping_planes(self): + def delete_clipping_planes(self, context): collection = bpy.data.collections.get("Sections") if not collection: return for section in collection.objects: - bpy.context.view_layer.objects.active = section + context.view_layer.objects.active = section bpy.ops.bim.remove_section_plane() - def delete_bitmaps(self): + def delete_bitmaps(self, context): collection = bpy.data.collections.get("Bitmaps") if not collection: collection = bpy.data.collections.new("Bitmaps") - bpy.context.scene.collection.children.link(collection) + context.scene.collection.children.link(collection) for bitmap in collection.objects: bpy.data.objects.remove(bitmap) @@ -822,26 +1032,26 @@ class ActivateBcfViewpoint(bpy.types.Operator): t = tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)) return [t[0] / 255.0, t[1] / 255.0, t[2] / 255.0, 1] + class OpenBcfReferenceLink(bpy.types.Operator): bl_idname = "bim.open_bcf_reference_link" bl_label = "Open BCF Reference Link" index: bpy.props.IntProperty() def execute(self, context): - webbrowser.open(bpy.context.scene.BCFProperties.topic_links[self.index].name) + webbrowser.open(context.scene.BCFProperties.topic_links[self.index].name) return {"FINISHED"} class SelectBcfHeaderFile(bpy.types.Operator): bl_idname = "bim.select_bcf_header_file" bl_label = "Select BCF Header File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): if self.filepath: - props = bpy.context.scene.BCFProperties - topic = props.topics[props.active_topic_index] - topic.file_reference = self.filepath + context.scene.BCFProperties.file_reference = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -852,13 +1062,12 @@ class SelectBcfHeaderFile(bpy.types.Operator): class SelectBcfBimSnippetReference(bpy.types.Operator): bl_idname = "bim.select_bcf_bim_snippet_reference" bl_label = "Select BCF BIM Snippet Reference" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): if self.filepath: - props = bpy.context.scene.BCFProperties - topic = props.topics[props.active_topic_index] - topic.bim_snippet_reference = self.filepath + context.scene.BCFProperties.bim_snippet_reference = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -869,13 +1078,12 @@ class SelectBcfBimSnippetReference(bpy.types.Operator): class SelectBcfDocumentReference(bpy.types.Operator): bl_idname = "bim.select_bcf_document_reference" bl_label = "Select BCF Document Reference" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): if self.filepath: - props = bpy.context.scene.BCFProperties - topic = props.topics[props.active_topic_index] - topic.document_reference = self.filepath + context.scene.BCFProperties.document_reference = self.filepath return {"FINISHED"} def invoke(self, context, event): diff --git a/src/blenderbim/blenderbim/bim/module/bcf/prop.py b/src/blenderbim/blenderbim/bim/module/bcf/prop.py index 5c7baa1bd1..e24f5adba0 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/prop.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import bcfstore from blenderbim.bim.prop import StrProperty @@ -23,49 +41,43 @@ def purge(): def updateBcfReferenceLink(self, context): - if bpy.context.scene.BCFProperties.is_loaded: + if context.scene.BCFProperties.is_loaded: bpy.ops.bim.edit_bcf_reference_links() def updateBcfLabel(self, context): - if bpy.context.scene.BCFProperties.is_loaded: + if context.scene.BCFProperties.is_loaded: bpy.ops.bim.edit_bcf_labels() def updateBcfProjectName(self, context): - if bpy.context.scene.BCFProperties.is_loaded: + if self.is_loaded: bpy.ops.bim.edit_bcf_project_name() def updateBcfAuthor(self, context): - if bpy.context.scene.BCFProperties.is_loaded: + if self.is_loaded: bpy.ops.bim.edit_bcf_author() def updateBcfTopicName(self, context): - if bpy.context.scene.BCFProperties.is_loaded: + if context.scene.BCFProperties.is_loaded: bpy.ops.bim.edit_bcf_topic_name() def updateBcfTopicIsEditable(self, context): - if bpy.context.scene.BCFProperties.is_loaded and not self.is_editable: + if context.scene.BCFProperties.is_loaded and not self.is_editable: bpy.ops.bim.edit_bcf_topic() def updateBcfCommentIsEditable(self, context): - if bpy.context.scene.BCFProperties.is_loaded and not self.is_editable: - bpy.ops.bim.edit_bcf_comment(comment_guid = self.name) + if context.scene.BCFProperties.is_loaded and not self.is_editable: + bpy.ops.bim.edit_bcf_comment(comment_guid=self.name) def refreshBcfTopic(self, context): - global bcfviewpoints_enum - bcfviewpoints_enum = None - - props = bpy.context.scene.BCFProperties - bcfxml = bcfstore.BcfStore.get_bcfxml() - topic = props.topics[props.active_topic_index] - header = bcfxml.get_header(topic.name) - getBcfViewpoints(self, context) + self.clear_input_fields() + getBcfViewpoints(None, context, force_update=True) class BcfReferenceLink(PropertyGroup): @@ -76,13 +88,13 @@ class BcfLabel(PropertyGroup): name: StringProperty(name="Name", update=updateBcfLabel) -def getBcfViewpoints(self, context): +def getBcfViewpoints(self, context, force_update=False): global bcfviewpoints_enum - if bcfviewpoints_enum is None: + if bcfviewpoints_enum is None or force_update: # Retrieving Viewpoints is slow. Make sure we only do when needed bcfviewpoints_enum = [] - props = bpy.context.scene.BCFProperties + props = context.scene.BCFProperties bcfxml = bcfstore.BcfStore.get_bcfxml() - topic = props.topics[props.active_topic_index] + topic = props.active_topic viewpoints = bcfxml.get_viewpoints(topic.name) bcfviewpoints_enum.extend([(v, f"Viewpoint {i+1}", "") for i, v in enumerate(viewpoints.keys())]) return bcfviewpoints_enum @@ -127,28 +139,15 @@ class BcfTopic(PropertyGroup): assigned_to: StringProperty(default="", name="Assigned To") due_date: StringProperty(default="", name="Due Date") description: StringProperty(default="", name="Description") - viewpoints: EnumProperty(items=getBcfViewpoints, name="BCF Viewpoints") + viewpoints: EnumProperty(items=lambda _, context: getBcfViewpoints(_, context), name="BCF Viewpoints") files: CollectionProperty(name="Files", type=StrProperty) - file_reference: StringProperty(default="", name="Reference") - file_ifc_project: StringProperty(default="", name="IFC Project") - file_ifc_spatial_structure_element: StringProperty(default="", name="IFC Spatial Structure Element") reference_links: CollectionProperty(name="Reference Links", type=BcfReferenceLink) - reference_link: StringProperty(default="", name="Reference Link") labels: CollectionProperty(name="Labels", type=BcfLabel) - label: StringProperty(default="", name="Label") bim_snippet: PointerProperty(type=BcfBimSnippet) - bim_snippet_type: StringProperty(default="", name="Type") - bim_snippet_reference: StringProperty(default="", name="Reference") - bim_snippet_schema: StringProperty(default="", name="Schema") document_references: CollectionProperty(name="Document References", type=BcfDocumentReference) - document_reference: StringProperty(default="", name="Referenced Document") - document_reference_description: StringProperty(default="", name="Description") related_topics: CollectionProperty(name="Related Topics", type=StrProperty) - related_topic: StringProperty(default="", name="Related Topic") comments: CollectionProperty(name="Comments", type=BcfComment) is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfTopicIsEditable) - comment: StringProperty(default="", name="Comment") - has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False) class BCFProperties(PropertyGroup): @@ -158,3 +157,44 @@ class BCFProperties(PropertyGroup): author: StringProperty(default="john@doe.com", name="Author Email", update=updateBcfAuthor) topics: CollectionProperty(name="BCF Topics", type=BcfTopic) active_topic_index: IntProperty(name="Active BCF Topic Index", update=refreshBcfTopic) + file_reference: StringProperty(default="", name="Reference") + file_ifc_project: StringProperty(default="", name="IFC Project") + file_ifc_spatial_structure_element: StringProperty(default="", name="IFC Spatial Structure Element") + reference_link: StringProperty(default="", name="Reference Link") + label: StringProperty(default="", name="Label") + bim_snippet_reference: StringProperty(default="", name="Reference") + bim_snippet_type: StringProperty(default="", name="Type") + bim_snippet_schema: StringProperty(default="", name="Schema") + document_reference: StringProperty(default="", name="Referenced Document") + document_reference_description: StringProperty(default="", name="Description") + related_topic: StringProperty(name="Related Topic") + comment: StringProperty(default="", name="Comment") + has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False) + + def clear_input_fields(self): + self.file_reference = "" + self.file_ifc_project = "" + self.file_ifc_spatial_structure_element = "" + self.reference_link = "" + self.label = "" + self.bim_snippet_reference = "" + self.bim_snippet_type = "" + self.bim_snippet_schema = "" + self.document_reference = "" + self.document_reference_description = "" + self.related_topic = "" + self.comment = "" + self.has_related_viewpoint = False + + @property + def active_topic(self): + if len(self.topics) == 0: + return None + if self.active_topic_index < 0: + self.active_topic_index = 0 + if self.active_topic_index >= len(self.topics): + self.active_topic_index = len(self.topics) - 1 + return self.topics[self.active_topic_index] + + def refresh_topic(self, context): + refreshBcfTopic(self, context) diff --git a/src/blenderbim/blenderbim/bim/module/bcf/ui.py b/src/blenderbim/blenderbim/bim/module/bcf/ui.py index f03032d4ef..4b1fa8d339 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/ui.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/ui.py @@ -1,8 +1,27 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy from . import bcfstore from bpy.types import Panel + class BIM_PT_bcf(Panel): bl_label = "BCF Project" bl_idname = "BIM_PT_bcf" @@ -17,7 +36,7 @@ class BIM_PT_bcf(Panel): layout.use_property_decorate = False scene = context.scene - props = bpy.context.scene.BCFProperties + props = scene.BCFProperties row = layout.row(align=True) row.operator("bim.new_bcf_project", text="New Project") @@ -34,17 +53,18 @@ class BIM_PT_bcf(Panel): row = layout.row() row.prop(props, "author") - props = bpy.context.scene.BCFProperties + props = context.scene.BCFProperties row = layout.row() row.template_list("BIM_UL_topics", "", props, "topics", props, "active_topic_index") col = row.column(align=True) col.operator("bim.add_bcf_topic", icon="ADD", text="") + col.operator("bim.remove_bcf_topic", icon="REMOVE", text="") if props.active_topic_index < len(props.topics): - topic = props.topics[props.active_topic_index] + topic = props.active_topic col.prop(topic, "is_editable", icon="CHECKMARK" if topic.is_editable else "GREASEPENCIL", icon_only=True) if props.active_topic_index < len(props.topics): - topic = props.topics[props.active_topic_index] + topic = props.active_topic row = layout.row() row.enabled = topic.is_editable row.prop(topic, "description", text="") @@ -93,13 +113,13 @@ class BIM_PT_bcf_metadata(Panel): layout.use_property_decorate = False scene = context.scene - props = bpy.context.scene.BCFProperties + props = scene.BCFProperties if props.active_topic_index >= len(props.topics): layout.label(text="No BCF project is loaded") return - topic = props.topics[props.active_topic_index] + topic = props.active_topic bcfxml = bcfstore.BcfStore.get_bcfxml() bcf_topic = bcfxml.topics[topic.name] @@ -132,12 +152,12 @@ class BIM_PT_bcf_metadata(Panel): row.label(text=f.ifc_spatial_structure_element) row = layout.row(align=True) - row.prop(topic, "file_reference") + row.prop(props, "file_reference") row.operator("bim.select_bcf_header_file", icon="FILE_FOLDER", text="") row = layout.row() - row.prop(topic, "file_ifc_project") + row.prop(props, "file_ifc_project") row = layout.row() - row.prop(topic, "file_ifc_spatial_structure_element") + row.prop(props, "file_ifc_spatial_structure_element") row = layout.row() row.operator("bim.add_bcf_header_file") @@ -149,7 +169,7 @@ class BIM_PT_bcf_metadata(Panel): row.operator("bim.open_uri", icon="URL", text="").uri = link.name row.operator("bim.remove_bcf_reference_link", icon="X", text="").index = index row = layout.row() - row.prop(topic, "reference_link") + row.prop(props, "reference_link") row = layout.row() row.operator("bim.add_bcf_reference_link") @@ -159,7 +179,7 @@ class BIM_PT_bcf_metadata(Panel): row.prop(label, "name", text="") row.operator("bim.remove_bcf_label", icon="X", text="").index = index row = layout.row() - row.prop(topic, "label") + row.prop(props, "label") row = layout.row() row.operator("bim.add_bcf_label") @@ -180,12 +200,12 @@ class BIM_PT_bcf_metadata(Panel): row.operator("bim.remove_bcf_bim_snippet", icon="X", text="") else: row = layout.row(align=True) - row.prop(topic, "bim_snippet_reference") + row.prop(props, "bim_snippet_reference") row.operator("bim.select_bcf_bim_snippet_reference", icon="FILE_FOLDER", text="") row = layout.row() - row.prop(topic, "bim_snippet_type") + row.prop(props, "bim_snippet_type") row = layout.row() - row.prop(topic, "bim_snippet_schema") + row.prop(props, "bim_snippet_schema") row = layout.row() row.operator("bim.add_bcf_bim_snippet") @@ -203,20 +223,26 @@ class BIM_PT_bcf_metadata(Panel): row = box.row(align=True) row.prop(doc, "description") row = layout.row(align=True) - row.prop(topic, "document_reference") + row.prop(props, "document_reference") row.operator("bim.select_bcf_document_reference", icon="FILE_FOLDER", text="") row = layout.row() - row.prop(topic, "document_reference_description") + row.prop(props, "document_reference_description") row = layout.row() row.operator("bim.add_bcf_document_reference") layout.label(text="Related Topics:") for index, related_topic in enumerate(topic.related_topics): - row = layout.row(align=True) - row.operator("bim.view_bcf_topic", text=bcfxml.topics[related_topic.name.lower()].title).topic_guid = related_topic.name - row.operator("bim.remove_bcf_related_topic", icon="X", text="").index = index + try: + row = layout.row(align=True) + op = row.operator( + "bim.view_bcf_topic", text=f"Select {bcfxml.topics[related_topic.name.lower()].title}" + ) + op.topic_guid = related_topic.name + row.operator("bim.remove_bcf_related_topic", icon="X", text="").index = index + except KeyError: + pass row = layout.row() - row.prop(topic, "related_topic") + row.prop(props, "related_topic") row = layout.row() row.operator("bim.add_bcf_related_topic") @@ -236,7 +262,7 @@ class BIM_PT_bcf_comments(Panel): layout.use_property_decorate = False scene = context.scene - props = bpy.context.scene.BCFProperties + props = scene.BCFProperties if props.active_topic_index >= len(props.topics): layout.label(text="No BCF project is loaded") @@ -245,7 +271,7 @@ class BIM_PT_bcf_comments(Panel): row = layout.row() row.prop(props, "comment_text_width") - topic = props.topics[props.active_topic_index] + topic = props.active_topic for comment in topic.comments: box = self.layout.box() @@ -276,8 +302,8 @@ class BIM_PT_bcf_comments(Panel): col.label(text=" ".join(line_words)) row = layout.row() - row.prop(topic, "comment") + row.prop(props, "comment") row = layout.row() - row.prop(topic, "has_related_viewpoint") + row.prop(props, "has_related_viewpoint") row = layout.row() row.operator("bim.add_bcf_comment") diff --git a/src/blenderbim/blenderbim/bim/module/bimtester/__init__.py b/src/blenderbim/blenderbim/bim/module/bimtester/__init__.py index 42feab2dcb..0018a98abe 100644 --- a/src/blenderbim/blenderbim/bim/module/bimtester/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/bimtester/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/bimtester/operator.py b/src/blenderbim/blenderbim/bim/module/bimtester/operator.py index cc3e941402..10dc74f20c 100644 --- a/src/blenderbim/blenderbim/bim/module/bimtester/operator.py +++ b/src/blenderbim/blenderbim/bim/module/bimtester/operator.py @@ -1,8 +1,25 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import tempfile import webbrowser -import ifcopenshell try: import bimtester @@ -12,7 +29,6 @@ except: print("Failed to load BIMTester. Try disabling other add-ons, in particular Blender-OSM. See bug #1318.") from pathlib import Path -from itertools import cycle from blenderbim.bim.ifc import IfcStore @@ -20,6 +36,11 @@ class ExecuteBIMTester(bpy.types.Operator): bl_idname = "bim.execute_bim_tester" bl_label = "Execute BIMTester" + @classmethod + def poll(cls, context): + props = context.scene.BimTesterProperties + return (props.ifc_file or props.should_load_from_memory) and props.feature + def execute(self, context): props = context.scene.BimTesterProperties @@ -51,12 +72,8 @@ class BIMTesterPurge(bpy.types.Operator): bl_label = "Purge Tests" def execute(self, context): - filename = os.path.join( - bpy.context.scene.BimTesterProperties.features_dir, - bpy.context.scene.BimTesterProperties.features_file + ".feature", - ) cwd = os.getcwd() - os.chdir(bpy.context.scene.BimTesterProperties.features_dir) + os.chdir(context.scene.BimTesterProperties.features_dir) bimtester.clean.TestPurger().purge() os.chdir(cwd) return {"FINISHED"} @@ -65,12 +82,13 @@ class BIMTesterPurge(bpy.types.Operator): class SelectFeature(bpy.types.Operator): bl_idname = "bim.select_feature" bl_label = "Select Feature / IDS" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".feature" filter_glob: bpy.props.StringProperty(default="*.feature;*.xml", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.BimTesterProperties.feature = self.filepath + context.scene.BimTesterProperties.feature = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -81,12 +99,13 @@ class SelectFeature(bpy.types.Operator): class SelectSteps(bpy.types.Operator): bl_idname = "bim.select_steps" bl_label = "Select Steps" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".py" filter_glob: bpy.props.StringProperty(default="*.py", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.BimTesterProperties.steps = self.filepath + context.scene.BimTesterProperties.steps = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -97,6 +116,7 @@ class SelectSteps(bpy.types.Operator): class SelectBIMTesterIfcFile(bpy.types.Operator): bl_idname = "bim.select_bimtester_ifc_file" bl_label = "Select BIMTester IFC File" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -117,14 +137,14 @@ class RejectElement(bpy.types.Operator): def execute(self, context): lines = [] self.file = IfcStore.get_file() - for obj in bpy.context.selected_objects: + for obj in context.selected_objects: lines.append( " * The element {} should not exist because {}".format( self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId, - bpy.context.scene.BimTesterProperties.qa_reject_element_reason, + context.scene.BimTesterProperties.qa_reject_element_reason, ) ) - QAHelper.append_to_scenario(lines) + QAHelper.append_to_scenario(lines, context) return {"FINISHED"} @@ -135,12 +155,12 @@ class ApproveClass(bpy.types.Operator): def execute(self, context): lines = [] self.file = IfcStore.get_file() - for obj in bpy.context.selected_objects: + for obj in context.selected_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) lines.append(" * The element {} is an {}".format(element.GlobalId, element.is_a())) - QAHelper.append_to_scenario(lines) + QAHelper.append_to_scenario(lines, context) return {"FINISHED"} @@ -151,27 +171,28 @@ class RejectClass(bpy.types.Operator): def execute(self, context): lines = [] self.file = IfcStore.get_file() - for obj in bpy.context.selected_objects: + for obj in context.selected_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue lines.append( " * The element {} is an {}".format( self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId, - bpy.context.scene.BimTesterProperties.audit_ifc_class, + context.scene.BimTesterProperties.audit_ifc_class, ) ) - QAHelper.append_to_scenario(lines) + QAHelper.append_to_scenario(lines, context) return {"FINISHED"} class SelectAudited(bpy.types.Operator): bl_idname = "bim.select_audited" bl_label = "Select Audited" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): audited_global_ids = [] self.file = IfcStore.get_file() - for filename in Path(bpy.context.scene.BimTesterProperties.features_dir).glob("*.feature"): + for filename in Path(context.scene.BimTesterProperties.features_dir).glob("*.feature"): with open(filename, "r") as feature_file: lines = feature_file.readlines() for line in lines: @@ -179,7 +200,7 @@ class SelectAudited(bpy.types.Operator): for word in words: if self.is_a_global_id(word): audited_global_ids.append(word) - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue if self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId in audited_global_ids: @@ -192,10 +213,10 @@ class SelectAudited(bpy.types.Operator): class QAHelper: @classmethod - def append_to_scenario(cls, lines): + def append_to_scenario(cls, lines, context): filename = os.path.join( - bpy.context.scene.BimTesterProperties.features_dir, - bpy.context.scene.BimTesterProperties.features_file + ".feature", + context.scene.BimTesterProperties.features_dir, + context.scene.BimTesterProperties.features_file + ".feature", ) if os.path.exists(filename + "~"): os.remove(filename + "~") @@ -206,7 +227,7 @@ class QAHelper: for source_line in source: if ( "Scenario: " in source_line - and bpy.context.scene.BimTesterProperties.scenario == source_line.strip()[len("Scenario: ") :] + and context.scene.BimTesterProperties.scenario == source_line.strip()[len("Scenario: ") :] ): is_in_scenario = True elif is_in_scenario: @@ -215,19 +236,3 @@ class QAHelper: is_in_scenario = False destination.write(source_line) os.remove(filename + "~") - - -colour_list = [ - (0.651, 0.81, 0.892, 1), - (0.121, 0.471, 0.706, 1), - (0.699, 0.876, 0.54, 1), - (0.199, 0.629, 0.174, 1), - (0.983, 0.605, 0.602, 1), - (0.89, 0.101, 0.112, 1), - (0.989, 0.751, 0.427, 1), - (0.986, 0.497, 0.1, 1), - (0.792, 0.699, 0.839, 1), - (0.414, 0.239, 0.603, 1), - (0.993, 0.999, 0.6, 1), - (0.693, 0.349, 0.157, 1), -] diff --git a/src/blenderbim/blenderbim/bim/module/bimtester/prop.py b/src/blenderbim/blenderbim/bim/module/bimtester/prop.py index db245767c7..e3b438423b 100644 --- a/src/blenderbim/blenderbim/bim/module/bimtester/prop.py +++ b/src/blenderbim/blenderbim/bim/module/bimtester/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os from pathlib import Path from blenderbim.bim.prop import StrProperty diff --git a/src/blenderbim/blenderbim/bim/module/bimtester/ui.py b/src/blenderbim/blenderbim/bim/module/bimtester/ui.py index b1508ad84f..74e6e853fd 100644 --- a/src/blenderbim/blenderbim/bim/module/bimtester/ui.py +++ b/src/blenderbim/blenderbim/bim/module/bimtester/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel from blenderbim.bim.ifc import IfcStore diff --git a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py index aab371b6fe..1f65558621 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/__init__.py @@ -1,9 +1,25 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui -classes = ( - ui.BIM_PT_boundary, -) +classes = (ui.BIM_PT_boundary,) def register(): diff --git a/src/blenderbim/blenderbim/bim/module/boundary/ui.py b/src/blenderbim/blenderbim/bim/module/boundary/ui.py index 4e0886a054..405f171125 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/ui.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import blenderbim.bim.helper from bpy.types import Panel, UIList diff --git a/src/blenderbim/blenderbim/bim/module/clash/__init__.py b/src/blenderbim/blenderbim/bim/module/clash/__init__.py index 63448c96e3..7fc23861f1 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/clash/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/clash/operator.py b/src/blenderbim/blenderbim/bim/module/clash/operator.py index b6824ace7f..2b2718ee7a 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/operator.py +++ b/src/blenderbim/blenderbim/bim/module/clash/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import json @@ -6,6 +24,7 @@ import logging import numpy as np from mathutils import Matrix from math import radians +from blenderbim.bim.ifc import IfcStore class ExportClashSets(bpy.types.Operator): @@ -23,7 +42,7 @@ class ExportClashSets(bpy.types.Operator): def execute(self, context): self.filepath = bpy.path.ensure_ext(self.filepath, ".json") clash_sets = [] - for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: + for clash_set in context.scene.BIMClashProperties.clash_sets: self.a = [] self.b = [] for ab in ["a", "b"]: @@ -42,6 +61,7 @@ class ExportClashSets(bpy.types.Operator): class ImportClashSets(bpy.types.Operator): bl_idname = "bim.import_clash_sets" bl_label = "Import Clash Sets" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".json" filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -55,7 +75,7 @@ class ImportClashSets(bpy.types.Operator): with open(self.filepath) as f: clash_sets = json.load(f) for clash_set in clash_sets: - new = bpy.context.scene.BIMClashProperties.clash_sets.add() + new = context.scene.BIMClashProperties.clash_sets.add() new.name = clash_set["name"] new.tolerance = clash_set["tolerance"] for clash_source in clash_set["a"]: @@ -77,9 +97,10 @@ class ImportClashSets(bpy.types.Operator): class AddClashSet(bpy.types.Operator): bl_idname = "bim.add_clash_set" bl_label = "Add Clash Set" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - new = bpy.context.scene.BIMClashProperties.clash_sets.add() + new = context.scene.BIMClashProperties.clash_sets.add() new.name = "New Clash Set" new.tolerance = 0.01 return {"FINISHED"} @@ -88,20 +109,22 @@ class AddClashSet(bpy.types.Operator): class RemoveClashSet(bpy.types.Operator): bl_idname = "bim.remove_clash_set" bl_label = "Remove Clash Set" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): - bpy.context.scene.BIMClashProperties.clash_sets.remove(self.index) + context.scene.BIMClashProperties.clash_sets.remove(self.index) return {"FINISHED"} class AddClashSource(bpy.types.Operator): bl_idname = "bim.add_clash_source" bl_label = "Add Clash Source" + bl_options = {"REGISTER", "UNDO"} group: bpy.props.StringProperty() def execute(self, context): - clash_set = bpy.context.scene.BIMClashProperties.clash_sets[bpy.context.scene.BIMClashProperties.active_clash_set_index] + clash_set = context.scene.BIMClashProperties.active_clash_set source = getattr(clash_set, self.group).add() return {"FINISHED"} @@ -109,11 +132,12 @@ class AddClashSource(bpy.types.Operator): class RemoveClashSource(bpy.types.Operator): bl_idname = "bim.remove_clash_source" bl_label = "Remove Clash Source" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() group: bpy.props.StringProperty() def execute(self, context): - clash_set = bpy.context.scene.BIMClashProperties.clash_sets[bpy.context.scene.BIMClashProperties.active_clash_set_index] + clash_set = context.scene.BIMClashProperties.active_clash_set getattr(clash_set, self.group).remove(self.index) return {"FINISHED"} @@ -121,12 +145,14 @@ class RemoveClashSource(bpy.types.Operator): class SelectClashSource(bpy.types.Operator): bl_idname = "bim.select_clash_source" bl_label = "Select Clash Source" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) index: bpy.props.IntProperty() group: bpy.props.StringProperty() def execute(self, context): - clash_set = bpy.context.scene.BIMClashProperties.clash_sets[bpy.context.scene.BIMClashProperties.active_clash_set_index] + clash_set = context.scene.BIMClashProperties.active_clash_set getattr(clash_set, self.group)[self.index].name = self.filepath return {"FINISHED"} @@ -138,10 +164,11 @@ class SelectClashSource(bpy.types.Operator): class SelectClashResults(bpy.types.Operator): bl_idname = "bim.select_clash_results" bl_label = "Select Clash Results" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.BIMClashProperties.clash_results_path = self.filepath + context.scene.BIMClashProperties.clash_results_path = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -152,10 +179,11 @@ class SelectClashResults(bpy.types.Operator): class SelectSmartGroupedClashesPath(bpy.types.Operator): bl_idname = "bim.select_smart_grouped_clashes_path" bl_label = "Select Smart-Grouped Clashes Path" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath + context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -177,42 +205,40 @@ class ExecuteIfcClash(bpy.types.Operator): return {"RUNNING_MODAL"} def execute(self, context): - import ifcclash + from ifcclash import ifcclash - settings = ifcclash.IfcClashSettings() + settings = ifcclash.ClashSettings() if ".json" not in self.filepath: self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf") settings.output = self.filepath settings.logger = logging.getLogger("Clash") settings.logger.setLevel(logging.DEBUG) - ifc_clasher = ifcclash.IfcClasher(settings) + clasher = ifcclash.Clasher(settings) - if bpy.context.scene.BIMClashProperties.should_create_clash_snapshots: + if context.scene.BIMClashProperties.should_create_clash_snapshots: - def get_viewpoint_snapshot(self, viewpoint, mat): + def get_viewpoint_snapshot(viewpoint, mat): camera = bpy.data.objects.get("IFC Clash Camera") if not camera: camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera")) - bpy.context.scene.collection.objects.link(camera) + context.scene.collection.objects.link(camera) camera.matrix_world = Matrix(mat) - bpy.context.scene.camera = camera + context.scene.camera = camera camera.data.angle = radians(60) - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area = next(area for area in context.screen.areas if area.type == "VIEW_3D") area.spaces[0].region_3d.view_perspective = "CAMERA" area.spaces[0].shading.show_xray = True - bpy.context.scene.render.resolution_x = 480 - bpy.context.scene.render.resolution_y = 270 - bpy.context.scene.render.image_settings.file_format = "PNG" - bpy.context.scene.render.filepath = os.path.join( - bpy.context.scene.BIMProperties.data_dir, "snapshot.png" - ) + context.scene.render.resolution_x = 480 + context.scene.render.resolution_y = 270 + context.scene.render.image_settings.file_format = "PNG" + context.scene.render.filepath = os.path.join(context.scene.BIMProperties.data_dir, "snapshot.png") bpy.ops.render.opengl(write_still=True) - return bpy.context.scene.render.filepath + return context.scene.render.filepath - ifcclash.IfcClasher.get_viewpoint_snapshot = get_viewpoint_snapshot + clasher.get_viewpoint_snapshot = get_viewpoint_snapshot - ifc_clasher.clash_sets = [] - for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: + clasher.clash_sets = [] + for clash_set in context.scene.BIMClashProperties.clash_sets: self.a = [] self.b = [] for ab in ["a", "b"]: @@ -222,17 +248,19 @@ class ExecuteIfcClash(bpy.types.Operator): clash_source["selector"] = data.selector clash_source["mode"] = data.mode getattr(self, ab).append(clash_source) - ifc_clasher.clash_sets.append( - {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b} - ) - ifc_clasher.clash() - ifc_clasher.export() + clash_set_data = {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a} + if self.b: + clash_set_data["b"] = self.b + clasher.clash_sets.append(clash_set_data) + clasher.clash() + clasher.export() return {"FINISHED"} class SelectIfcClashResults(bpy.types.Operator): bl_idname = "bim.select_ifc_clash_results" bl_label = "Select IFC Clash Results" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".json" filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -246,9 +274,7 @@ class SelectIfcClashResults(bpy.types.Operator): self.filepath = bpy.path.ensure_ext(self.filepath, ".json") with open(self.filepath) as f: clash_sets = json.load(f) - clash_set_name = bpy.context.scene.BIMClashProperties.clash_sets[ - bpy.context.scene.BIMClashProperties.active_clash_set_index - ].name + clash_set_name = context.scene.BIMClashProperties.active_clash_set.name global_ids = [] for clash_set in clash_sets: if clash_set["name"] != clash_set_name: @@ -258,7 +284,7 @@ class SelectIfcClashResults(bpy.types.Operator): return {"CANCELLED"} for clash in clash_set["clashes"].values(): global_ids.extend([clash["a_global_id"], clash["b_global_id"]]) - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) @@ -270,13 +296,18 @@ class SelectIfcClashResults(bpy.types.Operator): class SmartClashGroup(bpy.types.Operator): bl_idname = "bim.smart_clash_group" bl_label = "Smart Group Clashes" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") + @classmethod + def poll(cls, context): + return context.scene.BIMClashProperties.clash_results_path + def execute(self, context): import ifcclash settings = ifcclash.IfcClashSettings() - self.filepath = bpy.path.ensure_ext(bpy.context.scene.BIMClashProperties.clash_results_path, ".json") + self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json") settings.output = self.filepath settings.logger = logging.getLogger("Clash") settings.logger.setLevel(logging.DEBUG) @@ -286,21 +317,19 @@ class SmartClashGroup(bpy.types.Operator): clash_sets = json.load(f) # execute the smart grouping - save_path = bpy.path.ensure_ext(bpy.context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") + save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") smart_grouped_clashes = ifc_clasher.smart_group_clashes( - clash_sets, bpy.context.scene.BIMClashProperties.smart_clash_grouping_max_distance + clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance ) # save smart_groups to json with open(save_path, "w") as f: f.write(json.dumps(smart_grouped_clashes)) - clash_set_name = bpy.context.scene.BIMClashProperties.clash_sets[ - bpy.context.scene.BIMClashProperties.active_clash_set_index - ].name + clash_set_name = context.scene.BIMClashProperties.active_clash_set.name # Reset the list of smart_clash_groups for the UI - bpy.context.scene.BIMClashProperties.smart_clash_groups.clear() + context.scene.BIMClashProperties.smart_clash_groups.clear() for clash_set, smart_groups in smart_grouped_clashes.items(): # Only select the clashes that correspond to the actively selected IFC Clash Set @@ -308,7 +337,7 @@ class SmartClashGroup(bpy.types.Operator): continue else: for smart_group, global_id_pairs in smart_groups[0].items(): - new_group = bpy.context.scene.BIMClashProperties.smart_clash_groups.add() + new_group = context.scene.BIMClashProperties.smart_clash_groups.add() new_group.number = f"{smart_group}" for pair in global_id_pairs: @@ -322,19 +351,22 @@ class SmartClashGroup(bpy.types.Operator): class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): bl_idname = "bim.load_smart_groups_for_active_clash_set" bl_label = "Load Smart Groups for Active Clash Set" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.scene.BIMClashProperties.active_clash_set def execute(self, context): - smart_groups_path = bpy.path.ensure_ext(bpy.context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") + smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") - clash_set_name = bpy.context.scene.BIMClashProperties.clash_sets[ - bpy.context.scene.BIMClashProperties.active_clash_set_index - ].name + clash_set_name = context.scene.BIMClashProperties.active_clash_set.name with open(smart_groups_path) as f: smart_grouped_clashes = json.load(f) # Reset the list of smart_clash_groups for the UI - bpy.context.scene.BIMClashProperties.smart_clash_groups.clear() + context.scene.BIMClashProperties.smart_clash_groups.clear() for clash_set, smart_groups in smart_grouped_clashes.items(): # Only select the clashes that correspond to the actively selected IFC Clash Set @@ -342,7 +374,7 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): continue else: for smart_group, global_id_pairs in smart_groups[0].items(): - new_group = bpy.context.scene.BIMClashProperties.smart_clash_groups.add() + new_group = context.scene.BIMClashProperties.smart_clash_groups.add() new_group.number = f"{smart_group}" for pair in global_id_pairs: for id in pair: @@ -355,15 +387,19 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): class SelectSmartGroup(bpy.types.Operator): bl_idname = "bim.select_smart_group" bl_label = "Select Smart Group" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group def execute(self, context): + self.file = IfcStore.get_file() # Select smart group in view - selected_smart_group = bpy.context.scene.BIMClashProperties.smart_clash_groups[ - bpy.context.scene.BIMCLashProperties.active_smart_group_index - ] + selected_smart_group = context.scene.BIMClashProperties.active_smart_group # print(selected_smart_group.number) - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) @@ -378,13 +414,13 @@ class SelectSmartGroup(bpy.types.Operator): class BlenderClasher: - def process_clash_set(self): + def process_clash_set(self, context): import collision a_cm = collision.CollisionManager() b_cm = collision.CollisionManager() - self.add_to_cm(a_cm, bpy.context.scene.BIMClashProperties.blender_clash_set_a) - self.add_to_cm(b_cm, bpy.context.scene.BIMClashProperties.blender_clash_set_b) + self.add_to_cm(a_cm, context.scene.BIMClashProperties.blender_clash_set_a, context) + self.add_to_cm(b_cm, context.scene.BIMClashProperties.blender_clash_set_b, context) results = a_cm.in_collision_other(b_cm, return_data=True) if not results[0]: print("No clashes") @@ -397,20 +433,20 @@ class BlenderClasher: print(contact.raw.normal) print(contact.raw.pos) - def add_to_cm(self, cm, object_names): + def add_to_cm(self, cm, object_names, context): import ifcclash for object_name in object_names: name = object_name.name obj = bpy.data.objects[name] - triangulated_mesh = self.triangulate_mesh(obj) + triangulated_mesh = self.triangulate_mesh(obj, context) mesh = ifcclash.Mesh() mesh.vertices = np.array([tuple(obj.matrix_world @ v.co) for v in triangulated_mesh.vertices]) mesh.faces = np.array([tuple(p.vertices) for p in triangulated_mesh.polygons]) cm.add_object(name, mesh) - def triangulate_mesh(self, obj): - mesh = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() + def triangulate_mesh(self, obj, context): + mesh = obj.evaluated_get(context.evaluated_depsgraph_get()).to_mesh() bm = bmesh.new() bm.from_mesh(mesh) bmesh.ops.triangulate(bm, faces=bm.faces) @@ -423,12 +459,12 @@ class BlenderClasher: class SetBlenderClashSetA(bpy.types.Operator): bl_idname = "bim.set_blender_clash_set_a" bl_label = "Set Blender Clash Set A" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - while len(bpy.context.scene.BIMClashProperties.blender_clash_set_a) > 0: - bpy.context.scene.BIMClashProperties.blender_clash_set_a.remove(0) - for obj in bpy.context.selected_objects: - new = bpy.context.scene.BIMClashProperties.blender_clash_set_a.add() + context.scene.BIMClashProperties.blender_clash_set_a.clear() + for obj in context.selected_objects: + new = context.scene.BIMClashProperties.blender_clash_set_a.add() new.name = obj.name return {"FINISHED"} @@ -436,12 +472,12 @@ class SetBlenderClashSetA(bpy.types.Operator): class SetBlenderClashSetB(bpy.types.Operator): bl_idname = "bim.set_blender_clash_set_b" bl_label = "Set Blender Clash Set B" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - while len(bpy.context.scene.BIMClashProperties.blender_clash_set_b) > 0: - bpy.context.scene.BIMClashProperties.blender_clash_set_b.remove(0) - for obj in bpy.context.selected_objects: - new = bpy.context.scene.BIMClashProperties.blender_clash_set_b.add() + context.scene.BIMClashProperties.blender_clash_set_b.clear() + for obj in context.selected_objects: + new = context.scene.BIMClashProperties.blender_clash_set_b.add() new.name = obj.name return {"FINISHED"} @@ -452,5 +488,5 @@ class ExecuteBlenderClash(bpy.types.Operator): def execute(self, context): blender_clasher = BlenderClasher() - blender_clasher.process_clash_set() + blender_clasher.process_clash_set(context) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/clash/prop.py b/src/blenderbim/blenderbim/bim/module/clash/prop.py index ba685b64d3..f057dcd70c 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/prop.py +++ b/src/blenderbim/blenderbim/bim/module/clash/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup @@ -41,7 +59,7 @@ class BIMClashProperties(PropertyGroup): blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty) clash_sets: CollectionProperty(name="Clash Sets", type=ClashSet) - should_create_clash_snapshots: BoolProperty(name="Create Snapshots", default=True) + should_create_clash_snapshots: BoolProperty(name="Create Snapshots", default=False) clash_results_path: StringProperty(name="Clash Results Path") smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path") active_clash_set_index: IntProperty(name="Active Clash Set Index") @@ -50,3 +68,15 @@ class BIMClashProperties(PropertyGroup): smart_clash_grouping_max_distance: IntProperty( name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10 ) + + @property + def active_clash_set(self): + if not self.clash_sets: + return None + return self.clash_sets[self.active_clash_set_index] + + @property + def active_smart_group(self): + if not self.smart_clash_groups: + return None + return self.smart_clash_groups[self.active_smart_group_index] diff --git a/src/blenderbim/blenderbim/bim/module/clash/ui.py b/src/blenderbim/blenderbim/bim/module/clash/ui.py index a6a37a7680..bc2e7994c1 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/ui.py +++ b/src/blenderbim/blenderbim/bim/module/clash/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Panel @@ -38,7 +56,7 @@ class BIM_PT_ifcclash(Panel): layout.template_list("BIM_UL_clash_sets", "", props, "clash_sets", props, "active_clash_set_index") if props.active_clash_set_index < len(props.clash_sets): - clash_set = props.clash_sets[props.active_clash_set_index] + clash_set = props.active_clash_set row = layout.row(align=True) row.prop(clash_set, "name") diff --git a/src/blenderbim/blenderbim/bim/module/classification/__init__.py b/src/blenderbim/blenderbim/bim/module/classification/__init__.py index f486b4e3fa..64d2a29a5a 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/classification/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index 55c5b4d9b6..1db448c9c7 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell.api @@ -50,14 +68,14 @@ class EnableEditingClassification(bpy.types.Operator): def execute(self, context): props = context.scene.BIMClassificationProperties - while len(props.classification_attributes) > 0: - props.classification_attributes.remove(0) + props.classification_attributes.clear() classification_data = Data.classifications[self.classification] for attribute in IfcStore.get_schema().declaration_by_name("IfcClassification").all_attributes(): new = props.classification_attributes.add() new.name = attribute.name() new.is_null = classification_data[attribute.name()] is None new.is_optional = attribute.optional() + new.data_type = "string" if attribute.name() == "ReferenceTokens": new.string_value = "" if new.is_null else json.dumps(classification_data[attribute.name()]) else: @@ -133,10 +151,9 @@ class EnableEditingClassificationReference(bpy.types.Operator): obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object props = obj.BIMClassificationReferenceProperties - while len(props.reference_attributes) > 0: - props.reference_attributes.remove(0) + props.reference_attributes.clear() reference_data = Data.references[self.reference] for attribute in IfcStore.get_schema().declaration_by_name("IfcClassificationReference").all_attributes(): if attribute.name() == "ReferencedSource": @@ -145,6 +162,7 @@ class EnableEditingClassificationReference(bpy.types.Operator): new.name = attribute.name() new.is_null = reference_data[attribute.name()] is None new.is_optional = attribute.optional() + new.data_type = "string" new.string_value = "" if new.is_null else reference_data[attribute.name()] props.active_reference_id = self.reference return {"FINISHED"} @@ -157,7 +175,7 @@ class DisableEditingClassificationReference(bpy.types.Operator): obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object obj.BIMClassificationReferenceProperties.active_reference_id = 0 return {"FINISHED"} @@ -173,7 +191,7 @@ class RemoveClassificationReference(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "classification.remove_reference", @@ -198,7 +216,7 @@ class EditClassificationReference(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object props = obj.BIMClassificationReferenceProperties attributes = {} for attribute in props.reference_attributes: @@ -228,7 +246,7 @@ class AddClassificationReference(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() classification = None diff --git a/src/blenderbim/blenderbim/bim/module/classification/prop.py b/src/blenderbim/blenderbim/bim/module/classification/prop.py index 932335a3a3..4b5c091763 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/prop.py +++ b/src/blenderbim/blenderbim/bim/module/classification/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from ifcopenshell.api.classification.data import Data @@ -37,8 +55,7 @@ def updateClassification(self, context): def getReferences(self, context, parent_id=None): props = context.scene.BIMClassificationProperties - while len(props.available_library_references) > 0: - props.available_library_references.remove(0) + props.available_library_references.clear() for reference in Data.library_file.by_id(parent_id).HasReferences: new = props.available_library_references.add() new.identification = reference.Identification or "" diff --git a/src/blenderbim/blenderbim/bim/module/classification/ui.py b/src/blenderbim/blenderbim/bim/module/classification/ui.py index 032a7cd022..1a03ebf3a5 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/ui.py +++ b/src/blenderbim/blenderbim/bim/module/classification/ui.py @@ -1,3 +1,22 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.bim.module.classification.prop as classification_prop from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.classification.data import Data @@ -41,7 +60,7 @@ class BIM_PT_classifications(Panel): row = self.layout.row(align=True) row.prop(self.props.classification_attributes.get("Name"), "string_value", text="", icon="ASSET_MANAGER") row.operator("bim.edit_classification", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_classification", text="", icon="X") + row.operator("bim.disable_editing_classification", text="", icon="CANCEL") for attribute in self.props.classification_attributes: if attribute.name == "Name": @@ -85,7 +104,7 @@ class BIM_PT_classification_references(Panel): if self.oprops.ifc_definition_id not in Data.products: Data.load(IfcStore.get_file(), self.oprops.ifc_definition_id) - self.draw_add_ui() + self.draw_add_ui(context) reference_ids = Data.products[self.oprops.ifc_definition_id] if not reference_ids: @@ -99,8 +118,8 @@ class BIM_PT_classification_references(Panel): else: self.draw_ui(reference_id, reference) - def draw_add_ui(self): - if not self.sprops.available_classifications: + def draw_add_ui(self, context): + if not classification_prop.getClassifications(self.sprops, context): return name = Data.library_classifications[int(self.sprops.available_classifications)] @@ -127,7 +146,7 @@ class BIM_PT_classification_references(Panel): row = self.layout.row(align=True) row.prop(self.props.reference_attributes.get("Name"), "string_value", text="", icon="ASSET_MANAGER") row.operator("bim.edit_classification_reference", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_classification_reference", text="", icon="X") + row.operator("bim.disable_editing_classification_reference", text="", icon="CANCEL") for attribute in self.props.reference_attributes: if attribute.name == "Name": diff --git a/src/blenderbim/blenderbim/bim/module/cobie/__init__.py b/src/blenderbim/blenderbim/bim/module/cobie/__init__.py index 46ec55b5b6..a1ad500ae2 100644 --- a/src/blenderbim/blenderbim/bim/module/cobie/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cobie/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/cobie/operator.py b/src/blenderbim/blenderbim/bim/module/cobie/operator.py index 9e12c090be..3f1a512116 100644 --- a/src/blenderbim/blenderbim/bim/module/cobie/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cobie/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import os import logging @@ -11,10 +29,11 @@ from blenderbim.bim.ifc import IfcStore class SelectCobieIfcFile(bpy.types.Operator): bl_idname = "bim.select_cobie_ifc_file" bl_label = "Select COBie IFC File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.COBieProperties.cobie_ifc_file = self.filepath + context.scene.COBieProperties.cobie_ifc_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -25,10 +44,11 @@ class SelectCobieIfcFile(bpy.types.Operator): class SelectCobieJsonFile(bpy.types.Operator): bl_idname = "bim.select_cobie_json_file" bl_label = "Select COBie JSON File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.COBieProperties.cobie_json_file = self.filepath + context.scene.COBieProperties.cobie_json_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -41,15 +61,21 @@ class ExecuteIfcCobie(bpy.types.Operator): bl_label = "Execute IFCCOBie" file_format: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + props = context.scene.COBieProperties + return props.should_load_from_memory or props.cobie_ifc_file + def execute(self, context): from cobie import IfcCobieParser - props = bpy.context.scene.COBieProperties - - output_dir = os.path.dirname(props.cobie_ifc_file) - + + props = context.scene.COBieProperties + if props.should_load_from_memory: output_dir = tempfile.gettempdir() - + else: + output_dir = os.path.dirname(props.cobie_ifc_file) + output = os.path.join(output_dir, "output") logger = logging.getLogger("IFCtoCOBie") fh = logging.FileHandler(os.path.join(output_dir, "cobie.log")) @@ -66,10 +92,10 @@ class ExecuteIfcCobie(bpy.types.Operator): parser = IfcCobieParser(logger, selector) ifc_file = IfcStore.get_file() - + if not (ifc_file and props.should_load_from_memory): ifc_file = props.cobie_ifc_file - + parser.parse( ifc_file, props.cobie_types, @@ -96,4 +122,3 @@ class ExecuteIfcCobie(bpy.types.Operator): webbrowser.open("file://" + output_dir) webbrowser.open("file://" + output_dir + "/cobie.log") return {"FINISHED"} - diff --git a/src/blenderbim/blenderbim/bim/module/cobie/prop.py b/src/blenderbim/blenderbim/bim/module/cobie/prop.py index 095622298f..567a0f9960 100644 --- a/src/blenderbim/blenderbim/bim/module/cobie/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cobie/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import PropertyGroup from bpy.props import ( @@ -18,4 +36,3 @@ class COBieProperties(PropertyGroup): cobie_components: StringProperty(default=".COBie", name="COBie Components") cobie_json_file: StringProperty(default="", name="COBie JSON File") should_load_from_memory: BoolProperty(default=False, name="Load from Memory") - diff --git a/src/blenderbim/blenderbim/bim/module/cobie/ui.py b/src/blenderbim/blenderbim/bim/module/cobie/ui.py index cbce5b033e..2ae0179a53 100644 --- a/src/blenderbim/blenderbim/bim/module/cobie/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cobie/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel from blenderbim.bim.ifc import IfcStore @@ -17,11 +35,10 @@ class BIM_PT_cobie(Panel): scene = context.scene props = scene.COBieProperties - - if IfcStore.get_file(): + if IfcStore.get_file(): row = layout.row() row.prop(props, "should_load_from_memory") - + if not IfcStore.get_file() or not props.should_load_from_memory: row = layout.row(align=True) row.prop(props, "cobie_ifc_file") diff --git a/src/blenderbim/blenderbim/bim/module/constraint/__init__.py b/src/blenderbim/blenderbim/bim/module/constraint/__init__.py index 190ce3fe12..97c7dc78c6 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/constraint/operator.py b/src/blenderbim/blenderbim/bim/module/constraint/operator.py index 100b974e2a..e7accd3340 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/operator.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/operator.py @@ -1,7 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell.api import ifcopenshell.util.attribute +import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.constraint.data import Data @@ -13,8 +32,7 @@ class LoadObjectives(bpy.types.Operator): def execute(self, context): props = context.scene.BIMConstraintProperties - while len(props.constraints) > 0: - props.constraints.remove(0) + props.constraints.clear() for constraint_id, constraint in Data.objectives.items(): new = props.constraints.add() new.name = constraint["Name"] or "Unnamed" @@ -43,27 +61,12 @@ class EnableEditingConstraint(bpy.types.Operator): def execute(self, context): props = context.scene.BIMConstraintProperties - while len(props.constraint_attributes) > 0: - props.constraint_attributes.remove(0) + props.constraint_attributes.clear() if props.is_editing == "IfcObjective": data = Data.objectives[self.constraint] - for attribute in IfcStore.get_schema().declaration_by_name(props.is_editing).all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity" or (isinstance(data_type, tuple) and "entity" in ".".join(data_type)): - continue - new = props.constraint_attributes.add() - new.name = attribute.name() - new.is_null = data[attribute.name()] is None - new.is_optional = attribute.optional() - new.data_type = data_type - if data_type == "string": - new.string_value = "" if new.is_null else data[attribute.name()] - elif data_type == "enum": - new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) - if data[attribute.name()]: - new.enum_value = data[attribute.name()] + blenderbim.bim.helper.import_attributes(props.is_editing, props.constraint_attributes, data) props.active_constraint_id = self.constraint return {"FINISHED"} @@ -183,17 +186,21 @@ class AssignConstraint(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() - ifcopenshell.api.run( - "constraint.assign_constraint", - self.file, - **{ - "product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), - "constraint": self.file.by_id(self.constraint), - } - ) - Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects + for obj in objs: + obj_id = obj.BIMObjectProperties.ifc_definition_id + if not obj_id: + continue + ifcopenshell.api.run( + "constraint.assign_constraint", + self.file, + **{ + "product": self.file.by_id(obj_id), + "constraint": self.file.by_id(self.constraint), + } + ) + Data.load(self.file, obj_id) return {"FINISHED"} @@ -208,15 +215,19 @@ class UnassignConstraint(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() - ifcopenshell.api.run( - "constraint.unassign_constraint", - self.file, - **{ - "product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), - "constraint": self.file.by_id(self.constraint), - } - ) - Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects + for obj in objs: + obj_id = obj.BIMObjectProperties.ifc_definition_id + if not obj_id: + continue + ifcopenshell.api.run( + "constraint.unassign_constraint", + self.file, + **{ + "product": self.file.by_id(obj_id), + "constraint": self.file.by_id(self.constraint), + } + ) + Data.load(self.file, obj_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/constraint/prop.py b/src/blenderbim/blenderbim/bim/module/constraint/prop.py index b4474db3d1..aea987c4a7 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/prop.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup diff --git a/src/blenderbim/blenderbim/bim/module/constraint/ui.py b/src/blenderbim/blenderbim/bim/module/constraint/ui.py index 2a0ae2d6cd..27aced601b 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/ui.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/ui.py @@ -1,5 +1,24 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.constraint.data import Data @@ -44,14 +63,7 @@ class BIM_PT_constraints(Panel): self.draw_editable_ui(context) def draw_editable_ui(self, context): - for attribute in self.props.constraint_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.constraint_attributes, self.layout) class BIM_PT_object_constraints(Panel): @@ -92,7 +104,7 @@ class BIM_PT_object_constraints(Panel): constraint = Data.objectives[constraint_id] icon = "LIGHT" except: - pass # Metric not implemented + pass # Metric not implemented row = self.layout.row(align=True) row.label(text=constraint.get("Name") or "Unnamed") row.operator("bim.unassign_constraint", text="", icon="X").constraint = constraint_id @@ -102,7 +114,7 @@ class BIM_PT_object_constraints(Panel): row = self.layout.row(align=True) icon = "LIGHT" if self.props.is_adding == "IfcObjective" else "FILE_HIDDEN" row.label(text="Adding {}".format(self.props.is_adding), icon=icon) - row.operator("bim.disable_assigning_constraint", text="", icon="X") + row.operator("bim.disable_assigning_constraint", text="", icon="CANCEL") self.layout.template_list( "BIM_UL_object_constraints", "", @@ -125,7 +137,7 @@ class BIM_UL_constraints(UIList): if context.scene.BIMConstraintProperties.active_constraint_id == item.ifc_definition_id: if context.scene.BIMConstraintProperties.is_editing == "IfcObjective": row.operator("bim.edit_objective", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_constraint", text="", icon="X") + row.operator("bim.disable_editing_constraint", text="", icon="CANCEL") elif context.scene.BIMConstraintProperties.active_constraint_id: row.operator("bim.remove_constraint", text="", icon="X").constraint = item.ifc_definition_id else: diff --git a/src/blenderbim/blenderbim/bim/module/context/__init__.py b/src/blenderbim/blenderbim/bim/module/context/__init__.py index fa07da07e8..7577e1d78d 100644 --- a/src/blenderbim/blenderbim/bim/module/context/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/context/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, operator diff --git a/src/blenderbim/blenderbim/bim/module/context/operator.py b/src/blenderbim/blenderbim/bim/module/context/operator.py index 11096912d1..9417865eb8 100644 --- a/src/blenderbim/blenderbim/bim/module/context/operator.py +++ b/src/blenderbim/blenderbim/bim/module/context/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api from blenderbim.bim.ifc import IfcStore @@ -21,9 +39,9 @@ class AddSubcontext(bpy.types.Operator): "context.add_context", self.file, **{ - "context": self.context or bpy.context.scene.BIMProperties.available_contexts, - "subcontext": self.subcontext or bpy.context.scene.BIMProperties.available_subcontexts, - "target_view": self.target_view or bpy.context.scene.BIMProperties.available_target_views, + "context": self.context or context.scene.BIMProperties.available_contexts, + "subcontext": self.subcontext or context.scene.BIMProperties.available_subcontexts, + "target_view": self.target_view or context.scene.BIMProperties.available_target_views, }, ) Data.load(IfcStore.get_file()) diff --git a/src/blenderbim/blenderbim/bim/module/context/ui.py b/src/blenderbim/blenderbim/bim/module/context/ui.py index 2573cfa80f..3cdc87be29 100644 --- a/src/blenderbim/blenderbim/bim/module/context/ui.py +++ b/src/blenderbim/blenderbim/bim/module/context/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel from ifcopenshell.api.context.data import Data from blenderbim.bim.ifc import IfcStore diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 5a5abd7578..c431d481b1 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -7,7 +25,8 @@ classes = ( operator.EditCostSchedule, operator.EditCostItem, operator.EditCostItemQuantity, - operator.EditCostValue, + operator.EditCostItemValue, + operator.EditCostItemValueFormula, operator.EnableEditingCostSchedule, operator.EnableEditingCostItems, operator.EnableEditingCostItem, @@ -15,17 +34,22 @@ classes = ( operator.EnableEditingCostItemQuantity, operator.EnableEditingCostItemValues, operator.EnableEditingCostItemValue, + operator.EnableEditingCostItemValueFormula, operator.DisableEditingCostItem, operator.DisableEditingCostSchedule, operator.DisableEditingCostItemQuantity, operator.DisableEditingCostItemValue, + operator.AddCostColumn, + operator.RemoveCostColumn, operator.AddCostItem, operator.AddSummaryCostItem, operator.ExpandCostItem, operator.ContractCostItem, operator.RemoveCostItem, - operator.AssignCostItemProduct, - operator.UnassignCostItemProduct, + operator.AssignCostItemType, + operator.UnassignCostItemType, + operator.AssignCostItemQuantity, + operator.UnassignCostItemQuantity, operator.AddCostItemQuantity, operator.RemoveCostItemQuantity, operator.AddCostValue, @@ -33,16 +57,39 @@ classes = ( operator.CopyCostItemValues, operator.SelectCostItemProducts, operator.SelectCostScheduleProducts, + operator.ImportCostScheduleCsv, + operator.LoadCostItemQuantities, + operator.LoadCostItemTypes, + operator.AssignCostValue, + operator.LoadScheduleOfRates, + operator.ExpandCostItemRate, + operator.ContractCostItemRate, + operator.CalculateCostItemResourceValue, prop.CostItem, + prop.CostItemQuantity, + prop.CostItemType, prop.BIMCostProperties, ui.BIM_PT_cost_schedules, + ui.BIM_PT_cost_item_quantities, + ui.BIM_PT_cost_item_types, + ui.BIM_PT_cost_item_rates, ui.BIM_UL_cost_items, + ui.BIM_UL_cost_columns, + ui.BIM_UL_cost_item_quantities, + ui.BIM_UL_cost_item_types, + ui.BIM_UL_cost_item_rates, ) +def menu_func_import(self, context): + self.layout.operator(operator.ImportCostScheduleCsv.bl_idname, text="Cost Schedule (.csv)") + + def register(): bpy.types.Scene.BIMCostProperties = bpy.props.PointerProperty(type=prop.BIMCostProperties) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) def unregister(): del bpy.types.Scene.BIMCostProperties + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index be1475ac60..4d08f98010 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -1,18 +1,44 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import json +import time import ifcopenshell.api import blenderbim.bim.helper from blenderbim.bim.module.cost.prop import purge from blenderbim.bim.ifc import IfcStore +from bpy_extras.io_utils import ImportHelper from ifcopenshell.api.cost.data import Data +from ifcopenshell.api.unit.data import Data as UnitData +from ifcopenshell.api.resource.data import Data as ResourceData class AddCostSchedule(bpy.types.Operator): bl_idname = "bim.add_cost_schedule" bl_label = "Add Cost Schedule" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run("cost.add_cost_schedule", IfcStore.get_file()) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -21,8 +47,12 @@ class AddCostSchedule(bpy.types.Operator): class EditCostSchedule(bpy.types.Operator): bl_idname = "bim.edit_cost_schedule" bl_label = "Edit Cost Schedule" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMCostProperties attributes = blenderbim.bim.helper.export_attributes(props.cost_schedule_attributes) self.file = IfcStore.get_file() @@ -32,6 +62,7 @@ class EditCostSchedule(bpy.types.Operator): **{"cost_schedule": self.file.by_id(props.active_cost_schedule_id), "attributes": attributes}, ) Data.load(IfcStore.get_file()) + purge() bpy.ops.bim.disable_editing_cost_schedule() return {"FINISHED"} @@ -39,9 +70,13 @@ class EditCostSchedule(bpy.types.Operator): class RemoveCostSchedule(bpy.types.Operator): bl_idname = "bim.remove_cost_schedule" bl_label = "Remove Cost Schedule" + bl_options = {"REGISTER", "UNDO"} cost_schedule: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run( "cost.remove_cost_schedule", IfcStore.get_file(), @@ -54,13 +89,13 @@ class RemoveCostSchedule(bpy.types.Operator): class EnableEditingCostSchedule(bpy.types.Operator): bl_idname = "bim.enable_editing_cost_schedule" bl_label = "Enable Editing Cost Schedule" + bl_options = {"REGISTER", "UNDO"} cost_schedule: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMCostProperties self.props.active_cost_schedule_id = self.cost_schedule - while len(self.props.cost_schedule_attributes) > 0: - self.props.cost_schedule_attributes.remove(0) + self.props.cost_schedule_attributes.clear() self.enable_editing_cost_schedule() self.props.is_editing = "COST_SCHEDULE" return {"FINISHED"} @@ -80,41 +115,46 @@ class EnableEditingCostSchedule(bpy.types.Operator): class EnableEditingCostItems(bpy.types.Operator): bl_idname = "bim.enable_editing_cost_items" bl_label = "Enable Editing Cost Items" + bl_options = {"REGISTER", "UNDO"} cost_schedule: bpy.props.IntProperty() def execute(self, context): if context.preferences.addons["blenderbim"].preferences.should_play_chaching_sound: - # lol - # TODO: make pitch higher as costs rise - try: - import aud - - device = aud.Device() - # chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/ - sound = aud.Sound(os.path.join(context.scene.BIMProperties.data_dir, "chaching.mp3")) - handle = device.play(sound) - sound_buffered = aud.Sound.buffer(sound) - handle_buffered = device.play(sound_buffered) - handle.stop() - handle_buffered.stop() - except: - pass # ah well + self.play_chaching_sound() # lol self.props = context.scene.BIMCostProperties + self.props.is_cost_update_enabled = False self.props.active_cost_schedule_id = self.cost_schedule - while len(self.props.cost_items) > 0: - self.props.cost_items.remove(0) + self.props.cost_items.clear() self.contracted_cost_items = json.loads(self.props.contracted_cost_items) for related_object_id in Data.cost_schedules[self.cost_schedule]["Controls"]: self.create_new_cost_item_li(related_object_id, 0) self.props.is_editing = "COST_ITEMS" + self.props.is_cost_update_enabled = True return {"FINISHED"} + def play_chaching_sound(self): + # TODO: make pitch higher as costs rise + try: + import aud + + device = aud.Device() + # chaching.mp3 is by Lucish_ CC-BY-3.0 https://freesound.org/people/Lucish_/sounds/554841/ + sound = aud.Sound(os.path.join(context.scene.BIMProperties.data_dir, "chaching.mp3")) + handle = device.play(sound) + sound_buffered = aud.Sound.buffer(sound) + handle_buffered = device.play(sound_buffered) + handle.stop() + handle_buffered.stop() + except: + pass # ah well + def create_new_cost_item_li(self, related_object_id, level_index): cost_item = Data.cost_items[related_object_id] new = self.props.cost_items.add() new.ifc_definition_id = related_object_id new.name = cost_item["Name"] or "Unnamed" + new.identification = cost_item["Identification"] or "XXX" new.is_expanded = related_object_id not in self.contracted_cost_items new.level_index = level_index if cost_item["IsNestedBy"]: @@ -123,12 +163,11 @@ class EnableEditingCostItems(bpy.types.Operator): for related_object_id in cost_item["IsNestedBy"]: self.create_new_cost_item_li(related_object_id, level_index + 1) - return {"FINISHED"} - class DisableEditingCostSchedule(bpy.types.Operator): bl_idname = "bim.disable_editing_cost_schedule" bl_label = "Disable Editing Cost Schedule" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMCostProperties.active_cost_schedule_id = 0 @@ -138,9 +177,13 @@ class DisableEditingCostSchedule(bpy.types.Operator): class AddSummaryCostItem(bpy.types.Operator): bl_idname = "bim.add_summary_cost_item" bl_label = "Add Cost Item" + bl_options = {"REGISTER", "UNDO"} cost_schedule: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMCostProperties self.file = IfcStore.get_file() ifcopenshell.api.run("cost.add_cost_item", self.file, **{"cost_schedule": self.file.by_id(self.cost_schedule)}) @@ -152,9 +195,13 @@ class AddSummaryCostItem(bpy.types.Operator): class AddCostItem(bpy.types.Operator): bl_idname = "bim.add_cost_item" bl_label = "Add Cost Item" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMCostProperties self.file = IfcStore.get_file() ifcopenshell.api.run("cost.add_cost_item", self.file, **{"cost_item": self.file.by_id(self.cost_item)}) @@ -166,6 +213,7 @@ class AddCostItem(bpy.types.Operator): class ExpandCostItem(bpy.types.Operator): bl_idname = "bim.expand_cost_item" bl_label = "Expand Cost Item" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() def execute(self, context): @@ -181,6 +229,7 @@ class ExpandCostItem(bpy.types.Operator): class ContractCostItem(bpy.types.Operator): bl_idname = "bim.contract_cost_item" bl_label = "Contract Cost Item" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() def execute(self, context): @@ -196,9 +245,13 @@ class ContractCostItem(bpy.types.Operator): class RemoveCostItem(bpy.types.Operator): bl_idname = "bim.remove_cost_item" bl_label = "Remove Cost item" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMCostProperties self.file = IfcStore.get_file() ifcopenshell.api.run( @@ -218,12 +271,12 @@ class RemoveCostItem(bpy.types.Operator): class EnableEditingCostItem(bpy.types.Operator): bl_idname = "bim.enable_editing_cost_item" bl_label = "Enable Editing Cost Item" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() def execute(self, context): props = context.scene.BIMCostProperties - while len(props.cost_item_attributes) > 0: - props.cost_item_attributes.remove(0) + props.cost_item_attributes.clear() data = Data.cost_items[self.cost_item] blenderbim.bim.helper.import_attributes("IfcCostItem", props.cost_item_attributes, data) @@ -235,6 +288,7 @@ class EnableEditingCostItem(bpy.types.Operator): class DisableEditingCostItem(bpy.types.Operator): bl_idname = "bim.disable_editing_cost_item" bl_label = "Disable Editing Cost Item" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMCostProperties.active_cost_item_id = 0 @@ -244,8 +298,12 @@ class DisableEditingCostItem(bpy.types.Operator): class EditCostItem(bpy.types.Operator): bl_idname = "bim.edit_cost_item" bl_label = "Edit Cost Item" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMCostProperties attributes = blenderbim.bim.helper.export_attributes(props.cost_item_attributes) self.file = IfcStore.get_file() @@ -260,59 +318,145 @@ class EditCostItem(bpy.types.Operator): return {"FINISHED"} -class AssignCostItemProduct(bpy.types.Operator): - bl_idname = "bim.assign_cost_item_product" - bl_label = "Assign Control" +class AssignCostItemType(bpy.types.Operator): + bl_idname = "bim.assign_cost_item_type" + bl_label = "Assign Cost Item Type Product" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() - related_object: bpy.props.StringProperty() + prop_name: bpy.props.StringProperty() def execute(self, context): - related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects - ) + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() - ifcopenshell.api.run( - "cost.assign_cost_item_product", - self.file, - cost_item=self.file.by_id(self.cost_item), - products=[ - self.file.by_id(o.BIMObjectProperties.ifc_definition_id) - for o in related_objects - if o.BIMObjectProperties.ifc_definition_id - ], - ) + cost_item = self.file.by_id(self.cost_item) + for obj in context.selected_objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + if product.is_a("IfcTypeProduct"): + ifcopenshell.api.run( + "control.assign_control", self.file, relating_control=cost_item, related_object=product + ) Data.load(self.file) + bpy.ops.bim.load_cost_item_types() return {"FINISHED"} -class UnassignCostItemProduct(bpy.types.Operator): - bl_idname = "bim.unassign_cost_item_product" - bl_label = "Unassign Control" +class UnassignCostItemType(bpy.types.Operator): + bl_idname = "bim.unassign_cost_item_type" + bl_label = "Unassign Cost Item Type" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() - related_object: bpy.props.StringProperty() + related_object: bpy.props.IntProperty() def execute(self, context): - related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects - ) + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() + cost_item = self.file.by_id(self.cost_item) + if self.related_object: + products = [self.file.by_id(self.related_object)] + else: + for obj in context.selected_objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + if product.is_a("IfcTypeProduct"): + products.append(product) + for product in products: + ifcopenshell.api.run( + "control.unassign_control", self.file, relating_control=cost_item, related_object=product + ) + Data.load(self.file) + bpy.ops.bim.load_cost_item_types() + return {"FINISHED"} + + +class AssignCostItemQuantity(bpy.types.Operator): + bl_idname = "bim.assign_cost_item_quantity" + bl_label = "Assign Cost Item Quantity" + bl_options = {"REGISTER", "UNDO"} + cost_item: bpy.props.IntProperty() + related_object_type: bpy.props.StringProperty() + prop_name: bpy.props.StringProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + if self.related_object_type == "PRODUCT": + products = [ + self.file.by_id(o.BIMObjectProperties.ifc_definition_id) + for o in context.selected_objects + if o.BIMObjectProperties.ifc_definition_id + ] + elif self.related_object_type == "PROCESS": + products = [ + self.file.by_id( + context.scene.BIMTaskTreeProperties.tasks[ + context.scene.BIMWorkScheduleProperties.active_task_index + ].ifc_definition_id + ) + ] + elif self.related_object_type == "RESOURCE": + products = [ + self.file.by_id( + context.scene.BIMResourceTreeProperties.resources[ + context.scene.BIMResourceProperties.active_resource_index + ].ifc_definition_id + ) + ] ifcopenshell.api.run( - "cost.unassign_cost_item_product", + "cost.assign_cost_item_quantity", self.file, cost_item=self.file.by_id(self.cost_item), - products=[ - self.file.by_id(o.BIMObjectProperties.ifc_definition_id) - for o in related_objects - if o.BIMObjectProperties.ifc_definition_id - ], + products=products, + prop_name=self.prop_name, ) Data.load(self.file) + bpy.ops.bim.load_cost_item_quantities() + return {"FINISHED"} + + +class UnassignCostItemQuantity(bpy.types.Operator): + bl_idname = "bim.unassign_cost_item_quantity" + bl_label = "Unassign Cost Item Quantity" + bl_options = {"REGISTER", "UNDO"} + cost_item: bpy.props.IntProperty() + related_object: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + if self.related_object: + products = [self.file.by_id(self.related_object)] + else: + products = [ + self.file.by_id(o.BIMObjectProperties.ifc_definition_id) + for o in context.selected_objects + if o.BIMObjectProperties.ifc_definition_id + ] + ifcopenshell.api.run( + "cost.unassign_cost_item_quantity", + self.file, + cost_item=self.file.by_id(self.cost_item), + products=products, + ) + Data.load(self.file) + bpy.ops.bim.load_cost_item_quantities() return {"FINISHED"} class EnableEditingCostItemQuantities(bpy.types.Operator): bl_idname = "bim.enable_editing_cost_item_quantities" bl_label = "Enable Editing Cost Item Quantities" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() def execute(self, context): @@ -326,6 +470,7 @@ class EnableEditingCostItemQuantities(bpy.types.Operator): class EnableEditingCostItemValues(bpy.types.Operator): bl_idname = "bim.enable_editing_cost_item_values" bl_label = "Enable Editing Cost Item Values" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() def execute(self, context): @@ -339,27 +484,20 @@ class EnableEditingCostItemValues(bpy.types.Operator): class AddCostItemQuantity(bpy.types.Operator): bl_idname = "bim.add_cost_item_quantity" bl_label = "Add Cost Item Quantity" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() ifc_class: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() self.props = context.scene.BIMCostProperties - if self.props.quantity_types == "QTO": - self.add_quantities_from_qto_filter() - else: - self.add_manual_quantity() + self.add_manual_quantity() Data.load(self.file) return {"FINISHED"} - def add_quantities_from_qto_filter(self): - ifcopenshell.api.run( - "cost.assign_cost_item_product_quantities", - self.file, - cost_item=self.file.by_id(self.cost_item), - prop_name=self.props.quantity_names, - ) - def add_manual_quantity(self): ifcopenshell.api.run( "cost.add_cost_item_quantity", @@ -371,11 +509,15 @@ class AddCostItemQuantity(bpy.types.Operator): class RemoveCostItemQuantity(bpy.types.Operator): bl_idname = "bim.remove_cost_item_quantity" - bl_label = "Add Cost Item Quantity" + bl_label = "Remove Cost Item Quantity" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() physical_quantity: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "cost.remove_cost_item_quantity", @@ -390,12 +532,12 @@ class RemoveCostItemQuantity(bpy.types.Operator): class EnableEditingCostItemQuantity(bpy.types.Operator): bl_idname = "bim.enable_editing_cost_item_quantity" bl_label = "Enable Editing Cost Item Quantity" + bl_options = {"REGISTER", "UNDO"} physical_quantity: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMCostProperties - while len(self.props.quantity_attributes) > 0: - self.props.quantity_attributes.remove(0) + self.props.quantity_attributes.clear() self.props.active_cost_item_quantity_id = self.physical_quantity data = Data.physical_quantities[self.physical_quantity] blenderbim.bim.helper.import_attributes(data["type"], self.props.quantity_attributes, data) @@ -405,6 +547,7 @@ class EnableEditingCostItemQuantity(bpy.types.Operator): class DisableEditingCostItemQuantity(bpy.types.Operator): bl_idname = "bim.disable_editing_cost_item_quantity" bl_label = "Disable Editing Cost Item Quantity" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): props = context.scene.BIMCostProperties @@ -415,9 +558,13 @@ class DisableEditingCostItemQuantity(bpy.types.Operator): class EditCostItemQuantity(bpy.types.Operator): bl_idname = "bim.edit_cost_item_quantity" bl_label = "Edit Cost Item Quantity" + bl_options = {"REGISTER", "UNDO"} physical_quantity: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMCostProperties attributes = blenderbim.bim.helper.export_attributes(props.quantity_attributes) self.file = IfcStore.get_file() @@ -434,79 +581,196 @@ class EditCostItemQuantity(bpy.types.Operator): class AddCostValue(bpy.types.Operator): bl_idname = "bim.add_cost_value" bl_label = "Add Cost Value" + bl_options = {"REGISTER", "UNDO"} parent: bpy.props.IntProperty() cost_type: bpy.props.StringProperty() cost_category: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() + parent = self.file.by_id(self.parent) if self.cost_type == "FIXED": category = None + attributes = {"AppliedValue": 0.0} elif self.cost_type == "SUM": category = "*" + attributes = {"Category": category} elif self.cost_type == "CATEGORY": category = self.cost_category - value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=self.file.by_id(self.parent)) - ifcopenshell.api.run("cost.edit_cost_value", self.file, cost_value=value, attributes={"Category": category}) - Data.load(self.file) + attributes = {"Category": category} + value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=parent) + ifcopenshell.api.run("cost.edit_cost_value", self.file, cost_value=value, attributes=attributes) + if parent.is_a("IfcConstructionResource"): + ResourceData.load(self.file) + else: + Data.load(self.file) return {"FINISHED"} class RemoveCostItemValue(bpy.types.Operator): - bl_idname = "bim.remove_cost_item_value" + bl_idname = "bim.remove_cost_value" bl_label = "Add Cost Item Value" + bl_options = {"REGISTER", "UNDO"} + parent: bpy.props.IntProperty() cost_value: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() - ifcopenshell.api.run("cost.remove_cost_item_value", self.file, cost_value=self.file.by_id(self.cost_value)) - Data.load(self.file) + parent = self.file.by_id(self.parent) + ifcopenshell.api.run( + "cost.remove_cost_value", + self.file, + parent=parent, + cost_value=self.file.by_id(self.cost_value), + ) + if parent.is_a("IfcConstructionResource"): + ResourceData.load(self.file) + else: + Data.load(self.file) return {"FINISHED"} class EnableEditingCostItemValue(bpy.types.Operator): bl_idname = "bim.enable_editing_cost_item_value" bl_label = "Enable Editing Cost Item Value" + bl_options = {"REGISTER", "UNDO"} cost_value: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMCostProperties - while len(self.props.cost_value_attributes) > 0: - self.props.cost_value_attributes.remove(0) - self.props.active_cost_item_value_id = self.cost_value + self.props.cost_value_attributes.clear() + self.props.active_cost_value_id = self.cost_value + self.props.cost_value_editing_type = "ATTRIBUTES" data = Data.cost_values[self.cost_value] blenderbim.bim.helper.import_attributes( - data["type"], self.props.cost_value_attributes, data, self.import_attributes + data["type"], + self.props.cost_value_attributes, + data, + lambda name, prop, data: self.import_attributes(name, prop, data, context), ) return {"FINISHED"} - def import_attributes(self, name, prop, data): + def import_attributes(self, name, prop, data, context): if name == "AppliedValue": - # TODO: for now, only support simple values + # TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure) + prop = self.props.cost_value_attributes.add() prop.data_type = "float" + prop.name = "AppliedValue" + prop.is_optional = True prop.float_value = 0.0 if prop.is_null else data[name] return True + if ( + name == "UnitBasis" + and Data.cost_schedules[context.scene.BIMCostProperties.active_cost_schedule_id]["PredefinedType"] + == "SCHEDULEOFRATES" + ): + prop = self.props.cost_value_attributes.add() + prop.name = "UnitBasisValue" + prop.data_type = "float" + prop.is_null = data["UnitBasis"] is None + prop.is_optional = True + if data["UnitBasis"]: + prop.float_value = data["UnitBasis"]["ValueComponent"] or 0 + else: + prop.float_value = 0 + + prop = self.props.cost_value_attributes.add() + prop.name = "UnitBasisUnit" + prop.data_type = "enum" + prop.is_null = prop.is_optional = False + units = {} + for unit_id, unit in UnitData.units.items(): + if unit.get("UnitType", None) in [ + "AREAUNIT", + "LENGTHUNIT", + "TIMEUNIT", + "VOLUMEUNIT", + "MASSUNIT", + "USERDEFINED", + ]: + if unit["type"] == "IfcContextDependentUnit": + units[unit_id] = f"{unit['UnitType']} / {unit['Name']}" + else: + name = unit["Name"] + if unit.get("Prefix", None): + name = f"(unit['Prefix']) {name}" + units[unit_id] = f"{unit['UnitType']} / {name}" + prop.enum_items = json.dumps(units) + if data["UnitBasis"] and data["UnitBasis"]["UnitComponent"]: + prop.enum_value = str(data["UnitBasis"]["UnitComponent"]) + return True class DisableEditingCostItemValue(bpy.types.Operator): bl_idname = "bim.disable_editing_cost_item_value" bl_label = "Disable Editing Cost Item Value" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): props = context.scene.BIMCostProperties - props.active_cost_item_value_id = 0 + props.active_cost_value_id = 0 + props.cost_value_editing_type = "" return {"FINISHED"} -class EditCostValue(bpy.types.Operator): - bl_idname = "bim.edit_cost_value" - bl_label = "Edit Cost Item Value" +class EnableEditingCostItemValueFormula(bpy.types.Operator): + bl_idname = "bim.enable_editing_cost_item_value_formula" + bl_label = "Enable Editing Cost Item Value Formula" + bl_options = {"REGISTER", "UNDO"} cost_value: bpy.props.IntProperty() def execute(self, context): + self.props = context.scene.BIMCostProperties + self.props.cost_value_attributes.clear() + self.props.active_cost_value_id = self.cost_value + self.props.cost_value_editing_type = "FORMULA" + self.props.cost_value_formula = Data.cost_values[self.cost_value]["Formula"] + return {"FINISHED"} + + +class EditCostItemValueFormula(bpy.types.Operator): + bl_idname = "bim.edit_cost_value_formula" + bl_label = "Edit Cost Value Formula" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMCostProperties - attributes = blenderbim.bim.helper.export_attributes(props.cost_value_attributes) + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_value_formula", + self.file, + **{"cost_value": self.file.by_id(self.cost_value), "formula": props.cost_value_formula}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_cost_item_value() + return {"FINISHED"} + + +class EditCostItemValue(bpy.types.Operator): + bl_idname = "bim.edit_cost_value" + bl_label = "Edit Cost Value" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMCostProperties + attributes = blenderbim.bim.helper.export_attributes( + props.cost_value_attributes, lambda attributes, prop: self.export_attributes(attributes, prop, context) + ) self.file = IfcStore.get_file() ifcopenshell.api.run( "cost.edit_cost_value", @@ -517,14 +781,33 @@ class EditCostValue(bpy.types.Operator): bpy.ops.bim.disable_editing_cost_item_value() return {"FINISHED"} + def export_attributes(self, attributes, prop, context): + if prop.name == "UnitBasisValue": + if prop.is_null: + attributes["UnitBasis"] = None + return True + attributes["UnitBasis"] = { + "ValueComponent": prop.float_value or 1, + "UnitComponent": IfcStore.get_file().by_id( + int(context.scene.BIMCostProperties.cost_value_attributes.get("UnitBasisUnit").enum_value) + ), + } + return True + if prop.name == "UnitBasisUnit": + return True + class CopyCostItemValues(bpy.types.Operator): bl_idname = "bim.copy_cost_item_values" bl_label = "Copy Cost Item Values" + bl_options = {"REGISTER", "UNDO"} source: bpy.props.IntProperty() destination: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "cost.copy_cost_item_values", @@ -538,12 +821,13 @@ class CopyCostItemValues(bpy.types.Operator): class SelectCostItemProducts(bpy.types.Operator): bl_idname = "bim.select_cost_item_products" bl_label = "Select Cost Item Products" + bl_options = {"REGISTER", "UNDO"} cost_item: bpy.props.IntProperty() def execute(self, context): self.file = IfcStore.get_file() - related_products = Data.cost_items[self.cost_item]["Controls"] - for obj in bpy.context.visible_objects: + related_products = Data.cost_items[self.cost_item]["Controls"].keys() + for obj in context.visible_objects: obj.select_set(False) if obj.BIMObjectProperties.ifc_definition_id in related_products: obj.select_set(True) @@ -553,6 +837,7 @@ class SelectCostItemProducts(bpy.types.Operator): class SelectCostScheduleProducts(bpy.types.Operator): bl_idname = "bim.select_cost_schedule_products" bl_label = "Select Cost Schedule Products" + bl_options = {"REGISTER", "UNDO"} cost_schedule: bpy.props.IntProperty() def execute(self, context): @@ -561,13 +846,229 @@ class SelectCostScheduleProducts(bpy.types.Operator): for cost_item_id in Data.cost_schedules[self.cost_schedule]["Controls"]: self.get_related_products(Data.cost_items[cost_item_id]) self.related_products = set(self.related_products) - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: obj.select_set(False) if obj.BIMObjectProperties.ifc_definition_id in self.related_products: obj.select_set(True) return {"FINISHED"} def get_related_products(self, cost_item): - self.related_products.extend(cost_item["Controls"]) + self.related_products.extend(cost_item["Controls"].keys()) for child_id in cost_item["IsNestedBy"]: self.get_related_products(Data.cost_items[child_id]) + + +class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper): + bl_idname = "import_cost_schedule_csv.bim" + bl_label = "Import Cost Schedule CSV" + bl_options = {"REGISTER", "UNDO"} + filename_ext = ".csv" + filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + is_schedule_of_rates: bpy.props.BoolProperty(name="Is Schedule Of Rates", default=False) + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + from ifc5d.csv2ifc import Csv2Ifc + + self.file = IfcStore.get_file() + start = time.time() + csv2ifc = Csv2Ifc() + csv2ifc.csv = self.filepath + csv2ifc.file = self.file + csv2ifc.is_schedule_of_rates = self.is_schedule_of_rates + csv2ifc.execute() + Data.load(IfcStore.get_file()) + UnitData.load(IfcStore.get_file()) + purge() + print("Import finished in {:.2f} seconds".format(time.time() - start)) + return {"FINISHED"} + + +class AddCostColumn(bpy.types.Operator): + bl_idname = "bim.add_cost_column" + bl_label = "Add Cost Column" + bl_options = {"REGISTER", "UNDO"} + name: bpy.props.StringProperty() + + def execute(self, context): + self.props = context.scene.BIMCostProperties + new = self.props.columns.add() + new.name = self.name + Data.set_categories([c.name for c in self.props.columns]) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class RemoveCostColumn(bpy.types.Operator): + bl_idname = "bim.remove_cost_column" + bl_label = "Remove Cost Column" + bl_options = {"REGISTER", "UNDO"} + name: bpy.props.StringProperty() + + def execute(self, context): + self.props = context.scene.BIMCostProperties + self.props.columns.remove(self.props.columns.find(self.name)) + Data.set_categories([c.name for c in self.props.columns]) + Data.load(IfcStore.get_file()) + return {"FINISHED"} + + +class LoadCostItemQuantities(bpy.types.Operator): + bl_idname = "bim.load_cost_item_quantities" + bl_label = "Load Cost Item Quantities" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + self.props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + self.props.cost_item_products.clear() + self.props.cost_item_processes.clear() + self.props.cost_item_resources.clear() + ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id + for control_id, quantity_ids in Data.cost_items[ifc_definition_id]["Controls"].items(): + related_object = self.file.by_id(control_id) + if related_object.is_a("IfcProduct"): + new = self.props.cost_item_products.add() + elif related_object.is_a("IfcProcess"): + new = self.props.cost_item_processes.add() + elif related_object.is_a("IfcResource"): + new = self.props.cost_item_resources.add() + new.ifc_definition_id = control_id + new.name = related_object.Name or "Unnamed" + total_quantity = 0 + for quantity_id in quantity_ids: + total_quantity += self.file.by_id(quantity_id)[3] + new.total_quantity = total_quantity + return {"FINISHED"} + + +class LoadCostItemTypes(bpy.types.Operator): + bl_idname = "bim.load_cost_item_types" + bl_label = "Load Cost Item Types" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + self.props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + self.props.cost_item_type_products.clear() + # TODO implement process and resource types + # self.props.cost_item_processes.clear() + # self.props.cost_item_resources.clear() + ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id + for control_id, quantity_ids in Data.cost_items[ifc_definition_id]["Controls"].items(): + related_object = self.file.by_id(control_id) + if related_object.is_a("IfcTypeProduct"): + new = self.props.cost_item_type_products.add() + # TODO implement process and resource types + # elif related_object.is_a("IfcProcess"): + # new = self.props.cost_item_processes.add() + # elif related_object.is_a("IfcResource"): + # new = self.props.cost_item_resources.add() + new.ifc_definition_id = control_id + new.name = related_object.Name or "Unnamed" + return {"FINISHED"} + + +class AssignCostValue(bpy.types.Operator): + bl_idname = "bim.assign_cost_value" + bl_label = "Assign Cost Value" + bl_options = {"REGISTER", "UNDO"} + cost_item: bpy.props.IntProperty() + cost_rate: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.assign_cost_value", + self.file, + cost_item=self.file.by_id(self.cost_item), + cost_rate=self.file.by_id(self.cost_rate), + ) + Data.load(self.file) + return {"FINISHED"} + + +class LoadScheduleOfRates(bpy.types.Operator): + bl_idname = "bim.load_schedule_of_rates" + bl_label = "Load Schedule of Rates" + bl_options = {"REGISTER", "UNDO"} + cost_schedule: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMCostProperties + self.props.is_cost_update_enabled = False + self.props.cost_item_rates.clear() + self.contracted_cost_item_rates = json.loads(self.props.contracted_cost_item_rates) + for related_object_id in Data.cost_schedules[self.cost_schedule]["Controls"]: + self.create_new_cost_item_li(related_object_id, 0) + self.props.is_cost_update_enabled = True + return {"FINISHED"} + + def create_new_cost_item_li(self, related_object_id, level_index): + cost_item = Data.cost_items[related_object_id] + new = self.props.cost_item_rates.add() + new.ifc_definition_id = related_object_id + new.name = cost_item["Name"] or "Unnamed" + new.identification = cost_item["Identification"] or "XXX" + new.is_expanded = related_object_id not in self.contracted_cost_item_rates + new.level_index = level_index + if cost_item["IsNestedBy"]: + new.has_children = True + if new.is_expanded: + for related_object_id in cost_item["IsNestedBy"]: + self.create_new_cost_item_li(related_object_id, level_index + 1) + + +class ExpandCostItemRate(bpy.types.Operator): + bl_idname = "bim.expand_cost_item_rate" + bl_label = "Expand Cost Item Rate" + bl_options = {"REGISTER", "UNDO"} + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + contracted_cost_item_rates = json.loads(props.contracted_cost_item_rates) + contracted_cost_item_rates.remove(self.cost_item) + props.contracted_cost_item_rates = json.dumps(contracted_cost_item_rates) + bpy.ops.bim.load_schedule_of_rates(cost_schedule=int(props.schedule_of_rates)) + return {"FINISHED"} + + +class ContractCostItemRate(bpy.types.Operator): + bl_idname = "bim.contract_cost_item_rate" + bl_label = "Contract Cost Item Rate" + bl_options = {"REGISTER", "UNDO"} + cost_item: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + self.file = IfcStore.get_file() + contracted_cost_item_rates = json.loads(props.contracted_cost_item_rates) + contracted_cost_item_rates.append(self.cost_item) + props.contracted_cost_item_rates = json.dumps(contracted_cost_item_rates) + bpy.ops.bim.load_schedule_of_rates(cost_schedule=int(props.schedule_of_rates)) + return {"FINISHED"} + + +class CalculateCostItemResourceValue(bpy.types.Operator): + bl_idname = "bim.calculate_cost_item_resource_value" + bl_label = "Calculate Cost Item Resource Value" + bl_options = {"REGISTER", "UNDO"} + cost_item: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.calculate_cost_item_resource_value", self.file, cost_item=self.file.by_id(self.cost_item) + ) + Data.load(self.file) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py index 16c48c736c..69ac4f825f 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api from blenderbim.bim.ifc import IfcStore @@ -18,20 +36,52 @@ from bpy.props import ( quantitytypes_enum = [] -quantitynames_enum = [] +productquantitynames_enum = [] +productquantitynames_count = [] +processquantitynames_enum = [] +processquantitynames_id = 0 +resourcequantitynames_enum = [] +resourcequantitynames_id = 0 +scheduleofrates_enum = [] def purge(): global quantitytypes_enum - global quantitynames_enum + global productquantitynames_enum + global productquantitynames_count + global processquantitynames_enum + global processquantitynames_id + global resourcequantitynames_enum + global resourcequantitynames_id + global scheduleofrates_enum quantitytypes_enum = [] - quantitynames_enum = [] + productquantitynames_enum = [] + productquantitynames_count = [] + processquantitynames_enum = [] + processquantitynames_id = 0 + resourcequantitynames_enum = [] + resourcequantitynames_id = 0 + scheduleofrates_enum = [] -def getQuantityTypes(self, context): +def get_schedule_of_rates(self, context): + global scheduleofrates_enum + if len(scheduleofrates_enum) == 0: + scheduleofrates_enum.extend( + (str(ifc_definition_id), schedule["Name"] or "Unnamed", "") + for ifc_definition_id, schedule in Data.cost_schedules.items() + if schedule["PredefinedType"] == "SCHEDULEOFRATES" + ) + return scheduleofrates_enum + + +def update_schedule_of_rates(self, context): + bpy.ops.bim.load_schedule_of_rates(cost_schedule=int(self.schedule_of_rates)) + + +def get_quantity_types(self, context): global quantitytypes_enum if len(quantitytypes_enum) == 0 and IfcStore.get_schema(): - quantitytypes_enum = [("QTO", "Qto", "Derive quantities from IFC quantity sets")] quantitytypes_enum.extend( [ (t.name(), t.name(), "") @@ -41,26 +91,102 @@ def getQuantityTypes(self, context): return quantitytypes_enum -def getQuantityNames(self, context): - global quantitynames_enum +def get_product_quantity_names(self, context): + global productquantitynames_enum + global productquantitynames_count ifc_file = IfcStore.get_file() - if len(quantitynames_enum) == 0 and ifc_file: + total_selected_objects = len(context.selected_objects) + if total_selected_objects != productquantitynames_count or total_selected_objects == 1: + productquantitynames_enum = [] + productquantitynames_count = total_selected_objects names = set() - for element_id in Data.cost_items[self.active_cost_item_id]["Controls"]: + for obj in context.selected_objects: + element_id = obj.BIMObjectProperties.ifc_definition_id + if not element_id: + continue + potential_names = set() if element_id not in PsetData.products: - PsetData.load(IfcStore.get_file(), element_id) + PsetData.load(ifc_file, element_id) for qto_id in PsetData.products[element_id]["qtos"]: qto = PsetData.qtos[qto_id] - [names.add(PsetData.properties[p]["Name"]) for p in qto["Properties"]] - quantitynames_enum.extend([(n, n, "") for n in names]) - return quantitynames_enum + [potential_names.add(PsetData.properties[p]["Name"]) for p in qto["Properties"]] + names = names.intersection(potential_names) if names else potential_names + productquantitynames_enum.extend([(n, n, "") for n in names]) + return productquantitynames_enum -def updateCostItemName(self, context): - if self.name == "Unnamed": +def get_process_quantity_names(self, context): + global processquantitynames_enum + global processquantitynames_id + ifc_file = IfcStore.get_file() + active_task_index = context.scene.BIMWorkScheduleProperties.active_task_index + total_tasks = len(context.scene.BIMTaskTreeProperties.tasks) + if not total_tasks or active_task_index >= total_tasks: + return [] + ifc_definition_id = context.scene.BIMTaskTreeProperties.tasks[active_task_index].ifc_definition_id + if processquantitynames_id != ifc_definition_id: + processquantitynames_enum = [] + processquantitynames_id = ifc_definition_id + names = set() + if ifc_definition_id not in PsetData.products: + PsetData.load(ifc_file, ifc_definition_id) + for qto_id in PsetData.products[ifc_definition_id]["qtos"]: + qto = PsetData.qtos[qto_id] + [names.add(PsetData.properties[p]["Name"]) for p in qto["Properties"]] + processquantitynames_enum.extend([(n, n, "") for n in names]) + return processquantitynames_enum + + +def get_resource_quantity_names(self, context): + global resourcequantitynames_enum + global resourcequantitynames_id + ifc_file = IfcStore.get_file() + active_resource_index = context.scene.BIMResourceProperties.active_resource_index + total_resources = len(context.scene.BIMResourceTreeProperties.resources) + if not total_resources or active_resource_index >= total_resources: + return [] + ifc_definition_id = context.scene.BIMResourceTreeProperties.resources[active_resource_index].ifc_definition_id + if resourcequantitynames_id != ifc_definition_id: + resourcequantitynames_enum = [] + resourcequantitynames_id = ifc_definition_id + names = set() + if ifc_definition_id not in PsetData.products: + PsetData.load(ifc_file, ifc_definition_id) + for qto_id in PsetData.products[ifc_definition_id]["qtos"]: + qto = PsetData.qtos[qto_id] + [names.add(PsetData.properties[p]["Name"]) for p in qto["Properties"]] + resourcequantitynames_enum.extend([(n, n, "") for n in names]) + return resourcequantitynames_enum + + +def update_active_cost_item_index(self, context): + if Data.cost_schedules[self.active_cost_schedule_id]["PredefinedType"] == "SCHEDULEOFRATES": + bpy.ops.bim.load_cost_item_types() + else: + bpy.ops.bim.load_cost_item_quantities() + + +def update_cost_item_identification(self, context): + props = context.scene.BIMCostProperties + if not props.is_cost_update_enabled or self.identification == "XXX": return self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_item", + self.file, + **{"cost_item": self.file.by_id(self.ifc_definition_id), "attributes": {"Identification": self.identification}}, + ) + Data.load(self.file) + if props.active_cost_item_id == self.ifc_definition_id: + attribute = props.cost_item_attributes.get("Identification") + attribute.string_value = self.identification + + +def update_cost_item_name(self, context): props = context.scene.BIMCostProperties + if not props.is_cost_update_enabled or self.name == "Unnamed": + return + self.file = IfcStore.get_file() ifcopenshell.api.run( "cost.edit_cost_item", self.file, @@ -73,25 +199,40 @@ def updateCostItemName(self, context): class CostItem(PropertyGroup): - name: StringProperty(name="Name", update=updateCostItemName) + name: StringProperty(name="Name", update=update_cost_item_name) + identification: StringProperty(name="Identification", update=update_cost_item_identification) ifc_definition_id: IntProperty(name="IFC Definition ID") has_children: BoolProperty(name="Has Children") is_expanded: BoolProperty(name="Is Expanded") level_index: IntProperty(name="Level Index") +class CostItemQuantity(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + total_quantity: FloatProperty(name="Total Quantity") + + +class CostItemType(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + class BIMCostProperties(PropertyGroup): + is_cost_update_enabled: BoolProperty(name="Is Cost Update Enabled", default=True) cost_schedule_attributes: CollectionProperty(name="Cost Schedule Attributes", type=Attribute) is_editing: StringProperty(name="Is Editing") active_cost_schedule_id: IntProperty(name="Active Cost Schedule Id") - cost_items: CollectionProperty(name="Work Calendar", type=CostItem) + cost_items: CollectionProperty(name="Cost Items", type=CostItem) active_cost_item_id: IntProperty(name="Active Cost Id") cost_item_editing_type: StringProperty(name="Cost Item Editing Type") - active_cost_item_index: IntProperty(name="Active Cost Item Index") + active_cost_item_index: IntProperty(name="Active Cost Item Index", update=update_active_cost_item_index) cost_item_attributes: CollectionProperty(name="Task Attributes", type=Attribute) contracted_cost_items: StringProperty(name="Contracted Cost Items", default="[]") - quantity_types: EnumProperty(items=getQuantityTypes, name="Quantity Types") - quantity_names: EnumProperty(items=getQuantityNames, name="Quantity Names") + quantity_types: EnumProperty(items=get_quantity_types, name="Quantity Types") + product_quantity_names: EnumProperty(items=get_product_quantity_names, name="Product Quantity Names") + process_quantity_names: EnumProperty(items=get_process_quantity_names, name="Process Quantity Names") + resource_quantity_names: EnumProperty(items=get_resource_quantity_names, name="Resource Quantity Names") active_cost_item_quantity_id: IntProperty(name="Active Cost Item Quantity Id") quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute) cost_types: EnumProperty( @@ -103,5 +244,25 @@ class BIMCostProperties(PropertyGroup): name="Cost Types", ) cost_category: StringProperty(name="Cost Category") - active_cost_item_value_id: IntProperty(name="Active Cost Item Value Id") + active_cost_value_id: IntProperty(name="Active Cost Item Value Id") + cost_value_editing_type: StringProperty(name="Cost Value Editing Type") cost_value_attributes: CollectionProperty(name="Cost Value Attributes", type=Attribute) + cost_value_formula: StringProperty(name="Cost Value Formula") + cost_column: StringProperty(name="Cost Column") + should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False) + columns: CollectionProperty(name="Columns", type=StrProperty) + active_column_index: IntProperty(name="Active Column Index") + cost_item_products: CollectionProperty(name="Cost Item Products", type=CostItemQuantity) + active_cost_item_product_index: IntProperty(name="Active Cost Item Product Index") + cost_item_processes: CollectionProperty(name="Cost Item Processes", type=CostItemQuantity) + active_cost_item_process_index: IntProperty(name="Active Cost Item Process Index") + cost_item_resources: CollectionProperty(name="Cost Item Resources", type=CostItemQuantity) + active_cost_item_resource_index: IntProperty(name="Active Cost Item Resource Index") + cost_item_type_products: CollectionProperty(name="Cost Item Type Products", type=CostItemType) + active_cost_item_type_product_index: IntProperty(name="Active Cost Item Type Product Index") + schedule_of_rates: EnumProperty( + items=get_schedule_of_rates, name="Schedule Of Rates", update=update_schedule_of_rates + ) + cost_item_rates: CollectionProperty(name="Cost Item Rates", type=CostItem) + active_cost_item_rate_index: IntProperty(name="Active Cost Rate Index") + contracted_cost_item_rates: StringProperty(name="Contracted Cost Item Rates", default="[]") diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index a2a0a0d3c8..71f060ccf4 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -1,6 +1,27 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.bim.helper +import blenderbim.bim.module.cost.prop as CostProp from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.cost.data import Data +from ifcopenshell.api.unit.data import Data as UnitData class BIM_PT_cost_schedules(Panel): @@ -13,7 +34,8 @@ class BIM_PT_cost_schedules(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): self.props = context.scene.BIMCostProperties @@ -21,11 +43,19 @@ class BIM_PT_cost_schedules(Panel): if not Data.is_loaded: Data.load(IfcStore.get_file()) + if not UnitData.is_loaded: + UnitData.load(IfcStore.get_file()) + row = self.layout.row() row.operator("bim.add_cost_schedule", icon="ADD") - for cost_schedule_id, cost_schedule in Data.cost_schedules.items(): - self.draw_cost_schedule_ui(cost_schedule_id, cost_schedule) + if self.props.active_cost_schedule_id: + self.draw_cost_schedule_ui( + self.props.active_cost_schedule_id, Data.cost_schedules[self.props.active_cost_schedule_id] + ) + else: + for cost_schedule_id, cost_schedule in Data.cost_schedules.items(): + self.draw_cost_schedule_ui(cost_schedule_id, cost_schedule) def draw_cost_schedule_ui(self, cost_schedule_id, cost_schedule): row = self.layout.row(align=True) @@ -34,6 +64,7 @@ class BIM_PT_cost_schedules(Panel): if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule_id: op = row.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="") op.cost_schedule = cost_schedule_id + row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY") if self.props.is_editing == "COST_SCHEDULE": row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK") elif self.props.is_editing == "COST_ITEMS": @@ -52,19 +83,46 @@ class BIM_PT_cost_schedules(Panel): if self.props.is_editing == "COST_SCHEDULE": self.draw_editable_cost_schedule_ui() elif self.props.is_editing == "COST_ITEMS": + if self.props.should_show_column_ui: + self.draw_column_ui() self.draw_editable_cost_item_ui(cost_schedule_id) + def draw_column_ui(self): + row = self.layout.row(align=True) + row.prop(self.props, "cost_column", text="") + row.operator("bim.add_cost_column", text="", icon="ADD").name = self.props.cost_column + self.layout.template_list("BIM_UL_cost_columns", "", self.props, "columns", self.props, "active_column_index") + def draw_editable_cost_schedule_ui(self): - for attribute in self.props.cost_schedule_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + blenderbim.bim.helper.draw_attributes(self.props.cost_schedule_attributes, self.layout) def draw_editable_cost_item_ui(self, cost_schedule_id): + row = self.layout.row(align=True) + row.alignment = "RIGHT" + ifc_definition_id = None + if self.props.cost_items and self.props.active_cost_item_index < len(self.props.cost_items): + ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id + if ifc_definition_id: + + if Data.cost_schedules[self.props.active_cost_schedule_id]["PredefinedType"] != "SCHEDULEOFRATES": + op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES") + op.cost_item = ifc_definition_id + + op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC") + op.cost_item = ifc_definition_id + + row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = ifc_definition_id + + if self.props.active_cost_item_id == ifc_definition_id: + if self.props.cost_item_editing_type == "ATTRIBUTES": + row.operator("bim.edit_cost_item", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL") + else: + op = row.operator("bim.enable_editing_cost_item", text="", icon="GREASEPENCIL") + op.cost_item = ifc_definition_id + + row.operator("bim.remove_cost_item", text="", icon="X").cost_item = ifc_definition_id + self.layout.template_list( "BIM_UL_cost_items", "", @@ -82,24 +140,11 @@ class BIM_PT_cost_schedules(Panel): self.draw_editable_cost_item_values_ui() def draw_editable_cost_item_attributes_ui(self): - for attribute in self.props.cost_item_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + blenderbim.bim.helper.draw_attributes(self.props.cost_item_attributes, self.layout) def draw_editable_cost_item_quantities_ui(self): row = self.layout.row(align=True) row.prop(self.props, "quantity_types", text="") - if self.props.quantity_types == "QTO": - row.prop(self.props, "quantity_names", text="") op = row.operator("bim.add_cost_item_quantity", text="", icon="ADD") op.cost_item = self.props.active_cost_item_id op.ifc_class = self.props.quantity_types @@ -130,20 +175,7 @@ class BIM_PT_cost_schedules(Panel): self.draw_editable_cost_item_quantity_ui(box) def draw_editable_cost_item_quantity_ui(self, layout): - for attribute in self.props.quantity_attributes: - row = layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + blenderbim.bim.helper.draw_attributes(self.props.quantity_attributes, self.layout) def draw_editable_cost_item_values_ui(self): row = self.layout.row(align=True) @@ -160,141 +192,429 @@ class BIM_PT_cost_schedules(Panel): row = self.layout.row(align=True) self.draw_readonly_cost_value_ui(row, cost_value_id) - if self.props.active_cost_item_value_id: + if self.props.cost_value_editing_type == "ATTRIBUTES": box = self.layout.box() - self.draw_editable_cost_value_ui(box, Data.cost_values[self.props.active_cost_item_value_id]) - + self.draw_editable_cost_value_ui(box, Data.cost_values[self.props.active_cost_value_id]) def draw_readonly_cost_value_ui(self, layout, cost_value_id): cost_value = Data.cost_values[cost_value_id] - cost_value_label = "{0:.2f}".format(cost_value["AppliedValue"]) - if cost_value["Category"]: - cost_value_label += " ({})".format(cost_value["Category"]) - layout.label(text="", icon="DISC") - self.draw_cost_value_operator_ui(layout, cost_value_id) - layout.label(text=cost_value_label) - for component_id in cost_value["Components"] or []: - self.draw_readonly_component_cost_value_ui(layout, component_id) + if self.props.active_cost_value_id == cost_value_id and self.props.cost_value_editing_type == "FORMULA": + layout.prop(self.props, "cost_value_formula", text="") + else: + cost_value_label = "{0:.2f}".format(cost_value["AppliedValue"]) + cost_value_label += " = " + cost_value["Formula"] + layout.label(text=cost_value_label, icon="DISC") - def draw_readonly_component_cost_value_ui(self, layout, cost_value_id, level=1): - self.draw_cost_value_operator_ui(layout, cost_value_id) - cost_value = Data.cost_values[cost_value_id] - cost_value_label = ">" * level - cost_value_label += "{0:.2f}".format(cost_value["AppliedValue"]) - if cost_value["Category"]: - cost_value_label += " ({})".format(cost_value["Category"]) - layout.label(text=cost_value_label) + self.draw_cost_value_operator_ui(layout, cost_value_id, self.props.active_cost_item_id) - for component_id in cost_value["Components"] or []: - self.draw_readonly_component_cost_value_ui(layout, component_id, level + 1) - - def draw_cost_value_operator_ui(self, layout, cost_value_id): - if self.props.active_cost_item_value_id and self.props.active_cost_item_value_id == cost_value_id: - op = layout.operator("bim.edit_cost_value", text="", icon="CHECKMARK") - op.cost_value = cost_value_id - op = layout.operator("bim.add_cost_value", text="", icon="ADD") - op.parent = cost_value_id - op.cost_type = self.props.cost_types - if self.props.cost_types == "CATEGORY": - op.cost_category = self.props.cost_category + def draw_cost_value_operator_ui(self, layout, cost_value_id, parent_id): + if self.props.active_cost_value_id and self.props.active_cost_value_id == cost_value_id: + if self.props.cost_value_editing_type == "ATTRIBUTES": + op = layout.operator("bim.edit_cost_value", text="", icon="CHECKMARK") + op.cost_value = cost_value_id + elif self.props.cost_value_editing_type == "FORMULA": + op = layout.operator("bim.edit_cost_value_formula", text="", icon="CHECKMARK") + op.cost_value = cost_value_id layout.operator("bim.disable_editing_cost_item_value", text="", icon="CANCEL") - elif self.props.active_cost_item_value_id: - op = layout.operator("bim.add_cost_value", text="", icon="ADD") - op.parent = cost_value_id - op.cost_type = self.props.cost_types - if self.props.cost_types == "CATEGORY": - op.cost_category = self.props.cost_category - op = layout.operator("bim.remove_cost_item_value", text="", icon="X") + elif self.props.active_cost_value_id: + op = layout.operator("bim.remove_cost_value", text="", icon="X") + op.parent = parent_id op.cost_value = cost_value_id else: + op = layout.operator("bim.enable_editing_cost_item_value_formula", text="", icon="CON_TRANSLIKE") + op.cost_value = cost_value_id op = layout.operator("bim.enable_editing_cost_item_value", text="", icon="GREASEPENCIL") op.cost_value = cost_value_id - op = layout.operator("bim.add_cost_value", text="", icon="ADD") - op.parent = cost_value_id - op.cost_type = self.props.cost_types - if self.props.cost_types == "CATEGORY": - op.cost_category = self.props.cost_category - op = layout.operator("bim.remove_cost_item_value", text="", icon="X") + op = layout.operator("bim.remove_cost_value", text="", icon="X") + op.parent = parent_id op.cost_value = cost_value_id def draw_editable_cost_value_ui(self, layout, cost_value): - for attribute in self.props.cost_value_attributes: - row = layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + blenderbim.bim.helper.draw_attributes(self.props.cost_value_attributes, layout) +class BIM_PT_cost_item_types(Panel): + bl_label = "IFC Cost Item Types" + bl_idname = "BIM_PT_cost_item_types" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_cost_schedules" -class BIM_UL_cost_items(UIList): + @classmethod + def poll(cls, context): + props = context.scene.BIMCostProperties + total_cost_items = len(props.cost_items) + if not props.active_cost_schedule_id: + return False + if Data.cost_schedules[props.active_cost_schedule_id]["PredefinedType"] != "SCHEDULEOFRATES": + return False + if total_cost_items > 0 and props.active_cost_item_index < total_cost_items: + return True + return False + + def draw(self, context): + self.props = context.scene.BIMCostProperties + + cost_item = self.props.cost_items[self.props.active_cost_item_index] + + grid = self.layout.grid_flow(columns=3, even_columns=True) + + # Column1 + col = grid.column() + + row2 = col.row(align=True) + row2.label(text="Elements") + op = row2.operator("bim.assign_cost_item_type", text="", icon="ADD") + op.cost_item = cost_item.ifc_definition_id + op = row2.operator("bim.unassign_cost_item_type", text="", icon="REMOVE") + op.cost_item = cost_item.ifc_definition_id + op.related_object = 0 + op = row2.operator("bim.select_cost_item_products", icon="RESTRICT_SELECT_OFF", text="") + op.cost_item = cost_item.ifc_definition_id + + row2 = col.row() + row2.template_list( + "BIM_UL_cost_item_types", + "", + self.props, + "cost_item_type_products", + self.props, + "active_cost_item_type_product_index", + ) + + # Column2 + # TODO + col = grid.column() + + row2 = col.row(align=True) + row2.label(text="Tasks") + + row2 = col.row() + row2.template_list( + "BIM_UL_cost_item_quantities", + "", + self.props, + "cost_item_processes", + self.props, + "active_cost_item_process_index", + ) + + # Column3 + # TODO + col = grid.column() + + row2 = col.row(align=True) + row2.label(text="Resources") + + row2 = col.row() + row2.template_list( + "BIM_UL_cost_item_quantities", + "", + self.props, + "cost_item_resources", + self.props, + "active_cost_item_resource_index", + ) + + +class BIM_PT_cost_item_quantities(Panel): + bl_label = "IFC Cost Item Quantities" + bl_idname = "BIM_PT_cost_item_quantities" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_cost_schedules" + + @classmethod + def poll(cls, context): + props = context.scene.BIMCostProperties + total_cost_items = len(props.cost_items) + if not props.active_cost_schedule_id: + return False + if Data.cost_schedules[props.active_cost_schedule_id]["PredefinedType"] == "SCHEDULEOFRATES": + return False + if total_cost_items > 0 and props.active_cost_item_index < total_cost_items: + return True + return False + + def draw(self, context): + self.props = context.scene.BIMCostProperties + + cost_item = self.props.cost_items[self.props.active_cost_item_index] + + grid = self.layout.grid_flow(columns=3, even_columns=True) + + # Column1 + col = grid.column() + + has_quantity_names = CostProp.get_product_quantity_names(self, context) + + row2 = col.row(align=True) + row2.label(text="Elements") + op = row2.operator("bim.select_cost_item_products", icon="RESTRICT_SELECT_OFF", text="") + op.cost_item = cost_item.ifc_definition_id + + if context.selected_objects: + if has_quantity_names: + op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES") + op.related_object_type = "PRODUCT" + op.cost_item = cost_item.ifc_definition_id + op.prop_name = self.props.product_quantity_names + + op = row2.operator("bim.assign_cost_item_quantity", text="", icon="ADD") + op.related_object_type = "PRODUCT" + op.cost_item = cost_item.ifc_definition_id + op.prop_name = "" + + op = row2.operator("bim.unassign_cost_item_quantity", text="", icon="REMOVE") + op.cost_item = cost_item.ifc_definition_id + op.related_object = 0 + + row2 = col.row() + row2.template_list( + "BIM_UL_cost_item_quantities", + "", + self.props, + "cost_item_products", + self.props, + "active_cost_item_product_index", + ) + + if has_quantity_names: + row2 = col.row() + row2.prop(self.props, "product_quantity_names", text="") + + # Column2 + col = grid.column() + + has_quantity_names = CostProp.get_process_quantity_names(self, context) + + row2 = col.row(align=True) + row2.label(text="Tasks") + + tprops = context.scene.BIMTaskTreeProperties + wprops = context.scene.BIMWorkScheduleProperties + if tprops.tasks and wprops.active_task_index < len(tprops.tasks): + if has_quantity_names: + op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES") + op.related_object_type = "PROCESS" + op.cost_item = cost_item.ifc_definition_id + op.prop_name = self.props.process_quantity_names + + op = row2.operator("bim.assign_cost_item_quantity", text="", icon="ADD") + op.related_object_type = "PROCESS" + op.cost_item = cost_item.ifc_definition_id + op.prop_name = "" + + row2 = col.row() + row2.template_list( + "BIM_UL_cost_item_quantities", + "", + self.props, + "cost_item_processes", + self.props, + "active_cost_item_process_index", + ) + + if has_quantity_names: + row2 = col.row() + row2.prop(self.props, "process_quantity_names", text="") + + # Column3 + col = grid.column() + + has_quantity_names = CostProp.get_resource_quantity_names(self, context) + + row2 = col.row(align=True) + row2.label(text="Resources") + + op = row2.operator("bim.calculate_cost_item_resource_value", text="", icon="DISC") + op.cost_item = cost_item.ifc_definition_id + + rtprops = context.scene.BIMResourceTreeProperties + rprops = context.scene.BIMResourceProperties + if rtprops.resources and rprops.active_resource_index < len(rtprops.resources): + if has_quantity_names: + op = row2.operator("bim.assign_cost_item_quantity", text="", icon="PROPERTIES") + op.related_object_type = "RESOURCE" + op.cost_item = cost_item.ifc_definition_id + op.prop_name = self.props.resource_quantity_names + + op = row2.operator("bim.assign_cost_item_quantity", text="", icon="ADD") + op.related_object_type = "RESOURCE" + op.cost_item = cost_item.ifc_definition_id + op.prop_name = "" + + row2 = col.row() + row2.template_list( + "BIM_UL_cost_item_quantities", + "", + self.props, + "cost_item_resources", + self.props, + "active_cost_item_resource_index", + ) + + if has_quantity_names: + row2 = col.row() + row2.prop(self.props, "resource_quantity_names", text="") + + +class BIM_PT_cost_item_rates(Panel): + bl_label = "IFC Cost Item Rates" + bl_idname = "BIM_PT_cost_item_rates" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_cost_schedules" + + @classmethod + def poll(cls, context): + props = context.scene.BIMCostProperties + total_cost_items = len(props.cost_items) + if not props.active_cost_schedule_id: + return False + if Data.cost_schedules[props.active_cost_schedule_id]["PredefinedType"] == "SCHEDULEOFRATES": + return False + if total_cost_items > 0 and props.active_cost_item_index < total_cost_items: + return True + return False + + def draw(self, context): + self.props = context.scene.BIMCostProperties + cost_item = self.props.cost_items[self.props.active_cost_item_index] + row = self.layout.row(align=True) + row.prop(self.props, "schedule_of_rates", text="") + if self.props.active_cost_item_rate_index < len(self.props.cost_item_rates): + op = row.operator("bim.assign_cost_value", text="", icon="COPYDOWN") + op.cost_item = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id + op.cost_rate = self.props.cost_item_rates[self.props.active_cost_item_rate_index].ifc_definition_id + self.layout.template_list( + "BIM_UL_cost_item_rates", + "", + self.props, + "cost_item_rates", + self.props, + "active_cost_item_rate_index", + ) + + +class BIM_UL_cost_items_trait: def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: - props = context.scene.BIMCostProperties + self.props = context.scene.BIMCostProperties cost_item = Data.cost_items[item.ifc_definition_id] row = layout.row(align=True) - for i in range(0, item.level_index): - row.label(text="", icon="BLANK1") - if item.has_children: - if item.is_expanded: - row.operator( - "bim.contract_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" - ).cost_item = item.ifc_definition_id - else: - row.operator( - "bim.expand_cost_item", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" - ).cost_item = item.ifc_definition_id + self.draw_hierarchy(row, item) + + split1 = row.split(factor=0.1) + split1.prop(item, "identification", emboss=False, text="") + split2 = split1.split(factor=0.5) + split2.alignment = "RIGHT" + split2.prop(item, "name", emboss=False, text="") + + self.draw_quantity_column(split2, cost_item) + self.draw_value_column(split2, cost_item) + + for column in self.props.columns: + split2.label(text=str(cost_item["CategoryValues"].get(column.name, "-"))) + + self.draw_total_cost_column(split2, cost_item) + # TODO: reimplement "bim.copy_cost_item_values" somewhere with better UX + + def draw_hierarchy(self, row, item): + for i in range(0, item.level_index): + row.label(text="", icon="BLANK1") + if item.has_children: + if item.is_expanded: + op = row.operator(self.contract_operator, text="", emboss=False, icon="DISCLOSURE_TRI_DOWN") else: - row.label(text="", icon="DOT") - - split1 = row.split(factor=0.7) - split1.prop(item, "name", emboss=False, text="") - - op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES") + op = row.operator(self.expand_operator, text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT") op.cost_item = item.ifc_definition_id - row.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + " (M3)") + else: + row.label(text="", icon="DOT") - op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC") - op.cost_item = item.ifc_definition_id - row.label(text="{0:.2f}".format(cost_item["TotalAppliedValue"])) - row.label(text="{0:.2f}".format(cost_item["TotalCostValue"]), icon="CON_TRANSLIKE") + def draw_total_cost_column(self, layout, cost_item): + layout.label(text="{0:.2f}".format(cost_item["TotalCost"])) - if context.active_object: - oprops = context.active_object.BIMObjectProperties - row = layout.row(align=True) - if oprops.ifc_definition_id in cost_item["Controls"]: - op = row.operator("bim.unassign_cost_item_product", text="", icon="KEYFRAME_HLT", emboss=False) - op.cost_item = item.ifc_definition_id - else: - op = row.operator("bim.assign_cost_item_product", text="", icon="KEYFRAME", emboss=False) - op.cost_item = item.ifc_definition_id + def draw_quantity_column(self, layout, cost_item): + if Data.cost_schedules[self.props.active_cost_schedule_id]["PredefinedType"] == "SCHEDULEOFRATES": + self.draw_uom_column(layout, cost_item) + else: + self.draw_total_quantity_column(layout, cost_item) - if props.active_cost_item_id == item.ifc_definition_id: - if props.cost_item_editing_type == "ATTRIBUTES": - row.operator("bim.edit_cost_item", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL") - elif props.active_cost_item_id: - if props.cost_item_editing_type == "VALUES": - op = row.operator("bim.copy_cost_item_values", text="", icon="COPYDOWN") - op.source = props.active_cost_item_id - op.destination = item.ifc_definition_id - row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id - row.operator("bim.remove_cost_item", text="", icon="X").cost_item = item.ifc_definition_id - else: - op = row.operator("bim.select_cost_item_products", icon="RESTRICT_SELECT_OFF", text="") - op.cost_item = item.ifc_definition_id - row.operator( - "bim.enable_editing_cost_item", text="", icon="GREASEPENCIL" - ).cost_item = item.ifc_definition_id - row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = item.ifc_definition_id - row.operator("bim.remove_cost_item", text="", icon="X").cost_item = item.ifc_definition_id + def draw_value_column(self, layout, cost_item): + text = "{0:.2f}".format(cost_item["TotalAppliedValue"]) + if cost_item["UnitBasisValueComponent"] not in [None, 1]: + text += " / {}".format(round(cost_item["UnitBasisValueComponent"], 2)) + layout.label(text=text) + + def draw_uom_column(self, layout, cost_item): + layout.label(text=cost_item["UnitBasisUnitSymbol"] or "?" if cost_item["UnitBasisValueComponent"] else "-") + + def draw_total_quantity_column(self, layout, cost_item): + layout.label(text="{0:.2f}".format(cost_item["TotalCostQuantity"]) + f" {cost_item['UnitSymbol'] or '?'}") + + +class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList): + def __init__(self): + self.contract_operator = "bim.contract_cost_item" + self.expand_operator = "bim.expand_cost_item" + + +class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList): + # A schedule of rates UIList is identical to a regular cost items UIList but + # we want a separate UIList instance so that you can browse both lists + # independently in Blender. So we use a trait. + def __init__(self): + self.contract_operator = "bim.contract_cost_item_rate" + self.expand_operator = "bim.expand_cost_item_rate" + + def draw_quantity_column(self, layout, cost_item): + self.draw_uom_column(layout, cost_item) + + def draw_total_cost_column(self, layout, cost_item): + pass # No such thing as a total cost in a schedule of rates + + +class BIM_UL_cost_columns(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMCostProperties + if item: + row = layout.row(align=True) + row.prop(item, "name", emboss=False, text="") + row.operator("bim.remove_cost_column", text="", icon="X").name = item.name + + +class BIM_UL_cost_item_types(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMCostProperties + cost_item = props.cost_items[props.active_cost_item_index] + + if item: + row = layout.row(align=True) + row.label(text=item.name) + op = row.operator("bim.unassign_cost_item_type", text="", icon="X") + op.cost_item = cost_item.ifc_definition_id + op.related_object = item.ifc_definition_id + + +class BIM_UL_cost_item_quantities(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMCostProperties + cost_item = props.cost_items[props.active_cost_item_index] + + if item: + row = layout.row(align=True) + row.split(factor=0.8) + row.label(text=item.name) + row.label(text="{0:.2f}".format(item.total_quantity)) + op = row.operator("bim.unassign_cost_item_quantity", text="", icon="X") + op.cost_item = cost_item.ifc_definition_id + op.related_object = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/module/covetool/__init__.py b/src/blenderbim/blenderbim/bim/module/covetool/__init__.py index 0113965696..5a51292ed5 100644 --- a/src/blenderbim/blenderbim/bim/module/covetool/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/covetool/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/covetool/api.py b/src/blenderbim/blenderbim/bim/module/covetool/api.py index 50f39fdef4..cff6937d26 100644 --- a/src/blenderbim/blenderbim/bim/module/covetool/api.py +++ b/src/blenderbim/blenderbim/bim/module/covetool/api.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import requests diff --git a/src/blenderbim/blenderbim/bim/module/covetool/operator.py b/src/blenderbim/blenderbim/bim/module/covetool/operator.py index dcd4ec9dbc..e035063b9e 100644 --- a/src/blenderbim/blenderbim/bim/module/covetool/operator.py +++ b/src/blenderbim/blenderbim/bim/module/covetool/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell @@ -13,15 +31,16 @@ api = Api() class Login(bpy.types.Operator): bl_idname = "bim.covetool_login" bl_label = "Login to cove.tool" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - token = api.login(bpy.context.scene.CoveToolProperties.username, bpy.context.scene.CoveToolProperties.password) + token = api.login(context.scene.CoveToolProperties.username, context.scene.CoveToolProperties.password) if token: - bpy.context.scene.CoveToolProperties.token = token + context.scene.CoveToolProperties.token = token projects = api.get_request("projects") for project in projects: - new_project = bpy.context.scene.CoveToolProperties.projects.add() + new_project = context.scene.CoveToolProperties.projects.add() new_project.name = project["name"] new_project.run_set = project["run_set"][0] new_project.url = project["url"] @@ -35,10 +54,10 @@ class RunSimpleAnalysis(bpy.types.Operator): bl_label = "Run Simple Analysis" def execute(self, context): - simple_analysis = bpy.context.scene.CoveToolProperties.simple_analysis + simple_analysis = context.scene.CoveToolProperties.simple_analysis data = { - "run": bpy.context.scene.CoveToolProperties.projects[ - bpy.context.scene.CoveToolProperties.active_project_index + "run": context.scene.CoveToolProperties.projects[ + context.scene.CoveToolProperties.active_project_index ].run_set, "si_units": simple_analysis.si_units, "building_height": simple_analysis.building_height, @@ -83,10 +102,10 @@ class RunAnalysis(bpy.types.Operator): "roofs": [], "shading_devices": [], } - self.parse_objects() + self.parse_objects(context) data = { - "run": bpy.context.scene.CoveToolProperties.projects[ - bpy.context.scene.CoveToolProperties.active_project_index + "run": context.scene.CoveToolProperties.projects[ + context.scene.CoveToolProperties.active_project_index ].run_set, "source": "BlenderBIM", "rotation_angle": self.get_rotation_angle(), @@ -111,14 +130,14 @@ class RunAnalysis(bpy.types.Operator): rotation = 360 - rotation return rotation - def parse_objects(self): - for obj in bpy.context.visible_objects: + def parse_objects(self, context): + for obj in context.visible_objects: covetool_category = self.get_covetool_category(obj) if not covetool_category: continue if not self.has_triangulate_modifier(obj): obj.modifiers.new(name="Triangulate", type="TRIANGULATE") - mesh = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() + mesh = obj.evaluated_get(context.evaluated_depsgraph_get()).to_mesh() meshes = {} for polygon in mesh.polygons: normal = "{}|{}|{}".format( @@ -196,5 +215,4 @@ class RunAnalysis(bpy.types.Operator): return True def is_window_skylight(self, element): - predefined_type = element.get_info().get("PredefinedType") - return predefined_type and predefined_type.string_value == "SKYLIGHT" + return element.get_info().get("PredefinedType") == "SKYLIGHT" diff --git a/src/blenderbim/blenderbim/bim/module/covetool/prop.py b/src/blenderbim/blenderbim/bim/module/covetool/prop.py index 122d6b1cb1..68ad2188a0 100644 --- a/src/blenderbim/blenderbim/bim/module/covetool/prop.py +++ b/src/blenderbim/blenderbim/bim/module/covetool/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy.props import bpy.types diff --git a/src/blenderbim/blenderbim/bim/module/covetool/ui.py b/src/blenderbim/blenderbim/bim/module/covetool/ui.py index 70b35ef6c0..e67e654caf 100644 --- a/src/blenderbim/blenderbim/bim/module/covetool/ui.py +++ b/src/blenderbim/blenderbim/bim/module/covetool/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy.types diff --git a/src/blenderbim/blenderbim/bim/module/csv/__init__.py b/src/blenderbim/blenderbim/bim/module/csv/__init__.py index 3e0e3722b9..238b4698cb 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/csv/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -13,11 +31,9 @@ classes = ( ) - def register(): bpy.types.Scene.CsvProperties = bpy.props.PointerProperty(type=prop.CsvProperties) def unregister(): del bpy.types.Scene.CsvProperties - diff --git a/src/blenderbim/blenderbim/bim/module/csv/operator.py b/src/blenderbim/blenderbim/bim/module/csv/operator.py index 8618de15ca..e5f248821c 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/operator.py +++ b/src/blenderbim/blenderbim/bim/module/csv/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifccsv import ifcopenshell diff --git a/src/blenderbim/blenderbim/bim/module/csv/prop.py b/src/blenderbim/blenderbim/bim/module/csv/prop.py index a21893cc94..39418d754c 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/prop.py +++ b/src/blenderbim/blenderbim/bim/module/csv/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty from bpy.types import PropertyGroup diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py index 025dfc7396..819401c612 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel from blenderbim.bim.ifc import IfcStore diff --git a/src/blenderbim/blenderbim/bim/module/debug/__init__.py b/src/blenderbim/blenderbim/bim/module/debug/__init__.py index e13cdf8acc..11c6a2d54d 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/debug/__init__.py @@ -1,8 +1,28 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator classes = ( operator.PrintIfcFile, + operator.PurgeIfcLinks, + operator.PrintObjectPlacement, operator.ValidateIfcFile, operator.ProfileImportIFC, operator.CreateAllShapes, diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index ff4dbf56b7..203371d748 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -1,6 +1,28 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy +import time import logging import ifcopenshell +import ifcopenshell.util.placement +import ifcopenshell.util.representation +import blenderbim.bim.handler import blenderbim.bim.import_ifc as import_ifc from blenderbim.bim.ifc import IfcStore @@ -9,15 +31,42 @@ class PrintIfcFile(bpy.types.Operator): bl_idname = "bim.print_ifc_file" bl_label = "Print IFC File" + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): print(IfcStore.get_file().wrapped_data.to_string()) return {"FINISHED"} +class PurgeIfcLinks(bpy.types.Operator): + bl_idname = "bim.purge_ifc_links" + bl_label = "Purge IFC Links" + + def execute(self, context): + for obj in bpy.data.objects: + if obj.type in {"MESH", "EMPTY"}: + obj.BIMObjectProperties.ifc_definition_id = 0 + if obj.data: + obj.data.BIMMeshProperties.ifc_definition_id = 0 + for material in bpy.data.materials: + material.BIMMaterialProperties.ifc_style_id = False + context.scene.BIMProperties.ifc_file = "" + context.scene.BIMDebugProperties.attributes.clear() + IfcStore.purge() + blenderbim.bim.handler.purge_module_data() + return {"FINISHED"} + + class ValidateIfcFile(bpy.types.Operator): bl_idname = "bim.validate_ifc_file" bl_label = "Validate IFC File" + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): import ifcopenshell.validate @@ -31,16 +80,15 @@ class ProfileImportIFC(bpy.types.Operator): bl_idname = "bim.profile_import_ifc" bl_label = "Profile Import IFC" + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.scene.BIMProperties.ifc_file + def execute(self, context): import cProfile import pstats - # For Windows - filepath = bpy.context.scene.BIMProperties.ifc_file.replace('\\', '\\\\') - - cProfile.run( - f"import bpy; bpy.ops.import_ifc.bim(filepath='{filepath}')", "blender.prof" - ) + cProfile.run("import bpy; bpy.ops.bim.load_project_elements()", "blender.prof") p = pstats.Stats("blender.prof") p.sort_stats("cumulative").print_stats(50) return {"FINISHED"} @@ -50,24 +98,35 @@ class CreateAllShapes(bpy.types.Operator): bl_idname = "bim.create_all_shapes" bl_label = "Create All Shapes" + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): self.file = IfcStore.get_file() elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace") total = len(elements) settings = ifcopenshell.geom.settings() failures = [] - excludes = () # For the developer to debug with + excludes = () # For the developer to debug with for i, element in enumerate(elements): if element.GlobalId in excludes: continue - print(f'{i}/{total}:', element) + print(f"{i}/{total}:", element) + start = time.time() try: shape = ifcopenshell.geom.create_shape(settings, element) - print("Success", len(shape.geometry.verts), len(shape.geometry.edges), len(shape.geometry.faces)) + print( + "Success", + time.time() - start, + len(shape.geometry.verts), + len(shape.geometry.edges), + len(shape.geometry.faces), + ) except: failures.append(element) - print('***** FAILURE *****') - print('Failures:') + print("***** FAILURE *****") + print("Failures:") for failure in failures: print(failure) return {"FINISHED"} @@ -76,39 +135,44 @@ class CreateAllShapes(bpy.types.Operator): class CreateShapeFromStepId(bpy.types.Operator): bl_idname = "bim.create_shape_from_step_id" bl_label = "Create Shape From STEP ID" + bl_options = {"REGISTER", "UNDO"} + should_include_curves: bpy.props.BoolProperty() + + @classmethod + def poll(cls, context): + return IfcStore.get_file() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): logger = logging.getLogger("ImportIFC") - self.ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, logger) + self.ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) self.file = IfcStore.get_file() - element = self.file.by_id(int(bpy.context.scene.BIMDebugProperties.step_id)) + element = self.file.by_id(int(context.scene.BIMDebugProperties.step_id)) settings = ifcopenshell.geom.settings() - # settings.set(settings.INCLUDE_CURVES, True) + if self.should_include_curves: + settings.set(settings.INCLUDE_CURVES, True) shape = ifcopenshell.geom.create_shape(settings, element) ifc_importer = import_ifc.IfcImporter(self.ifc_import_settings) ifc_importer.file = self.file mesh = ifc_importer.create_mesh(element, shape) obj = bpy.data.objects.new("Debug", mesh) - bpy.context.scene.collection.objects.link(obj) + context.scene.collection.objects.link(obj) return {"FINISHED"} class SelectHighPolygonMeshes(bpy.types.Operator): bl_idname = "bim.select_high_polygon_meshes" bl_label = "Select High Polygon Meshes" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - results = {} - for obj in bpy.data.objects: - if not isinstance(obj.data, bpy.types.Mesh) or len(obj.data.polygons) < int( - bpy.context.scene.BIMDebugProperties.number_of_polygons - ): - continue - try: - obj.select_set(True) - except: - # If it is not in the view layer - pass + [ + o.select_set(True) + for o in context.view_layer.objects + if o.type == "MESH" and len(o.data.polygons) > context.scene.BIMDebugProperties.number_of_polygons + ] return {"FINISHED"} @@ -117,7 +181,7 @@ class RewindInspector(bpy.types.Operator): bl_label = "Rewind Inspector" def execute(self, context): - props = bpy.context.scene.BIMDebugProperties + props = context.scene.BIMDebugProperties total_breadcrumbs = len(props.step_id_breadcrumb) if total_breadcrumbs < 2: return {"FINISHED"} @@ -133,20 +197,22 @@ class InspectFromStepId(bpy.types.Operator): bl_label = "Inspect From STEP ID" step_id: bpy.props.IntProperty() + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): self.file = IfcStore.get_file() - bpy.context.scene.BIMDebugProperties.active_step_id = self.step_id - crumb = bpy.context.scene.BIMDebugProperties.step_id_breadcrumb.add() + debug_props = context.scene.BIMDebugProperties + debug_props.active_step_id = self.step_id + crumb = context.scene.BIMDebugProperties.step_id_breadcrumb.add() crumb.name = str(self.step_id) element = self.file.by_id(self.step_id) - while len(bpy.context.scene.BIMDebugProperties.attributes) > 0: - bpy.context.scene.BIMDebugProperties.attributes.remove(0) - while len(bpy.context.scene.BIMDebugProperties.inverse_attributes) > 0: - bpy.context.scene.BIMDebugProperties.inverse_attributes.remove(0) - while len(bpy.context.scene.BIMDebugProperties.inverse_references) > 0: - bpy.context.scene.BIMDebugProperties.inverse_references.remove(0) + debug_props.attributes.clear() + debug_props.inverse_attributes.clear() + debug_props.inverse_references.clear() for key, value in element.get_info().items(): - self.add_attribute(bpy.context.scene.BIMDebugProperties.attributes, key, value) + self.add_attribute(debug_props.attributes, key, value) for key in dir(element): if ( not key[0].isalpha() @@ -155,9 +221,9 @@ class InspectFromStepId(bpy.types.Operator): or not getattr(element, key) ): continue - self.add_attribute(bpy.context.scene.BIMDebugProperties.inverse_attributes, key, getattr(element, key)) + self.add_attribute(debug_props.inverse_attributes, key, getattr(element, key)) for inverse in self.file.get_inverse(element): - new = bpy.context.scene.BIMDebugProperties.inverse_references.add() + new = debug_props.inverse_references.add() new.string_value = str(inverse) new.int_value = inverse.id() return {"FINISHED"} @@ -181,8 +247,18 @@ class InspectFromObject(bpy.types.Operator): bl_label = "Inspect From Object" def execute(self, context): - ifc_definition_id = bpy.context.active_object.BIMObjectProperties.ifc_definition_id + ifc_definition_id = context.active_object.BIMObjectProperties.ifc_definition_id if not ifc_definition_id: return {"FINISHED"} bpy.ops.bim.inspect_from_step_id(step_id=ifc_definition_id) return {"FINISHED"} + + +class PrintObjectPlacement(bpy.types.Operator): + bl_idname = "bim.print_object_placement" + bl_label = "Print Object Placement" + step_id: bpy.props.IntProperty() + + def execute(self, context): + print(ifcopenshell.util.placement.get_local_placement(IfcStore.get_file().by_id(self.step_id))) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/debug/prop.py b/src/blenderbim/blenderbim/bim/module/debug/prop.py index cc1ef6213c..2257fa1e52 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/prop.py +++ b/src/blenderbim/blenderbim/bim/module/debug/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -14,7 +32,7 @@ from bpy.props import ( class BIMDebugProperties(PropertyGroup): step_id: IntProperty(name="STEP ID") - number_of_polygons: IntProperty(name="Number of Polygons") + number_of_polygons: IntProperty(name="Number of Polygons", min=0) active_step_id: IntProperty(name="STEP ID") step_id_breadcrumb: CollectionProperty(name="STEP ID Breadcrumb", type=StrProperty) attributes: CollectionProperty(name="Attributes", type=Attribute) diff --git a/src/blenderbim/blenderbim/bim/module/debug/ui.py b/src/blenderbim/blenderbim/bim/module/debug/ui.py index 5519f6dd86..ea14629b81 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/ui.py +++ b/src/blenderbim/blenderbim/bim/module/debug/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Panel @@ -23,21 +41,23 @@ class BIM_PT_debug(Panel): row = layout.row() row.operator("bim.print_ifc_file") + row = layout.row() + row.operator("bim.purge_ifc_links") + row = layout.row() row.operator("bim.create_all_shapes") row = layout.row() row.operator("bim.profile_import_ifc") - row = layout.row() + row = layout.split(factor=0.5, align=True) + row.operator("bim.create_shape_from_step_id").should_include_curves = False + row.operator("bim.create_shape_from_step_id", text="", icon="IPO_ELASTIC").should_include_curves = True row.prop(props, "step_id", text="") - row = layout.row() - row.operator("bim.create_shape_from_step_id") - row = layout.row() - row.prop(props, "number_of_polygons", text="") - row = layout.row() + row = layout.split(factor=0.7, align=True) row.operator("bim.select_high_polygon_meshes") + row.prop(props, "number_of_polygons", text="") layout.label(text="Inspector:") @@ -46,7 +66,7 @@ class BIM_PT_debug(Panel): row.operator("bim.rewind_inspector", icon="FRAME_PREV", text="") row.prop(props, "active_step_id", text="") row = layout.row(align=True) - row.operator("bim.inspect_from_step_id").step_id = bpy.context.scene.BIMDebugProperties.active_step_id + row.operator("bim.inspect_from_step_id").step_id = context.scene.BIMDebugProperties.active_step_id row.operator("bim.inspect_from_object") if props.attributes: @@ -59,6 +79,9 @@ class BIM_PT_debug(Panel): if attribute.name == "GlobalId": op = row.operator("bim.select_global_id", icon="RESTRICT_SELECT_OFF", text="") op.global_id = attribute.string_value + if attribute.name == "ObjectPlacement": + op = row.operator("bim.print_object_placement", icon="TRACKER", text="") + op.step_id = attribute.int_value if attribute.int_value: row.operator( "bim.inspect_from_step_id", icon="DISCLOSURE_TRI_RIGHT", text="" diff --git a/src/blenderbim/blenderbim/bim/module/diff/__init__.py b/src/blenderbim/blenderbim/bim/module/diff/__init__.py index 6c551c11ce..02c9fbda20 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/diff/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -12,11 +30,9 @@ classes = ( ) - def register(): bpy.types.Scene.DiffProperties = bpy.props.PointerProperty(type=prop.DiffProperties) def unregister(): del bpy.types.Scene.DiffProperties - diff --git a/src/blenderbim/blenderbim/bim/module/diff/operator.py b/src/blenderbim/blenderbim/bim/module/diff/operator.py index 5ca9dfefc8..574c79a1b7 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/operator.py +++ b/src/blenderbim/blenderbim/bim/module/diff/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifccsv import ifcopenshell @@ -5,14 +23,14 @@ import json from blenderbim.bim.ifc import IfcStore - class SelectDiffJsonFile(bpy.types.Operator): bl_idname = "bim.select_diff_json_file" bl_label = "Select Diff JSON File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.DiffProperties.diff_json_file = self.filepath + context.scene.DiffProperties.diff_json_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -23,24 +41,25 @@ class SelectDiffJsonFile(bpy.types.Operator): class VisualiseDiff(bpy.types.Operator): bl_idname = "bim.visualise_diff" bl_label = "Visualise Diff" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - #ifc_file = IfcStore.get_file() # In case we get from Store - ifc_file = ifcopenshell.open(context.scene.DiffProperties.diff_new_file) # for Now refer to the new file - with open(bpy.context.scene.DiffProperties.diff_json_file, "r") as file: + # ifc_file = IfcStore.get_file() # In case we get from Store + ifc_file = ifcopenshell.open(context.scene.DiffProperties.diff_new_file) # for Now refer to the new file + with open(context.scene.DiffProperties.diff_json_file, "r") as file: diff = json.load(file) - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: obj.color = (1.0, 1.0, 1.0, 0.2) global_id = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId if not global_id: continue - if global_id.string_value in diff["deleted"]: + if global_id in diff["deleted"]: obj.color = (1.0, 0.0, 0.0, 0.2) - elif global_id.string_value in diff["added"]: + elif global_id in diff["added"]: obj.color = (0.0, 1.0, 0.0, 0.2) - elif global_id.string_value in diff["changed"]: + elif global_id in diff["changed"]: obj.color = (0.0, 0.0, 1.0, 0.2) - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area = next(area for area in context.screen.areas if area.type == "VIEW_3D") area.spaces[0].shading.color_type = "OBJECT" return {"FINISHED"} @@ -48,10 +67,11 @@ class VisualiseDiff(bpy.types.Operator): class SelectDiffOldFile(bpy.types.Operator): bl_idname = "bim.select_diff_old_file" bl_label = "Select Diff Old File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.DiffProperties.diff_old_file = self.filepath + context.scene.DiffProperties.diff_old_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -62,10 +82,11 @@ class SelectDiffOldFile(bpy.types.Operator): class SelectDiffNewFile(bpy.types.Operator): bl_idname = "bim.select_diff_new_file" bl_label = "Select Diff New File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.DiffProperties.diff_new_file = self.filepath + context.scene.DiffProperties.diff_new_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -89,12 +110,12 @@ class ExecuteIfcDiff(bpy.types.Operator): import ifcdiff ifc_diff = ifcdiff.IfcDiff( - bpy.context.scene.DiffProperties.diff_old_file, - bpy.context.scene.DiffProperties.diff_new_file, + context.scene.DiffProperties.diff_old_file, + context.scene.DiffProperties.diff_new_file, self.filepath, - bpy.context.scene.DiffProperties.diff_relationships.split(), + context.scene.DiffProperties.diff_relationships.split(), ) ifc_diff.diff() ifc_diff.export() - bpy.context.scene.DiffProperties.diff_json_file = self.filepath + context.scene.DiffProperties.diff_json_file = self.filepath return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/diff/prop.py b/src/blenderbim/blenderbim/bim/module/diff/prop.py index 3de92192a7..1ae467c960 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/prop.py +++ b/src/blenderbim/blenderbim/bim/module/diff/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty from bpy.types import PropertyGroup diff --git a/src/blenderbim/blenderbim/bim/module/diff/ui.py b/src/blenderbim/blenderbim/bim/module/diff/ui.py index 692b79d37e..525455329c 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/ui.py +++ b/src/blenderbim/blenderbim/bim/module/diff/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel from blenderbim.bim.ifc import IfcStore diff --git a/src/blenderbim/blenderbim/bim/module/document/__init__.py b/src/blenderbim/blenderbim/bim/module/document/__init__.py index cd2ef94e5d..edfe7a60ef 100644 --- a/src/blenderbim/blenderbim/bim/module/document/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/document/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/document/operator.py b/src/blenderbim/blenderbim/bim/module/document/operator.py index 906fbb97e5..a81a053524 100644 --- a/src/blenderbim/blenderbim/bim/module/document/operator.py +++ b/src/blenderbim/blenderbim/bim/module/document/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell.api @@ -14,8 +32,7 @@ class LoadInformation(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMDocumentProperties - while len(props.documents) > 0: - props.documents.remove(0) + props.documents.clear() for information_id, information in Data.information.items(): new = props.documents.add() new.name = information["Name"] or "Unnamed" @@ -37,8 +54,7 @@ class LoadDocumentReferences(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMDocumentProperties - while len(props.documents) > 0: - props.documents.remove(0) + props.documents.clear() for reference_id, reference in Data.references.items(): new = props.documents.add() new.name = reference["Name"] or "Unnamed" @@ -71,8 +87,7 @@ class EnableEditingDocument(bpy.types.Operator): def execute(self, context): props = context.scene.BIMDocumentProperties - while len(props.document_attributes) > 0: - props.document_attributes.remove(0) + props.document_attributes.clear() if props.is_editing == "information": data = Data.information[self.document] @@ -260,17 +275,21 @@ class AssignDocument(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() - ifcopenshell.api.run( - "document.assign_document", - self.file, - **{ - "product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), - "document": self.file.by_id(self.document), - } - ) - Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects + for obj in objs: + obj_id = obj.BIMObjectProperties.ifc_definition_id + if not obj_id: + continue + ifcopenshell.api.run( + "document.assign_document", + self.file, + **{ + "product": self.file.by_id(obj_id), + "document": self.file.by_id(self.document), + } + ) + Data.load(self.file, obj_id) return {"FINISHED"} @@ -285,15 +304,19 @@ class UnassignDocument(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() - ifcopenshell.api.run( - "document.unassign_document", - self.file, - **{ - "product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), - "document": self.file.by_id(self.document), - } - ) - Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects + for obj in objs: + obj_id = obj.BIMObjectProperties.ifc_definition_id + if not obj_id: + continue + ifcopenshell.api.run( + "document.unassign_document", + self.file, + **{ + "product": self.file.by_id(obj_id), + "document": self.file.by_id(self.document), + } + ) + Data.load(self.file, obj_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/document/prop.py b/src/blenderbim/blenderbim/bim/module/document/prop.py index 2208551acf..a470e715ae 100644 --- a/src/blenderbim/blenderbim/bim/module/document/prop.py +++ b/src/blenderbim/blenderbim/bim/module/document/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup diff --git a/src/blenderbim/blenderbim/bim/module/document/ui.py b/src/blenderbim/blenderbim/bim/module/document/ui.py index 0bbafafb77..2eb5e8b482 100644 --- a/src/blenderbim/blenderbim/bim/module/document/ui.py +++ b/src/blenderbim/blenderbim/bim/module/document/ui.py @@ -1,5 +1,24 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.document.data import Data @@ -26,7 +45,7 @@ class BIM_PT_documents(Panel): row.label(text="{} Documents Found".format(len(Data.information)), icon="FILE") if self.props.is_editing == "information": row.operator("bim.add_information", text="", icon="ADD") - row.operator("bim.disable_document_editing_ui", text="", icon="X") + row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL") else: row.operator("bim.load_information", text="", icon="IMPORT") @@ -35,7 +54,7 @@ class BIM_PT_documents(Panel): row.label(text="{} References Found".format(len(Data.references)), icon="FILE_HIDDEN") if self.props.is_editing == "reference": row.operator("bim.add_document_reference", text="", icon="ADD") - row.operator("bim.disable_document_editing_ui", text="", icon="X") + row.operator("bim.disable_document_editing_ui", text="", icon="CANCEL") else: row.operator("bim.load_document_references", text="", icon="IMPORT") @@ -48,14 +67,7 @@ class BIM_PT_documents(Panel): self.draw_editable_ui(context) def draw_editable_ui(self, context): - for attribute in self.props.document_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.document_attributes, self.layout) class BIM_PT_object_documents(Panel): @@ -116,7 +128,7 @@ class BIM_PT_object_documents(Panel): row = self.layout.row(align=True) icon = "FILE" if self.props.is_adding == "IfcDocumentInformation" else "FILE_HIDDEN" row.label(text="Adding {}".format(self.props.is_adding), icon=icon) - row.operator("bim.disable_assigning_document", text="", icon="X") + row.operator("bim.disable_assigning_document", text="", icon="CANCEL") self.layout.template_list( "BIM_UL_object_documents", "", @@ -142,7 +154,7 @@ class BIM_UL_documents(UIList): row.operator("bim.edit_information", text="", icon="CHECKMARK") elif context.scene.BIMDocumentProperties.is_editing == "reference": row.operator("bim.edit_document_reference", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_document", text="", icon="X") + row.operator("bim.disable_editing_document", text="", icon="CANCEL") elif context.scene.BIMDocumentProperties.active_document_id: row.operator("bim.remove_document", text="", icon="X").document = item.ifc_definition_id else: diff --git a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py index e289ea4aa1..b24d93f953 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator, handler, gizmos diff --git a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py index 7853d424aa..085be95aa1 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/annotation.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/annotation.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Maxim Vasilyev +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import os from mathutils import Vector @@ -16,13 +34,13 @@ class Annotator: return float(sizes[str(size)]) @staticmethod - def add_text(related_element=None): + def add_text(context, related_element=None): curve = bpy.data.curves.new(type="FONT", name="Text") curve.body = "TEXT" obj = bpy.data.objects.new("Text", curve) - obj.matrix_world = bpy.context.scene.camera.matrix_world + obj.matrix_world = context.scene.camera.matrix_world if related_element is None: - location, _, _, _ = Annotator.get_placeholder_coords() + location, _, _, _ = Annotator.get_placeholder_coords(context) else: obj.data.BIMTextProperties.related_element = related_element location = related_element.location @@ -31,12 +49,12 @@ class Annotator: font = bpy.data.fonts.get("OpenGost TypeB TT") if not font: font = bpy.data.fonts.load( - os.path.join(bpy.context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf") + os.path.join(context.scene.BIMProperties.data_dir, "fonts", "OpenGost Type B TT.ttf") ) font.name = "OpenGost Type B TT" obj.data.font = font obj.data.BIMTextProperties.font_size = "2.5" - collection = bpy.context.scene.camera.users_collection[0] + collection = context.scene.camera.users_collection[0] collection.objects.link(obj) Annotator.resize_text(obj) return obj @@ -64,9 +82,9 @@ class Annotator: text_obj.data.size = font_size @staticmethod - def add_line_to_annotation(obj, co1=None, co2=None): + def add_line_to_annotation(obj, context, co1=None, co2=None): if co1 is None: - co1, co2, _, _ = Annotator.get_placeholder_coords() + co1, co2, _, _ = Annotator.get_placeholder_coords(context) co1 = obj.matrix_world.inverted() @ co1 co2 = obj.matrix_world.inverted() @ co2 if isinstance(obj.data, bpy.types.Mesh): @@ -83,8 +101,8 @@ class Annotator: return obj @staticmethod - def add_plane_to_annotation(obj): - co1, co2, co3, co4 = Annotator.get_placeholder_coords() + def add_plane_to_annotation(obj, context): + co1, co2, co3, co4 = Annotator.get_placeholder_coords(context) co1 = obj.matrix_world.inverted() @ co1 # bot left co2 = obj.matrix_world.inverted() @ co2 # top left co3 = obj.matrix_world.inverted() @ co3 # bot right @@ -132,8 +150,8 @@ class Annotator: return obj @staticmethod - def get_annotation_obj(name, data_type): - collection = bpy.context.scene.camera.users_collection[0] + def get_annotation_obj(name, data_type, context): + collection = context.scene.camera.users_collection[0] for obj in collection.objects: if name in obj.name: return obj @@ -148,20 +166,18 @@ class Annotator: return obj @staticmethod - def get_placeholder_coords(): - camera = bpy.context.scene.camera + def get_placeholder_coords(context): + camera = context.scene.camera z_offset = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)) - if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y: - y = ( - camera.data.ortho_scale - * (bpy.context.scene.render.resolution_y / bpy.context.scene.render.resolution_x) - / 4 - ) + if context.scene.render.resolution_x > context.scene.render.resolution_y: + y = camera.data.ortho_scale * (context.scene.render.resolution_y / context.scene.render.resolution_x) / 4 else: y = camera.data.ortho_scale / 4 y_offset = camera.matrix_world.to_quaternion() @ Vector((0, y, 0)) - x_offset = camera.matrix_world.to_quaternion() @ Vector((y/2, 0, 0)) - return (camera.location + z_offset, - camera.location + z_offset + y_offset, - camera.location + z_offset + x_offset, - camera.location + z_offset + x_offset + y_offset) + x_offset = camera.matrix_world.to_quaternion() @ Vector((y / 2, 0, 0)) + return ( + camera.location + z_offset, + camera.location + z_offset + y_offset, + camera.location + z_offset + x_offset, + camera.location + z_offset + x_offset + y_offset, + ) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index ac36c8506c..1e17b5e9f9 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Maxim Vasilyev +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + """Viewport decorations""" import math from functools import reduce @@ -16,7 +34,7 @@ from gpu_extras.batch import batch_for_shader import blenderbim.bim.module.drawing.helper as helper -class BaseDecorator(): +class BaseDecorator: # base name of objects to decorate basename = "IfcAnnotation/Something" @@ -109,10 +127,12 @@ class BaseDecorator(): def __init__(self): # NB: libcode param doesn't work - self.shader = GPUShader(vertexcode=self.VERT_GLSL, - fragcode=self.FRAG_GLSL, - geocode=self.LIB_GLSL + self.GEOM_GLSL, - defines=self.DEF_GLSL) + self.shader = GPUShader( + vertexcode=self.VERT_GLSL, + fragcode=self.FRAG_GLSL, + geocode=self.LIB_GLSL + self.GEOM_GLSL, + defines=self.DEF_GLSL, + ) def get_objects(self, collection): """find relevant objects @@ -153,7 +173,7 @@ class BaseDecorator(): topology.append(1) topology.extend([0] * max(0, cnt - 2)) topology.append(2) - indices.extend((idx+i, idx+i+1) for i in range(cnt-1)) + indices.extend((idx + i, idx + i + 1) for i in range(cnt - 1)) idx += cnt return vertices, indices, topology @@ -173,8 +193,7 @@ class BaseDecorator(): return vertices, indices def get_editmesh_geom(self, obj): - """Parses editmode mesh geometry into line segments - """ + """Parses editmode mesh geometry into line segments""" mesh = bmesh.from_edit_mesh(obj.data) vertices = [] indices = [] @@ -182,7 +201,7 @@ class BaseDecorator(): for edge in mesh.edges: vertices.extend(edge.verts) - indices.append((idx, idx+1)) + indices.append((idx, idx + 1)) idx += 2 vertices = [obj.matrix_world @ v.co for v in vertices] @@ -198,18 +217,18 @@ class BaseDecorator(): color = context.scene.DocProperties.decorations_colour fmt = GPUVertFormat() - fmt.attr_add(id="pos", comp_type='F32', len=3, fetch_mode='FLOAT') + fmt.attr_add(id="pos", comp_type="F32", len=3, fetch_mode="FLOAT") if topology: - fmt.attr_add(id="topo", comp_type='U8', len=1, fetch_mode='INT') + fmt.attr_add(id="topo", comp_type="U8", len=1, fetch_mode="INT") vbo = GPUVertBuf(len=len(vertices), format=fmt) vbo.attr_fill(id="pos", data=vertices) if topology: vbo.attr_fill(id="topo", data=topology) - ibo = GPUIndexBuf(type='LINES', seq=indices) + ibo = GPUIndexBuf(type="LINES", seq=indices) - batch = GPUBatch(type='LINES', buf=vbo, elem=ibo) + batch = GPUBatch(type="LINES", buf=vbo, elem=ibo) bgl.glEnable(bgl.GL_LINE_SMOOTH) bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST) @@ -270,7 +289,7 @@ class BaseDecorator(): def format_value(self, context, value): unit_system = context.scene.unit_settings.system - if unit_system == 'IMPERIAL': + if unit_system == "IMPERIAL": precision = context.scene.BIMProperties.imperial_precision if precision == "NONE": precision = 256 @@ -278,12 +297,12 @@ class BaseDecorator(): precision = 1 elif "/" in precision: precision = int(precision.split("/")[1]) - elif unit_system == 'METRIC': + elif unit_system == "METRIC": precision = 3 else: return - return bpy.utils.units.to_string(unit_system, 'LENGTH', value, precision, split_unit=True) + return bpy.utils.units.to_string(unit_system, "LENGTH", value, precision, split_unit=True) class DimensionDecorator(BaseDecorator): @@ -291,12 +310,16 @@ class DimensionDecorator(BaseDecorator): - each edge of a segment with arrow - puts metric text next to each segment """ + basename = "IfcAnnotation/Dimension" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define ARROW_ANGLE PI / 12.0 #define ARROW_SIZE 16.0 """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -373,7 +396,7 @@ class DimensionDecorator(BaseDecorator): continue length = (v1 - v0).length text = self.format_value(context, length) - self.draw_label(context, text, p0 + (dir) * .5, dir) + self.draw_label(context, text, p0 + (dir) * 0.5, dir) class EqualityDecorator(DimensionDecorator): @@ -382,6 +405,7 @@ class EqualityDecorator(DimensionDecorator): - augments with arrows on both sides - puts 'EQ' label """ + basename = "IfcAnnotation/Equal" def draw_labels(self, context, obj, vertices, indices): @@ -395,7 +419,7 @@ class EqualityDecorator(DimensionDecorator): dir = p1 - p0 if dir.length < 1: continue - self.draw_label(context, "EQ", p0 + (dir) * .5, dir) + self.draw_label(context, "EQ", p0 + (dir) * 0.5, dir) class LeaderDecorator(BaseDecorator): @@ -403,12 +427,16 @@ class LeaderDecorator(BaseDecorator): - head point with arrow - middle points w/out decorations """ + basename = "IfcAnnotation/Leader" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define ARROW_ANGLE PI / 12.0 #define ARROW_SIZE 16.0 """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -471,13 +499,17 @@ class StairDecorator(BaseDecorator): - tail point with circle - middle points w/out decorations """ + basename = "IfcAnnotation/Stair" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define CIRCLE_SIZE 8.0 #define ARROW_ANGLE PI / 3.0 #define ARROW_SIZE 24.0 """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -553,10 +585,13 @@ class StairDecorator(BaseDecorator): class HiddenDecorator(BaseDecorator): basename = "IfcAnnotation/Hidden" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define DASH_SIZE 16.0 #define DASH_PATTERN 0x0000FFFFU """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -620,7 +655,6 @@ class MiscDecorator(HiddenDecorator): class LevelDecorator(BaseDecorator): - def get_splines(self, obj): """Iterates through splines Args: @@ -645,10 +679,13 @@ class LevelDecorator(BaseDecorator): class PlanLevelDecorator(LevelDecorator): basename = "IfcAnnotation/Plan Level" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define CIRCLE_SIZE 8.0 #define CROSS_SIZE 16.0 """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -727,10 +764,13 @@ class PlanLevelDecorator(LevelDecorator): class SectionLevelDecorator(LevelDecorator): basename = "IfcAnnotation/Section Level" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define CALLOUT_GAP 8.0 #define CALLOUT_SIZE 64.0 """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -809,12 +849,16 @@ class BreakDecorator(BaseDecorator): Uses first two vertices in verts list. """ + basename = "IfcAnnotation/Break" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define BREAK_LENGTH 32.0 #define BREAK_WIDTH 16.0 """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -886,11 +930,14 @@ class BreakDecorator(BaseDecorator): class GridDecorator(BaseDecorator): basename = "IfcGridAxis/" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define CIRCLE_SIZE 16.0 #define DASH_SIZE 48.0 #define DASH_PATTERN 0x03C0FFFFU """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -1000,11 +1047,14 @@ class GridDecorator(BaseDecorator): class SectionViewDecorator(LevelDecorator): basename = "IfcAnnotation/Section" - DEF_GLSL = BaseDecorator.DEF_GLSL + """ + DEF_GLSL = ( + BaseDecorator.DEF_GLSL + + """ #define CIRCLE_SIZE 8.0 #define TRIANGLE_L 32.0 #define TRIANGLE_W 16.0 """ + ) GEOM_GLSL = """ uniform vec2 winsize; @@ -1099,7 +1149,7 @@ class SectionViewDecorator(LevelDecorator): self.draw_lines(context, obj, verts, [(0, 1)]) -class DecorationsHandler(): +class DecorationsHandler: decorators_classes = [ DimensionDecorator, EqualityDecorator, @@ -1111,7 +1161,7 @@ class DecorationsHandler(): SectionLevelDecorator, StairDecorator, BreakDecorator, - SectionViewDecorator + SectionViewDecorator, ] installed = None @@ -1121,12 +1171,12 @@ class DecorationsHandler(): if cls.installed: cls.uninstall() handler = cls() - cls.installed = SpaceView3D.draw_handler_add(handler, (context,), 'WINDOW', 'POST_PIXEL') + cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL") @classmethod def uninstall(cls): try: - SpaceView3D.draw_handler_remove(cls.installed, 'WINDOW') + SpaceView3D.draw_handler_remove(cls.installed, "WINDOW") except ValueError: pass cls.installed = None diff --git a/src/blenderbim/blenderbim/bim/module/drawing/gizmos.py b/src/blenderbim/blenderbim/bim/module/drawing/gizmos.py index 65fbe7e339..e939713dae 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/gizmos.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/gizmos.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Maxim Vasilyev +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import blf import math @@ -7,6 +25,7 @@ from mathutils import Vector, Matrix from mathutils import geometry from bpy_extras import view3d_utils from blenderbim.bim.module.drawing.shaders import DotsGizmoShader, ExtrusionGuidesShader, BaseLinesShader +from ifcopenshell.util.unit import si_conversions """Gizmos under the hood @@ -451,8 +470,6 @@ class ExtrusionWidget(types.GizmoGroup): bl_region_type = "WINDOW" bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"} - # FIXME: use proper scale from ifc value to blender units - @classmethod def poll(cls, ctx): obj = ctx.object @@ -468,6 +485,7 @@ class ExtrusionWidget(types.GizmoGroup): basis = target.matrix_world.normalized() theme = ctx.preferences.themes[0].user_interface + scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit) gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d") gz.matrix_basis = basis @@ -477,7 +495,7 @@ class ExtrusionWidget(types.GizmoGroup): gz.alpha_highlight = 1.0 gz.use_draw_modal = True gz.target_set_prop("offset", prop, "value") - gz.scale_value = 1000 + gz.scale_value = scale_value gz = self.guides = self.gizmos.new("BIM_GT_extrusion_guides") gz.matrix_basis = basis @@ -485,7 +503,7 @@ class ExtrusionWidget(types.GizmoGroup): gz.alpha = gz.alpha_highlight = 0.5 gz.use_draw_modal = True gz.target_set_prop("depth", prop, "value") - gz.scale_value = 1000 + gz.scale_value = scale_value # gz = self.label = self.gizmos.new('GIZMO_GT_dimension_label') # gz.matrix_basis = basis @@ -504,10 +522,30 @@ class ExtrusionWidget(types.GizmoGroup): def update(self, ctx): """updating object""" bpy.ops.bim.update_parametric_representation() - # parameters disappear after update - # need to retrieve and rebind them again - bpy.ops.bim.get_representation_ifc_parameters() target = ctx.object prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") self.handle.target_set_prop("offset", prop, "value") self.guides.target_set_prop("depth", prop, "value") + + @staticmethod + def get_scale_value(system, length_unit): + scale_value = 1 + if system == "METRIC": + if length_unit == "KILOMETERS": + scale_value /= 1000 + elif length_unit == "CENTIMETERS": + scale_value *= 100 + elif length_unit == "MILLIMETERS": + scale_value *= 1000 + elif length_unit == "MICROMETERS": + scale_value *= 1000000 + elif system == "IMPERIAL": + if length_unit == "MILES": + scale_value /= si_conversions["mile"] + elif length_unit == "FEET": + scale_value /= si_conversions["foot"] + elif length_unit == "INCHES": + scale_value /= si_conversions["inch"] + elif length_unit == "THOU": + scale_value /= si_conversions["thou"] + return scale_value diff --git a/src/blenderbim/blenderbim/bim/module/drawing/handler.py b/src/blenderbim/blenderbim/bim/module/drawing/handler.py index ef581c6d78..27ea862264 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/handler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/handler.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import blenderbim.bim.module.drawing.decoration as decoration from bpy.app.handlers import persistent diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py index 6285251c4d..e00ef1a5f6 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import math import mathutils.geometry @@ -244,7 +262,7 @@ def get_active_drawing(scene): if props.active_drawing_index is None or len(props.drawings) == 0: return None, None try: - drawing = props.drawings[props.active_drawing_index] + drawing = props.active_drawing return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera except (KeyError, IndexError): raise RuntimeError("missing drawing collection") diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 6ef13d829b..ee0eec8ac1 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import re import bpy @@ -7,13 +25,18 @@ import bmesh import shutil import subprocess import webbrowser +import multiprocessing +import ifcopenshell +import ifcopenshell.geom import ifcopenshell.util.selector import ifcopenshell.util.representation +import blenderbim.bim.schema import blenderbim.bim.module.drawing.svgwriter as svgwriter import blenderbim.bim.module.drawing.annotation as annotation import blenderbim.bim.module.drawing.sheeter as sheeter import blenderbim.bim.module.drawing.scheduler as scheduler import blenderbim.bim.module.drawing.helper as helper +from blenderbim.bim.module.drawing.prop import RasterStyleProperty from mathutils import Vector, Matrix, Euler, geometry from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.group.data import Data as GroupData @@ -34,8 +57,16 @@ def open_with_user_command(user_command, path): class AddDrawing(bpy.types.Operator): bl_idname = "bim.add_drawing" bl_label = "Add Drawing" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return IfcStore.get_file() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() new = context.scene.DocProperties.drawings.add() new.name = "DRAWING {}".format(len(context.scene.DocProperties.drawings)) @@ -65,34 +96,56 @@ class AddDrawing(bpy.types.Operator): group = self.file.by_id(sorted(GroupData.groups.keys())[-1]) ifcopenshell.api.run("group.edit_group", self.file, **{"group": group, "attributes": {"Name": new.name}}) bpy.ops.bim.assign_group(product=camera.name, group=group.id()) - bpy.ops.bim.add_pset(obj=camera.name, obj_type="Object", pset_name="EPset_Drawing") - pset_id = sorted(PsetData.products[camera.BIMObjectProperties.ifc_definition_id]["psets"])[-1] - bpy.ops.bim.edit_pset( - obj=camera.name, - obj_type="Object", - pset_id=pset_id, - properties=json.dumps({"TargetView": "PLAN_VIEW", "Scale": "1/100"}), + pset = ifcopenshell.api.run( + "pset.add_pset", + self.file, + **{ + "product": self.file.by_id(camera.BIMObjectProperties.ifc_definition_id), + "name": "EPset_Drawing", + }, ) + ifcopenshell.api.run( + "pset.edit_pset", + self.file, + **{ + "pset": pset, + "properties": {"TargetView": "PLAN_VIEW", "Scale": "1/100"}, + "pset_template": blenderbim.bim.schema.ifc.psetqto.get_by_name("EPset_Drawing"), + }, + ) + PsetData.load(IfcStore.get_file(), camera.BIMObjectProperties.ifc_definition_id) return {"FINISHED"} class CreateDrawing(bpy.types.Operator): + """Creates a svg drawing + + Only available if : + - IFC file is created + - Camera is in Orthographic mode + """ + bl_idname = "bim.create_drawing" bl_label = "Create Drawing" + @classmethod + def poll(cls, context): + camera = context.scene.camera + return ( + IfcStore.get_file() + and camera.type == "CAMERA" + and camera.data.type == "ORTHO" + and camera.BIMObjectProperties.ifc_definition_id + ) + def execute(self, context): self.camera = context.scene.camera - if ( - not (self.camera.type == "CAMERA" and self.camera.data.type == "ORTHO") - or not self.camera.BIMObjectProperties.ifc_definition_id - ): - return self.file = IfcStore.get_file() self.time = None start = time.time() self.profile_code("Start drawing generation process") self.props = context.scene.DocProperties - self.drawing_name = IfcStore.get_file().by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name + self.drawing_name = self.file.by_id(self.camera.BIMObjectProperties.ifc_definition_id).Name underlay_svg = self.generate_underlay(context) self.profile_code("Generate underlay") linework_svg = self.generate_linework(context) @@ -101,7 +154,7 @@ class CreateDrawing(bpy.types.Operator): self.profile_code("Generate annotation") svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg) self.profile_code("Combine SVG layers") - open_with_user_command(bpy.context.preferences.addons["blenderbim"].preferences.svg_command, svg_path) + open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, svg_path) print("Total Time: {:.2f}".format(time.time() - start)) return {"FINISHED"} @@ -159,8 +212,8 @@ class CreateDrawing(bpy.types.Operator): if not self.props.has_underlay: return svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-underlay.svg") - bpy.context.scene.render.filepath = svg_path[0:-4] + ".png" - drawing_style = bpy.context.scene.DocProperties.drawing_styles[ + context.scene.render.filepath = svg_path[0:-4] + ".png" + drawing_style = context.scene.DocProperties.drawing_styles[ self.camera.data.BIMCameraProperties.active_drawing_style_index ] @@ -171,7 +224,7 @@ class CreateDrawing(bpy.types.Operator): for obj in self.camera.users_collection[0].objects: previous_visibility[obj.name] = obj.hide_get() obj.hide_set(True) - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: if ( (not obj.data and not obj.instance_collection) or isinstance(obj.data, bpy.types.Camera) @@ -182,14 +235,14 @@ class CreateDrawing(bpy.types.Operator): previous_visibility[obj.name] = obj.hide_get() obj.hide_set(True) - space = self.get_view_3d() + space = self.get_view_3d(context.screen.areas) previous_shading = space.shading.type - previous_format = bpy.context.scene.render.image_settings.file_format + previous_format = context.scene.render.image_settings.file_format space.shading.type = "RENDERED" - bpy.context.scene.render.image_settings.file_format = "PNG" + context.scene.render.image_settings.file_format = "PNG" bpy.ops.render.opengl(write_still=True) space.shading.type = previous_shading - bpy.context.scene.render.image_settings.file_format = previous_format + context.scene.render.image_settings.file_format = previous_format for name, value in previous_visibility.items(): bpy.data.objects[name].hide_set(value) @@ -203,7 +256,7 @@ class CreateDrawing(bpy.types.Operator): svg_writer.human_scale = "NTS" else: svg_writer.human_scale = human_scale - render = bpy.context.scene.render + render = context.scene.render if self.is_landscape(): width = self.camera.data.ortho_scale height = width / render.resolution_x * render.resolution_y @@ -211,13 +264,13 @@ class CreateDrawing(bpy.types.Operator): height = self.camera.data.ortho_scale width = height / render.resolution_y * render.resolution_x svg_writer.output = svg_path - svg_writer.data_dir = bpy.context.scene.BIMProperties.data_dir + svg_writer.data_dir = context.scene.BIMProperties.data_dir svg_writer.vector_style = drawing_style.vector_style svg_writer.camera = self.camera svg_writer.camera_width = width svg_writer.camera_height = height svg_writer.camera_projection = tuple(self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))) - svg_writer.background_image = bpy.context.scene.render.filepath + svg_writer.background_image = context.scene.render.filepath svg_writer.write("underlay") return svg_path @@ -227,6 +280,39 @@ class CreateDrawing(bpy.types.Operator): svg_path = os.path.join(context.scene.BIMProperties.data_dir, "cache", self.drawing_name + "-linework.svg") if os.path.isfile(svg_path) and self.props.should_use_linework_cache: return svg_path + # This is a work in progress. See #1153 and #1564. + # Switch from old to new if you are testing v0.7.0 + self.generate_linework_old(context, svg_path) + # self.generate_linework_new(svg_path) + return svg_path + + def generate_linework_new(self, svg_path): + settings = ifcopenshell.geom.settings( + APPLY_DEFAULT_MATERIALS=True, + DISABLE_TRIANGULATION=True, + INCLUDE_CURVES=True, + EXCLUDE_SOLIDS_AND_SURFACES=False, + ) + buffer = ifcopenshell.geom.serializers.buffer() + serialiser = ifcopenshell.geom.serializers.svg(buffer, settings) + serialiser.setFile(self.file) + serialiser.setElevationRef("DRAWING") + serialiser.setUseNamespace(True) + serialiser.setAlwaysProject(True) + serialiser.setUseHlrPoly(True) + serialiser.setWithoutStoreys(True) + excluded_elements = [] + for ifc_class in ["IfcSpace", "IfcOpeningElement", "IfcDoor", "IfcWindow"]: + excluded_elements += self.file.by_type(ifc_class) + for element in ifcopenshell.geom.iterate( + settings, self.file, multiprocessing.cpu_count(), exclude=excluded_elements + ): + serialiser.write(element) + serialiser.finalize() + with open(svg_path, "w") as svg: + svg.write(buffer.get_value()) + + def generate_linework_old(self, context, svg_path): ifcconvert_path = os.path.join(cwd, "..", "..", "..", "libs", "IfcConvert") subprocess.run( [ @@ -273,12 +359,12 @@ class CreateDrawing(bpy.types.Operator): else: svg_writer.human_scale = human_scale - drawing_style = bpy.context.scene.DocProperties.drawing_styles[ + drawing_style = context.scene.DocProperties.drawing_styles[ camera.data.BIMCameraProperties.active_drawing_style_index ] - render = bpy.context.scene.render - if self.is_landscape(): + render = context.scene.render + if self.is_landscape(render): width = camera.data.ortho_scale height = width / render.resolution_x * render.resolution_y else: @@ -287,7 +373,7 @@ class CreateDrawing(bpy.types.Operator): svg_writer.scale = float(numerator) / float(denominator) svg_writer.output = svg_path - svg_writer.data_dir = bpy.context.scene.BIMProperties.data_dir + svg_writer.data_dir = context.scene.BIMProperties.data_dir svg_writer.vector_style = drawing_style.vector_style svg_writer.camera = camera svg_writer.camera_width = width @@ -332,11 +418,11 @@ class CreateDrawing(bpy.types.Operator): svg_writer.write("annotation") return svg_writer.output - def is_landscape(self): - return bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y + def is_landscape(self, render): + return render.resolution_x > render.resolution_y - def get_view_3d(self): - for area in bpy.context.screen.areas: + def get_view_3d(self, areas): + for area in areas: if area.type != "VIEW_3D": continue for space in area.spaces: @@ -419,29 +505,35 @@ class CreateDrawing(bpy.types.Operator): class AddAnnotation(bpy.types.Operator): bl_idname = "bim.add_annotation" bl_label = "Add Annotation" + bl_options = {"REGISTER", "UNDO"} obj_name: bpy.props.StringProperty() data_type: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.scene.camera + def execute(self, context): - if not bpy.context.scene.camera: - return {"FINISHED"} + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): subcontext = ifcopenshell.util.representation.get_context( IfcStore.get_file(), "Plan", "Annotation", context.scene.camera.data.BIMCameraProperties.target_view ) if not subcontext: return {"FINISHED"} if self.data_type == "text": - if bpy.context.selected_objects: - for selected_object in bpy.context.selected_objects: - obj = annotation.Annotator.add_text(related_element=selected_object) + if context.selected_objects: + for selected_object in context.selected_objects: + obj = annotation.Annotator.add_text(context, related_element=selected_object) else: - obj = annotation.Annotator.add_text() + obj = annotation.Annotator.add_text(context) else: - obj = annotation.Annotator.get_annotation_obj(self.obj_name, self.data_type) + obj = annotation.Annotator.get_annotation_obj(self.obj_name, self.data_type, context) if self.obj_name == "Break": - obj = annotation.Annotator.add_plane_to_annotation(obj) + obj = annotation.Annotator.add_plane_to_annotation(obj, context) else: - obj = annotation.Annotator.add_line_to_annotation(obj) + obj = annotation.Annotator.add_line_to_annotation(obj, context) if not obj.BIMObjectProperties.ifc_definition_id: bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcAnnotation", context_id=subcontext.id()) @@ -449,7 +541,7 @@ class AddAnnotation(bpy.types.Operator): bpy.ops.bim.update_representation(obj=obj.name) bpy.ops.object.select_all(action="DESELECT") - bpy.context.view_layer.objects.active = obj + context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} @@ -458,13 +550,16 @@ class AddAnnotation(bpy.types.Operator): class AddSheet(bpy.types.Operator): bl_idname = "bim.add_sheet" bl_label = "Add Sheet" + bl_options = {"REGISTER", "UNDO"} + # TODO: check undo redo def execute(self, context): - new = bpy.context.scene.DocProperties.sheets.add() - new.name = "{} - SHEET".format(len(bpy.context.scene.DocProperties.sheets)) + scene = context.scene + new = scene.DocProperties.sheets.add() + new.name = "{} - SHEET".format(len(scene.DocProperties.sheets)) sheet_builder = sheeter.SheetBuilder() - sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir - sheet_builder.create(new.name, bpy.context.scene.DocProperties.titleblock) + sheet_builder.data_dir = scene.BIMProperties.data_dir + sheet_builder.create(new.name, scene.DocProperties.titleblock) return {"FINISHED"} @@ -473,12 +568,10 @@ class OpenSheet(bpy.types.Operator): bl_label = "Open Sheet" def execute(self, context): - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties open_with_user_command( - bpy.context.preferences.addons["blenderbim"].preferences.svg_command, - os.path.join( - bpy.context.scene.BIMProperties.data_dir, "sheets", props.sheets[props.active_sheet_index].name + ".svg" - ), + context.preferences.addons["blenderbim"].preferences.svg_command, + os.path.join(context.scene.BIMProperties.data_dir, "sheets", props.active_sheet.name + ".svg"), ) return {"FINISHED"} @@ -486,36 +579,44 @@ class OpenSheet(bpy.types.Operator): class AddDrawingToSheet(bpy.types.Operator): bl_idname = "bim.add_drawing_to_sheet" bl_label = "Add Drawing To Sheet" + bl_options = {"REGISTER", "UNDO"} + # TODO: check undo redo + + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + return props.drawings and props.sheets and context.scene.BIMProperties.data_dir def execute(self, context): - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties sheet_builder = sheeter.SheetBuilder() - sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir - try: - sheet_builder.add_drawing( - props.drawings[props.active_drawing_index].name, props.sheets[props.active_sheet_index].name - ) - except: - self.report({"ERROR"}, "Drawings need to be created before being added to a sheet") + sheet_builder.data_dir = context.scene.BIMProperties.data_dir + sheet_builder.add_drawing(props.active_drawing.name, props.active_sheet.name) return {"FINISHED"} class CreateSheets(bpy.types.Operator): bl_idname = "bim.create_sheets" bl_label = "Create Sheets" + # TODO: check undo redo + + @classmethod + def poll(cls, context): + return context.scene.DocProperties.sheets and context.scene.BIMProperties.data_dir def execute(self, context): - props = bpy.context.scene.DocProperties - name = props.sheets[props.active_sheet_index].name + scene = context.scene + props = scene.DocProperties + name = props.active_sheet.name sheet_builder = sheeter.SheetBuilder() - sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir + sheet_builder.data_dir = scene.BIMProperties.data_dir sheet_builder.build(name) - svg2pdf_command = bpy.context.preferences.addons["blenderbim"].preferences.svg2pdf_command - svg2dxf_command = bpy.context.preferences.addons["blenderbim"].preferences.svg2dxf_command + svg2pdf_command = context.preferences.addons["blenderbim"].preferences.svg2pdf_command + svg2dxf_command = context.preferences.addons["blenderbim"].preferences.svg2dxf_command if svg2pdf_command: - path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", name) + path = os.path.join(scene.BIMProperties.data_dir, "build", name) svg = os.path.join(path, name + ".svg") pdf = os.path.join(path, name + ".pdf") # With great power comes great responsibility. Example: @@ -525,7 +626,7 @@ class CreateSheets(bpy.types.Operator): subprocess.run(command) if svg2dxf_command: - path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", name) + path = os.path.join(scene.BIMProperties.data_dir, "build", name) svg = os.path.join(path, name + ".svg") eps = os.path.join(path, name + ".eps") dxf = os.path.join(path, name + ".dxf") @@ -537,11 +638,11 @@ class CreateSheets(bpy.types.Operator): subprocess.run(command) if svg2pdf_command: - open_with_user_command(bpy.context.preferences.addons["blenderbim"].preferences.pdf_command, pdf) + open_with_user_command(context.preferences.addons["blenderbim"].preferences.pdf_command, pdf) else: open_with_user_command( - bpy.context.preferences.addons["blenderbim"].preferences.svg_command, - os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", name, name + ".svg"), + context.preferences.addons["blenderbim"].preferences.svg_command, + os.path.join(scene.BIMProperties.data_dir, "build", name, name + ".svg"), ) return {"FINISHED"} @@ -553,25 +654,32 @@ class OpenView(bpy.types.Operator): def execute(self, context): open_with_user_command( - bpy.context.preferences.addons["blenderbim"].preferences.svg_command, - os.path.join(bpy.context.scene.BIMProperties.data_dir, "diagrams", self.view + ".svg"), + context.preferences.addons["blenderbim"].preferences.svg_command, + os.path.join(context.scene.BIMProperties.data_dir, "diagrams", self.view + ".svg"), ) return {"FINISHED"} class OpenViewCamera(bpy.types.Operator): + """Select this drawing's camera object and expand its drawing properties""" + bl_idname = "bim.open_view_camera" bl_label = "Open View Camera" + bl_options = {"REGISTER", "UNDO"} view_name: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return context.mode == "OBJECT" + def execute(self, context): - new_drawing_index = bpy.context.scene.DocProperties.drawings.find(self.view_name) - bpy.context.scene.DocProperties.active_drawing_index = new_drawing_index - drawing = bpy.context.scene.DocProperties.drawings[new_drawing_index] - bpy.context.view_layer.objects.active = drawing.camera + doc_props = context.scene.DocProperties + doc_props.active_drawing_index = doc_props.drawings.find(self.view_name) + drawing = doc_props.active_drawing bpy.ops.object.select_all(action="DESELECT") drawing.camera.select_set(True) - for area in bpy.context.screen.areas: + context.view_layer.objects.active = drawing.camera + for area in context.screen.areas: if area.ui_type == "PROPERTIES": for space in area.spaces: space.context = "DATA" @@ -581,25 +689,26 @@ class OpenViewCamera(bpy.types.Operator): class ActivateView(bpy.types.Operator): bl_idname = "bim.activate_view" bl_label = "Activate View" + bl_options = {"REGISTER", "UNDO"} drawing_index: bpy.props.IntProperty() def execute(self, context): - camera = bpy.context.scene.DocProperties.drawings[self.drawing_index].camera + camera = context.scene.DocProperties.drawings[self.drawing_index].camera if not camera: return {"FINISHED"} - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area = next(area for area in context.screen.areas if area.type == "VIEW_3D") is_local_view = area.spaces[0].local_view is not None if is_local_view: bpy.ops.view3d.localview() - bpy.context.scene.camera = camera + context.scene.camera = camera bpy.ops.view3d.localview() else: - bpy.context.scene.camera = camera + context.scene.camera = camera area.spaces[0].region_3d.view_perspective = "CAMERA" views_collection = bpy.data.collections.get("Views") for collection in views_collection.children: # We assume the project collection is at the top level - for project_collection in bpy.context.view_layer.layer_collection.children: + for project_collection in context.view_layer.layer_collection.children: # We assume a convention that the 'Views' collection is directly # in the project collection if ( @@ -608,7 +717,7 @@ class ActivateView(bpy.types.Operator): ): project_collection.children["Views"].children[collection.name].hide_viewport = True bpy.data.collections.get(collection.name).hide_render = True - bpy.context.view_layer.layer_collection.children["Views"].children[ + context.view_layer.layer_collection.children["Views"].children[ camera.users_collection[0].name ].hide_viewport = False bpy.data.collections.get(camera.users_collection[0].name).hide_render = False @@ -619,11 +728,13 @@ class ActivateView(bpy.types.Operator): class SelectDocIfcFile(bpy.types.Operator): bl_idname = "bim.select_doc_ifc_file" bl_label = "Select Documentation IFC File" + bl_options = {"REGISTER", "UNDO"} + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") index: bpy.props.IntProperty() def execute(self, context): - bpy.context.scene.DocProperties.ifc_files[self.index].name = self.filepath + context.scene.DocProperties.ifc_files[self.index].name = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -634,18 +745,19 @@ class SelectDocIfcFile(bpy.types.Operator): class GenerateReferences(bpy.types.Operator): bl_idname = "bim.generate_references" bl_label = "Generate References" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.camera = bpy.context.scene.camera + self.camera = context.scene.camera self.filter_potential_references() if self.camera.data.BIMCameraProperties.target_view == "PLAN_VIEW": self.generate_grids() if self.camera.data.BIMCameraProperties.target_view == "ELEVATION_VIEW": self.generate_grids() - self.generate_levels() + self.generate_levels(context) if self.camera.data.BIMCameraProperties.target_view == "SECTION_VIEW": self.generate_grids() - self.generate_levels() + self.generate_levels(context) return {"FINISHED"} def filter_potential_references(self): @@ -661,7 +773,7 @@ class GenerateReferences(bpy.types.Operator): # TODO pass - def generate_levels(self): + def generate_levels(self, context): if self.camera.data.BIMCameraProperties.raster_x > self.camera.data.BIMCameraProperties.raster_y: width = self.camera.data.ortho_scale height = ( @@ -672,7 +784,7 @@ class GenerateReferences(bpy.types.Operator): width = ( height / self.camera.data.BIMCameraProperties.raster_y * self.camera.data.BIMCameraProperties.raster_x ) - level_obj = annotation.Annotator.get_annotation_obj("Section Level", "curve") + level_obj = annotation.Annotator.get_annotation_obj("Section Level", "curve", context) width_in_mm = width * 1000 if self.camera.data.BIMCameraProperties.diagram_scale == "CUSTOM": @@ -689,7 +801,7 @@ class GenerateReferences(bpy.types.Operator): projection = self.project_point_onto_camera(obj.location) co1 = self.camera.matrix_world @ Vector((width / 2 - (offset_percentage * width), projection[1], -1)) co2 = self.camera.matrix_world @ Vector((-(width / 2), projection[1], -1)) - annotation.Annotator.add_line_to_annotation(level_obj, co1, co2) + annotation.Annotator.add_line_to_annotation(level_obj, context, co1, co2) def project_point_onto_camera(self, point): projection = self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1)) @@ -701,9 +813,11 @@ class GenerateReferences(bpy.types.Operator): class ResizeText(bpy.types.Operator): bl_idname = "bim.resize_text" bl_label = "Resize Text" + bl_options = {"REGISTER", "UNDO"} + # TODO: check undo redo def execute(self, context): - for obj in bpy.context.scene.camera.users_collection[0].objects: + for obj in context.scene.camera.users_collection[0].objects: if isinstance(obj.data, bpy.types.TextCurve): annotation.Annotator.resize_text(obj) return {"FINISHED"} @@ -712,29 +826,32 @@ class ResizeText(bpy.types.Operator): class AddVariable(bpy.types.Operator): bl_idname = "bim.add_variable" bl_label = "Add Variable" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.context.active_object.data.BIMTextProperties.variables.add() + context.active_object.data.BIMTextProperties.variables.add() return {"FINISHED"} class RemoveVariable(bpy.types.Operator): bl_idname = "bim.remove_variable" bl_label = "Remove Variable" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): - bpy.context.active_object.data.BIMTextProperties.variables.remove(self.index) + context.active_object.data.BIMTextProperties.variables.remove(self.index) return {"FINISHED"} class PropagateTextData(bpy.types.Operator): bl_idname = "bim.propagate_text_data" bl_label = "Propagate Text Data" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - source = bpy.context.active_object - for obj in bpy.context.selected_objects: + source = context.active_object + for obj in context.selected_objects: if obj == source: continue obj.data.body = source.data.body @@ -742,8 +859,7 @@ class PropagateTextData(bpy.types.Operator): obj.data.align_y = source.data.align_y obj.data.BIMTextProperties.font_size = source.data.BIMTextProperties.font_size obj.data.BIMTextProperties.symbol = source.data.BIMTextProperties.symbol - while len(obj.data.BIMTextProperties.variables) > 0: - obj.data.BIMTextProperties.variables.remove(0) + obj.data.BIMTextProperties.variables.clear() for variable in source.data.BIMTextProperties.variables: new_variable = obj.data.BIMTextProperties.variables.add() new_variable.name = variable.name @@ -754,10 +870,11 @@ class PropagateTextData(bpy.types.Operator): class RemoveDrawing(bpy.types.Operator): bl_idname = "bim.remove_drawing" bl_label = "Remove Drawing" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties camera = props.drawings[self.index].camera collection = camera.users_collection[0] for obj in collection.objects: @@ -770,9 +887,10 @@ class RemoveDrawing(bpy.types.Operator): class AddDrawingStyle(bpy.types.Operator): bl_idname = "bim.add_drawing_style" bl_label = "Add Drawing Style" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - new = bpy.context.scene.DocProperties.drawing_styles.add() + new = context.scene.DocProperties.drawing_styles.add() new.name = "New Drawing Style" return {"FINISHED"} @@ -780,55 +898,42 @@ class AddDrawingStyle(bpy.types.Operator): class RemoveDrawingStyle(bpy.types.Operator): bl_idname = "bim.remove_drawing_style" bl_label = "Remove Drawing Style" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): - bpy.context.scene.DocProperties.drawing_styles.remove(self.index) + context.scene.DocProperties.drawing_styles.remove(self.index) return {"FINISHED"} class SaveDrawingStyle(bpy.types.Operator): bl_idname = "bim.save_drawing_style" bl_label = "Save Drawing Style" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.StringProperty() + # TODO: check undo redo def execute(self, context): - space = self.get_view_3d() - style = { - "bpy.data.worlds[0].color": tuple(bpy.data.worlds[0].color), - "bpy.context.scene.render.engine": bpy.context.scene.render.engine, - "bpy.context.scene.render.film_transparent": bpy.context.scene.render.film_transparent, - "bpy.context.scene.display.shading.show_object_outline": bpy.context.scene.display.shading.show_object_outline, - "bpy.context.scene.display.shading.show_cavity": bpy.context.scene.display.shading.show_cavity, - "bpy.context.scene.display.shading.cavity_type": bpy.context.scene.display.shading.cavity_type, - "bpy.context.scene.display.shading.curvature_ridge_factor": bpy.context.scene.display.shading.curvature_ridge_factor, - "bpy.context.scene.display.shading.curvature_valley_factor": bpy.context.scene.display.shading.curvature_valley_factor, - "bpy.context.scene.view_settings.view_transform": bpy.context.scene.view_settings.view_transform, - "bpy.context.scene.display.shading.light": bpy.context.scene.display.shading.light, - "bpy.context.scene.display.shading.color_type": bpy.context.scene.display.shading.color_type, - "bpy.context.scene.display.shading.single_color": tuple(bpy.context.scene.display.shading.single_color), - "bpy.context.scene.display.shading.show_shadows": bpy.context.scene.display.shading.show_shadows, - "bpy.context.scene.display.shading.shadow_intensity": bpy.context.scene.display.shading.shadow_intensity, - "bpy.context.scene.display.light_direction": tuple(bpy.context.scene.display.light_direction), - "bpy.context.scene.view_settings.use_curve_mapping": bpy.context.scene.view_settings.use_curve_mapping, - "space.overlay.show_wireframes": space.overlay.show_wireframes, - "space.overlay.wireframe_threshold": space.overlay.wireframe_threshold, - "space.overlay.show_floor": space.overlay.show_floor, - "space.overlay.show_axis_x": space.overlay.show_axis_x, - "space.overlay.show_axis_y": space.overlay.show_axis_y, - "space.overlay.show_axis_z": space.overlay.show_axis_z, - "space.overlay.show_object_origins": space.overlay.show_object_origins, - "space.overlay.show_relationship_lines": space.overlay.show_relationship_lines, - } + space = self.get_view_3d(context) # Do not remove. It is used later in eval + scene = context.scene + style = {} + for prop in RasterStyleProperty: + value = eval(prop.value) + if not isinstance(value, str): + try: + value = tuple(value) + except TypeError: + pass + style[prop.value] = value if self.index: index = int(self.index) else: - index = bpy.context.active_object.data.BIMCameraProperties.active_drawing_style_index - bpy.context.scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style) + index = context.active_object.data.BIMCameraProperties.active_drawing_style_index + scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style) return {"FINISHED"} - def get_view_3d(self): - for area in bpy.context.screen.areas: + def get_view_3d(self, context): + for area in context.screen.areas: if area.type != "VIEW_3D": continue for space in area.spaces: @@ -840,58 +945,33 @@ class SaveDrawingStyle(bpy.types.Operator): class ActivateDrawingStyle(bpy.types.Operator): bl_idname = "bim.activate_drawing_style" bl_label = "Activate Drawing Style" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - if context.scene.camera.data.BIMCameraProperties.active_drawing_style_index < len( - bpy.context.scene.DocProperties.drawing_styles - ): - self.drawing_style = bpy.context.scene.DocProperties.drawing_styles[ - context.scene.camera.data.BIMCameraProperties.active_drawing_style_index + scene = context.scene + if scene.camera.data.BIMCameraProperties.active_drawing_style_index < len(scene.DocProperties.drawing_styles): + self.drawing_style = scene.DocProperties.drawing_styles[ + scene.camera.data.BIMCameraProperties.active_drawing_style_index ] - self.set_raster_style() - self.set_query() + self.set_raster_style(context) + self.set_query(context) return {"FINISHED"} - def set_raster_style(self): - space = self.get_view_3d() + def set_raster_style(self, context): + scene = context.scene # Do not remove. It is used in exec later + space = self.get_view_3d(context) # Do not remove. It is used in exec later style = json.loads(self.drawing_style.raster_style) - bpy.data.worlds[0].color = style["bpy.data.worlds[0].color"] - bpy.context.scene.render.engine = style["bpy.context.scene.render.engine"] - bpy.context.scene.render.film_transparent = style["bpy.context.scene.render.film_transparent"] - bpy.context.scene.display.shading.show_object_outline = style[ - "bpy.context.scene.display.shading.show_object_outline" - ] - bpy.context.scene.display.shading.show_cavity = style["bpy.context.scene.display.shading.show_cavity"] - bpy.context.scene.display.shading.cavity_type = style["bpy.context.scene.display.shading.cavity_type"] - bpy.context.scene.display.shading.curvature_ridge_factor = style[ - "bpy.context.scene.display.shading.curvature_ridge_factor" - ] - bpy.context.scene.display.shading.curvature_valley_factor = style[ - "bpy.context.scene.display.shading.curvature_valley_factor" - ] - bpy.context.scene.view_settings.view_transform = style["bpy.context.scene.view_settings.view_transform"] - bpy.context.scene.display.shading.light = style["bpy.context.scene.display.shading.light"] - bpy.context.scene.display.shading.color_type = style["bpy.context.scene.display.shading.color_type"] - bpy.context.scene.display.shading.single_color = style["bpy.context.scene.display.shading.single_color"] - bpy.context.scene.display.shading.show_shadows = style["bpy.context.scene.display.shading.show_shadows"] - bpy.context.scene.display.shading.shadow_intensity = style["bpy.context.scene.display.shading.shadow_intensity"] - bpy.context.scene.display.light_direction = style["bpy.context.scene.display.light_direction"] + for path, value in style.items(): + if isinstance(value, str): + exec(f"{path} = '{value}'") + else: + exec(f"{path} = {value}") - bpy.context.scene.view_settings.use_curve_mapping = style["bpy.context.scene.view_settings.use_curve_mapping"] - space.overlay.show_wireframes = style["space.overlay.show_wireframes"] - space.overlay.wireframe_threshold = style["space.overlay.wireframe_threshold"] - space.overlay.show_floor = style["space.overlay.show_floor"] - space.overlay.show_axis_x = style["space.overlay.show_axis_x"] - space.overlay.show_axis_y = style["space.overlay.show_axis_y"] - space.overlay.show_axis_z = style["space.overlay.show_axis_z"] - space.overlay.show_object_origins = style["space.overlay.show_object_origins"] - space.overlay.show_relationship_lines = style["space.overlay.show_relationship_lines"] - - def set_query(self): + def set_query(self, context): self.selector = ifcopenshell.util.selector.Selector() self.include_global_ids = [] self.exclude_global_ids = [] - for ifc_file in bpy.context.scene.DocProperties.ifc_files: + for ifc_file in context.scene.DocProperties.ifc_files: try: ifc = ifcopenshell.open(ifc_file.name) except: @@ -903,15 +983,15 @@ class ActivateDrawingStyle(bpy.types.Operator): results = self.selector.parse(ifc, self.drawing_style.exclude_query) self.exclude_global_ids.extend([e.GlobalId for e in results]) if self.drawing_style.include_query: - self.parse_filter_query("INCLUDE") + self.parse_filter_query("INCLUDE", context) if self.drawing_style.exclude_query: - self.parse_filter_query("EXCLUDE") + self.parse_filter_query("EXCLUDE", context) - def parse_filter_query(self, mode): + def parse_filter_query(self, mode, context): if mode == "INCLUDE": - objects = bpy.context.scene.objects + objects = context.scene.objects elif mode == "EXCLUDE": - objects = bpy.context.visible_objects + objects = context.visible_objects for obj in objects: if mode == "INCLUDE": obj.hide_viewport = False # Note: this breaks alt-H @@ -926,8 +1006,8 @@ class ActivateDrawingStyle(bpy.types.Operator): if global_id in self.exclude_global_ids: obj.hide_viewport = True # Note: this breaks alt-H - def get_view_3d(self): - for area in bpy.context.screen.areas: + def get_view_3d(self, context): + for area in context.screen.areas: if area.type != "VIEW_3D": continue for space in area.spaces: @@ -939,6 +1019,7 @@ class ActivateDrawingStyle(bpy.types.Operator): class EditVectorStyle(bpy.types.Operator): bl_idname = "bim.edit_vector_style" bl_label = "Edit Vector Style" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): camera = context.scene.camera @@ -952,10 +1033,11 @@ class EditVectorStyle(bpy.types.Operator): class RemoveSheet(bpy.types.Operator): bl_idname = "bim.remove_sheet" bl_label = "Remove Sheet" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties props.sheets.remove(self.index) return {"FINISHED"} @@ -963,20 +1045,22 @@ class RemoveSheet(bpy.types.Operator): class AddSchedule(bpy.types.Operator): bl_idname = "bim.add_schedule" bl_label = "Add Schedule" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - new = bpy.context.scene.DocProperties.schedules.add() - new.name = "SCHEDULE {}".format(len(bpy.context.scene.DocProperties.schedules)) + new = context.scene.DocProperties.schedules.add() + new.name = "SCHEDULE {}".format(len(context.scene.DocProperties.schedules)) return {"FINISHED"} class RemoveSchedule(bpy.types.Operator): bl_idname = "bim.remove_schedule" bl_label = "Remove Schedule" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties props.schedules.remove(self.index) return {"FINISHED"} @@ -984,13 +1068,14 @@ class RemoveSchedule(bpy.types.Operator): class SelectScheduleFile(bpy.types.Operator): bl_idname = "bim.select_schedule_file" bl_label = "Select Documentation IFC File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ods", options={"HIDDEN"}) index: bpy.props.IntProperty() def execute(self, context): - props = bpy.context.scene.DocProperties - props.schedules[props.active_schedule_index].file = self.filepath + props = context.scene.DocProperties + props.active_schedule.file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -1002,36 +1087,46 @@ class BuildSchedule(bpy.types.Operator): bl_idname = "bim.build_schedule" bl_label = "Build Schedule" + @classmethod + def poll(cls, context): + return context.scene.DocProperties.active_schedule.file + def execute(self, context): - props = bpy.context.scene.DocProperties - schedule = props.schedules[props.active_schedule_index] + props = context.scene.DocProperties + schedule = props.active_schedule schedule_creator = scheduler.Scheduler() - outfile = os.path.join(bpy.context.scene.BIMProperties.data_dir, "schedules", schedule.name + ".svg") + outfile = os.path.join(context.scene.BIMProperties.data_dir, "schedules", schedule.name + ".svg") schedule_creator.schedule(schedule.file, outfile) - open_with_user_command(bpy.context.preferences.addons["blenderbim"].preferences.svg_command, outfile) + open_with_user_command(context.preferences.addons["blenderbim"].preferences.svg_command, outfile) return {"FINISHED"} class AddScheduleToSheet(bpy.types.Operator): bl_idname = "bim.add_schedule_to_sheet" bl_label = "Add Schedule To Sheet" + bl_options = {"REGISTER", "UNDO"} + # TODO: check undo redo + + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + return props.schedules and props.sheets and context.scene.BIMProperties.data_dir def execute(self, context): - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties sheet_builder = sheeter.SheetBuilder() - sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir - sheet_builder.add_schedule( - props.schedules[props.active_schedule_index].name, props.sheets[props.active_sheet_index].name - ) + sheet_builder.data_dir = context.scene.BIMProperties.data_dir + sheet_builder.add_schedule(props.active_schedule.name, props.active_sheet.name) return {"FINISHED"} class AddDrawingStyleAttribute(bpy.types.Operator): bl_idname = "bim.add_drawing_style_attribute" bl_label = "Add Drawing Style Attribute" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = bpy.context.scene.camera.data.BIMCameraProperties + props = context.scene.camera.data.BIMCameraProperties context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.add() return {"FINISHED"} @@ -1039,10 +1134,11 @@ class AddDrawingStyleAttribute(bpy.types.Operator): class RemoveDrawingStyleAttribute(bpy.types.Operator): bl_idname = "bim.remove_drawing_style_attribute" bl_label = "Remove Drawing Style Attribute" + bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() def execute(self, context): - props = bpy.context.scene.camera.data.BIMCameraProperties + props = context.scene.camera.data.BIMCameraProperties context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index) return {"FINISHED"} @@ -1052,13 +1148,13 @@ class RefreshDrawingList(bpy.types.Operator): bl_label = "Refresh Drawing List" def execute(self, context): - while len(bpy.context.scene.DocProperties.drawings) > 0: - bpy.context.scene.DocProperties.drawings.remove(0) - for obj in bpy.context.scene.objects: + doc_props = context.scene.DocProperties + doc_props.drawings.clear() + for obj in context.scene.objects: if not isinstance(obj.data, bpy.types.Camera): continue if "IfcAnnotation/" in obj.name: - new = bpy.context.scene.DocProperties.drawings.add() + new = doc_props.drawings.add() new.name = "/".join(obj.name.split("/")[1:]) new.camera = obj return {"FINISHED"} @@ -1067,16 +1163,15 @@ class RefreshDrawingList(bpy.types.Operator): class CleanWireframes(bpy.types.Operator): bl_idname = "bim.clean_wireframes" bl_label = "Clean Wireframes" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - objects = bpy.data.objects - if bpy.context.selected_objects: - objects = bpy.context.selected_objects - for obj in objects: - if not isinstance(obj.data, bpy.types.Mesh): - continue - # TODO: probably breaks i18n - if not obj.modifiers.get("EdgeSplit"): + if context.selected_objects: + objects = context.selected_objects + else: + objects = context.scene.objects + for obj in (o for o in objects if o.type == "MESH"): + if "EDGE_SPLIT" not in (m.type for m in obj.modifiers): obj.modifiers.new("EdgeSplit", "EDGE_SPLIT") return {"FINISHED"} @@ -1084,13 +1179,15 @@ class CleanWireframes(bpy.types.Operator): class CopyGrid(bpy.types.Operator): bl_idname = "bim.add_grid" bl_label = "Add Grid" - bl_options = {"UNDO"} + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return helper.get_active_drawing(context.scene)[0] is not None def execute(self, context): proj_coll = helper.get_project_collection(context.scene) view_coll, camera = helper.get_active_drawing(context.scene) - if view_coll is None: - return {"CANCELLED"} is_ortho = camera.data.type == "ORTHO" bounds = helper.ortho_view_frame(camera.data) if is_ortho else None clipping = is_ortho and camera.data.BIMCameraProperties.target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") @@ -1165,15 +1262,17 @@ class CopyGrid(bpy.types.Operator): class AddSectionsAnnotations(bpy.types.Operator): bl_idname = "bim.add_sections_annotations" bl_label = "Add Sections" - bl_options = {"UNDO"} + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + camera = helper.get_active_drawing(context.scene)[1] + return camera and camera.data.type == "ORTHO" def execute(self, context): scene = context.scene view_coll, camera = helper.get_active_drawing(scene) - is_ortho = camera.data.type == "ORTHO" - if not is_ortho: - return {"CANCELLED"} - bounds = helper.ortho_view_frame(camera.data) if is_ortho else None + bounds = helper.ortho_view_frame(camera.data) drawings = [ d diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index b64a1dcba6..f87e4e6ee1 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -1,7 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import blenderbim.bim.module.drawing.annotation as annotation import blenderbim.bim.module.drawing.decoration as decoration +import enum from pathlib import Path from blenderbim.bim.prop import Attribute, StrProperty from bpy.types import PropertyGroup @@ -34,14 +53,14 @@ def purge(): vector_styles_enum = [] -def getDiagramScales(self, context): +def get_diagram_scales(self, context): global diagram_scales_enum if ( len(diagram_scales_enum) < 1 - or (bpy.context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13) - or (bpy.context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31) + or (context.scene.unit_settings.system == "IMPERIAL" and len(diagram_scales_enum) == 13) + or (context.scene.unit_settings.system == "METRIC" and len(diagram_scales_enum) == 31) ): - if bpy.context.scene.unit_settings.system == "IMPERIAL": + if context.scene.unit_settings.system == "IMPERIAL": diagram_scales_enum = [ ("CUSTOM", "Custom", ""), ("1'=1'-0\"|1/1", "1'=1'-0\"", ""), @@ -199,6 +218,33 @@ class DrawingStyle(PropertyGroup): attributes: CollectionProperty(name="Attributes", type=StrProperty) +class RasterStyleProperty(enum.Enum): + WORLD_COLOR = "bpy.data.worlds[0].color" + RENDER_ENGINE = "scene.render.engine" + RENDER_TRANSPARENT = "scene.render.film_transparent" + VIEW_TRANSFORM = "scene.view_settings.view_transform" + SHADING_SHOW_OBJECT_OUTLINE = "scene.display.shading.show_object_outline" + SHADING_SHOW_CAVITY = "scene.display.shading.show_cavity" + SHADING_CAVITY_TYPE = "scene.display.shading.cavity_type" + SHADING_CURVATURE_RIDGE_FACTOR = "scene.display.shading.curvature_ridge_factor" + SHADING_CURVATURE_VALLEY_FACTOR = "scene.display.shading.curvature_valley_factor" + SHADING_LIGHT = "scene.display.shading.light" + SHADING_COLOR_TYPE = "scene.display.shading.color_type" + SHADING_SINGLE_COLOR = "scene.display.shading.single_color" + SHADING_SHOW_SHADOWS = "scene.display.shading.show_shadows" + SHADING_SHADOW_INTENSITY = "scene.display.shading.shadow_intensity" + DISPLAY_LIGHT_DIRECTION = "scene.display.light_direction" + VIEW_USE_CURVE_MAPPING = "scene.view_settings.use_curve_mapping" + OVERLAY_SHOW_WIREFRAMES = "space.overlay.show_wireframes" + OVERLAY_WIREFRAME_THRESHOLD = "space.overlay.wireframe_threshold" + OVERLAY_SHOW_FLOOR = "space.overlay.show_floor" + OVERLAY_SHOW_AXIS_X = "space.overlay.show_axis_x" + OVERLAY_SHOW_AXIS_Y = "space.overlay.show_axis_y" + OVERLAY_SHOW_AXIS_Z = "space.overlay.show_axis_z" + OVERLAY_SHOW_OBJECT_ORIGINS = "space.overlay.show_object_origins" + OVERLAY_SHOW_RELATIONSHIP_LINES = "space.overlay.show_relationship_lines" + + class DocProperties(PropertyGroup): has_underlay: BoolProperty(name="Underlay", default=False) has_linework: BoolProperty(name="Linework", default=True) @@ -222,6 +268,18 @@ class DocProperties(PropertyGroup): name="Decorations Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4 ) + @property + def active_schedule(self): + return self.schedules[self.active_schedule_index] + + @property + def active_drawing(self): + return self.drawings[self.active_drawing_index] + + @property + def active_sheet(self): + return self.sheets[self.active_sheet_index] + class BIMCameraProperties(PropertyGroup): view_name: StringProperty(name="View Name") @@ -236,7 +294,7 @@ class BIMCameraProperties(PropertyGroup): name="Target View", default="PLAN_VIEW", ) - diagram_scale: EnumProperty(items=getDiagramScales, name="Drawing Scale") + diagram_scale: EnumProperty(items=get_diagram_scales, name="Drawing Scale") custom_diagram_scale: StringProperty(name="Custom Scale") raster_x: IntProperty(name="Raster X", default=1000) raster_y: IntProperty(name="Raster Y", default=1000) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py index 81555cb55c..e25e49a835 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/scheduler.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import svgwrite from odf.opendocument import load @@ -8,7 +26,11 @@ from odf.style import Style class Scheduler: def schedule(self, infile, outfile): - self.svg = svgwrite.Drawing(outfile, debug=False, id="root",) + self.svg = svgwrite.Drawing( + outfile, + debug=False, + id="root", + ) self.padding = 1 self.margin = 1 doc = load(infile) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/shaders.py b/src/blenderbim/blenderbim/bim/module/drawing/shaders.py index 8b9c79efc8..384b8a0917 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/shaders.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/shaders.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Maxim Vasilyev +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bgl from mathutils import Matrix from gpu.types import GPUShader @@ -5,7 +23,7 @@ from gpu_extras.batch import batch_for_shader class BaseShader: - """Wrapepr for GPUShader + """Wrapper for GPUShader To use for viewport decorations with geometry generated on GPU side. The Geometry shader works in clipping coords (aftre projecting before division and window scaling). diff --git a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py index 5290337ab7..9d705c195b 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/sheeter.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import xml.etree.ElementTree as ET import urllib.parse import pystache diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index 45fb2c5fe2..c048c40800 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import re import bpy @@ -26,9 +44,9 @@ class External(svgwrite.container.Group): self.xml = xml # Remove namespace - ns = u"{http://www.w3.org/2000/svg}" + ns = "{http://www.w3.org/2000/svg}" nsl = len(ns) - for elem in self.xml.getiterator(): + for elem in self.xml.iter(): if elem.tag.startswith(ns): elem.tag = elem.tag[nsl:] @@ -85,31 +103,31 @@ class SvgWriter: def add_markers(self): tree = ET.parse(os.path.join(self.data_dir, "templates", "markers.svg")) root = tree.getroot() - for child in root.getchildren(): + for child in root: self.svg.defs.add(External(child)) def add_symbols(self): tree = ET.parse(os.path.join(self.data_dir, "templates", "symbols.svg")) root = tree.getroot() - for child in root.getchildren(): + for child in root: self.svg.defs.add(External(child)) def add_patterns(self): tree = ET.parse(os.path.join(self.data_dir, "templates", "patterns.svg")) root = tree.getroot() - for child in root.getchildren(): + for child in root: self.svg.defs.add(External(child)) def draw_background_image(self): self.svg.add( self.svg.image( os.path.join("..", "diagrams", os.path.basename(self.background_image)), - **{"width": self.width, "height": self.height} + **{"width": self.width, "height": self.height}, ) ) def draw_background_elements(self): - return # TODO purge? + return # TODO purge? for element in self.ifc_cutter.background_elements: if element["type"] == "polygon": self.draw_polygon(element, "background") @@ -161,7 +179,7 @@ class SvgWriter: "text-anchor": "middle", "alignment-baseline": "middle", "dominant-baseline": "middle", - } + }, ) ) self.svg.add( @@ -174,7 +192,7 @@ class SvgWriter: "text-anchor": "middle", "alignment-baseline": "middle", "dominant-baseline": "middle", - } + }, ) ) @@ -233,7 +251,7 @@ class SvgWriter: "text-anchor": text_anchor, "alignment-baseline": "baseline", "dominant-baseline": "baseline", - } + }, ) ) @@ -275,7 +293,7 @@ class SvgWriter: "text-anchor": "start", "alignment-baseline": "baseline", "dominant-baseline": "baseline", - } + }, ) ) @@ -306,7 +324,7 @@ class SvgWriter: "text-anchor": "middle", "alignment-baseline": "middle", "dominant-baseline": "middle", - } + }, ) ) self.draw_text_annotations() @@ -454,7 +472,9 @@ class SvgWriter: ) transform = "rotate({}, {}, {})".format( - angle, (text_position * self.scale)[0], (text_position * self.scale)[1], + angle, + (text_position * self.scale)[0], + (text_position * self.scale)[1], ) if text_obj.data.BIMTextProperties.symbol != "None": @@ -497,7 +517,7 @@ class SvgWriter: "alignment-baseline": alignment_baseline, "dominant-baseline": alignment_baseline, "transform": transform, - } + }, ) ) @@ -588,7 +608,7 @@ class SvgWriter: "font-size": annotation.Annotator.get_svg_text_size(2.5), "font-family": "OpenGost Type B TT", "text-anchor": "middle", - } + }, ) ) @@ -604,7 +624,7 @@ class SvgWriter: return spline.bezier_points if spline.bezier_points else spline.points def draw_cut_polygons(self): - return # deprecate? + return # deprecate? for polygon in self.ifc_cutter.cut_polygons: self.draw_polygon(polygon, "cut") diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 81c04ed4cc..f45a1b3775 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -1,7 +1,23 @@ -import os +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Panel -from bpy.props import StringProperty, BoolProperty class BIM_PT_camera(Panel): @@ -13,7 +29,6 @@ class BIM_PT_camera(Panel): @classmethod def poll(cls, context): - engine = context.engine return context.camera and hasattr(context.active_object.data, "BIMCameraProperties") def draw(self, context): @@ -24,7 +39,7 @@ class BIM_PT_camera(Panel): return layout.use_property_split = True - dprops = bpy.context.scene.DocProperties + dprops = context.scene.DocProperties props = context.active_object.data.BIMCameraProperties col = layout.column(align=True) @@ -86,13 +101,12 @@ class BIM_PT_drawing_underlay(Panel): @classmethod def poll(cls, context): - engine = context.engine return context.camera and hasattr(context.active_object.data, "BIMCameraProperties") def draw(self, context): layout = self.layout layout.use_property_split = True - dprops = bpy.context.scene.DocProperties + dprops = context.scene.DocProperties props = context.active_object.data.BIMCameraProperties row = layout.row(align=True) @@ -131,7 +145,6 @@ class BIM_PT_drawing_underlay(Panel): row.operator("bim.activate_drawing_style") - class BIM_PT_drawings(Panel): bl_label = "SVG Drawings" bl_idname = "BIM_PT_drawings" @@ -142,7 +155,7 @@ class BIM_PT_drawings(Panel): def draw(self, context): layout = self.layout layout.use_property_split = True - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties row = layout.row(align=True) row.operator("bim.add_drawing") @@ -151,9 +164,9 @@ class BIM_PT_drawings(Panel): if props.drawings: if props.active_drawing_index < len(props.drawings): op = row.operator("bim.open_view", icon="URL", text="") - op.view = props.drawings[props.active_drawing_index].name + op.view = props.active_drawing.name row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index - layout.template_list("BIM_UL_generic", "", props, "drawings", props, "active_drawing_index") + layout.template_list("BIM_UL_drawinglist", "", props, "drawings", props, "active_drawing_index") row = layout.row() row.operator("bim.add_ifc_file") @@ -175,7 +188,7 @@ class BIM_PT_schedules(Panel): def draw(self, context): layout = self.layout layout.use_property_split = True - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties row = layout.row(align=True) row.operator("bim.add_schedule") @@ -187,7 +200,7 @@ class BIM_PT_schedules(Panel): layout.template_list("BIM_UL_generic", "", props, "schedules", props, "active_schedule_index") row = layout.row() - row.prop(props.schedules[props.active_schedule_index], "file") + row.prop(props.active_schedule, "file") row.operator("bim.select_schedule_file", icon="FILE_FOLDER", text="") @@ -200,7 +213,7 @@ class BIM_PT_sheets(Panel): def draw(self, context): layout = self.layout - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties row = layout.row(align=True) row.prop(props, "titleblock", text="") @@ -270,8 +283,6 @@ class BIM_PT_annotation_utilities(Panel): row = layout.row(align=True) row.operator("bim.clean_wireframes") row = layout.row(align=True) - row.operator("bim.link_ifc") - row = layout.row(align=True) row.operator("bim.add_grid") row = layout.row(align=True) row.operator("bim.add_sections_annotations") @@ -315,7 +326,7 @@ class BIM_PT_annotation_utilities(Panel): op.obj_name = "Misc" op.data_type = "mesh" - props = bpy.context.scene.DocProperties + props = context.scene.DocProperties row = layout.row(align=True) row.operator("bim.add_drawing") @@ -324,7 +335,7 @@ class BIM_PT_annotation_utilities(Panel): if props.drawings: if props.active_drawing_index < len(props.drawings): op = row.operator("bim.open_view", icon="URL", text="") - op.view = props.drawings[props.active_drawing_index].name + op.view = props.active_drawing.name row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index layout.template_list("BIM_UL_drawinglist", "", props, "drawings", props, "active_drawing_index") diff --git a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py index 433e308052..70a06345b6 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/geometry/helper.py b/src/blenderbim/blenderbim/bim/module/geometry/helper.py index 1733ea1d9c..1fd3ab338c 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/helper.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/helper.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import bmesh import mathutils diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index d533adb430..565dce4095 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import numpy as np import ifcopenshell @@ -24,15 +42,15 @@ class EditObjectPlacement(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - objs = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects + objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects self.file = IfcStore.get_file() # TODO: determine how to deal with this module dependency - props = bpy.context.scene.BIMGeoreferenceProperties + props = context.scene.BIMGeoreferenceProperties for obj in objs: if not obj.BIMObjectProperties.ifc_definition_id: continue matrix = np.array(obj.matrix_world) - if props.has_blender_offset and props.blender_offset_type == "OBJECT_PLACEMENT": + if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "OBJECT_PLACEMENT": unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) # TODO: np.array? Why not matrix? matrix = np.array( @@ -69,7 +87,7 @@ class AddRepresentation(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() bpy.ops.bim.edit_object_placement(obj=obj.name) @@ -79,12 +97,12 @@ class AddRepresentation(bpy.types.Operator): product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - context_id = self.context_id or int(bpy.context.scene.BIMProperties.contexts) + context_id = self.context_id or int(context.scene.BIMProperties.contexts) context_of_items = self.file.by_id(context_id) gprop = context.scene.BIMGeoreferenceProperties coordinate_offset = None - if gprop.has_blender_offset and gprop.blender_offset_type == "CARTESIAN_POINT": + if gprop.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT": coordinate_offset = Vector( ( float(gprop.blender_eastings), @@ -102,7 +120,7 @@ class AddRepresentation(bpy.types.Operator): "should_force_faceted_brep": context.scene.BIMGeometryProperties.should_force_faceted_brep, "should_force_triangulation": context.scene.BIMGeometryProperties.should_force_triangulation, "ifc_representation_class": self.ifc_representation_class, - "profile_set_usage": self.file.by_id(self.profile_set_usage) if self.profile_set_usage else None + "profile_set_usage": self.file.by_id(self.profile_set_usage) if self.profile_set_usage else None, } result = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data) @@ -117,29 +135,29 @@ class AddRepresentation(bpy.types.Operator): if s.material and not s.material.BIMMaterialProperties.ifc_style_id ] - ifcopenshell.api.run( - "geometry.assign_styles", - self.file, - **{ - "shape_representation": result, - "styles": [ - self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id) - for s in obj.material_slots - if s.material - ], - "should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, - }, - ) + if isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons): + ifcopenshell.api.run( + "style.assign_representation_styles", + self.file, + **{ + "shape_representation": result, + "styles": [ + self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id) + for s in obj.material_slots + if s.material + ], + "should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, + }, + ) ifcopenshell.api.run( "geometry.assign_representation", self.file, **{"product": product, "representation": result} ) - existing_mesh = obj.data mesh = obj.data.copy() mesh.name = "{}/{}".format(context_id, result.id()) mesh.BIMMeshProperties.ifc_definition_id = int(result.id()) obj.data = mesh - Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + Data.load(self.file, obj.BIMObjectProperties.ifc_definition_id) if product.is_a("IfcTypeProduct"): if self.file.schema == "IFC2X3": @@ -148,7 +166,7 @@ class AddRepresentation(bpy.types.Operator): types = product.Types if types: for element in types[0].RelatedObjects: - Data.load(IfcStore.get_file(), element.id()) + Data.load(self.file, element.id()) return {"FINISHED"} @@ -160,9 +178,10 @@ class SwitchRepresentation(bpy.types.Operator): ifc_definition_id: bpy.props.IntProperty() should_reload: bpy.props.BoolProperty() disable_opening_subtractions: bpy.props.BoolProperty() + should_switch_all_meshes: bpy.props.BoolProperty() def execute(self, context): - self.element_obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + self.element_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.oprops = self.element_obj.BIMObjectProperties self.file = IfcStore.get_file() @@ -171,11 +190,17 @@ class SwitchRepresentation(bpy.types.Operator): mesh = bpy.data.meshes.get(self.mesh_name) if mesh: - self.element_obj.data.user_remap(mesh) + self.switch_mesh(mesh) if not mesh or self.should_reload: - self.pull_mesh_from_ifc() + self.pull_mesh_from_ifc(context) return {"FINISHED"} + def switch_mesh(self, mesh): + if self.should_switch_all_meshes or self.file.by_id(self.oprops.ifc_definition_id).is_a("IfcTypeProduct"): + self.element_obj.data.user_remap(mesh) + else: + self.element_obj.data = mesh + def get_mesh_name(self): representation = self.resolve_mapped_representation(self.file.by_id(self.ifc_definition_id)) return "{}/{}".format(self.context_of_items.id(), representation.id()) @@ -185,9 +210,9 @@ class SwitchRepresentation(bpy.types.Operator): return self.resolve_mapped_representation(representation.Items[0].MappingSource.MappedRepresentation) return representation - def pull_mesh_from_ifc(self): + def pull_mesh_from_ifc(self, context): logger = logging.getLogger("ImportIFC") - ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, logger) + ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) element = self.file.by_id(self.oprops.ifc_definition_id) settings = ifcopenshell.geom.settings() @@ -205,13 +230,14 @@ class SwitchRepresentation(bpy.types.Operator): mesh = ifc_importer.create_mesh(element, shape) mesh.name = self.mesh_name mesh.BIMMeshProperties.ifc_definition_id = self.ifc_definition_id - self.element_obj.data.user_remap(mesh) + self.switch_mesh(mesh) material_creator = import_ifc.MaterialCreator(ifc_import_settings, ifc_importer) + material_creator.load_existing_materials() material_creator.create(element, self.element_obj, mesh) if self.disable_opening_subtractions and self.context_of_items.ContextIdentifier == "Body": if self.oprops.ifc_definition_id not in VoidData.products: - VoidData.load(IfcStore.get_file(), self.oprops.ifc_definition_id) + VoidData.load(self.file, self.oprops.ifc_definition_id) for opening_id in VoidData.products[self.oprops.ifc_definition_id]: opening = IfcStore.get_element(opening_id) if not opening: @@ -219,6 +245,8 @@ class SwitchRepresentation(bpy.types.Operator): modifier = self.element_obj.modifiers.new("IfcOpeningElement", "BOOLEAN") modifier.operation = "DIFFERENCE" modifier.object = opening + modifier.solver = "EXACT" + modifier.use_self = True else: for modifier in self.element_obj.modifiers: if modifier.type == "BOOLEAN" and "IfcOpeningElement" in modifier.name: @@ -238,7 +266,7 @@ class RemoveRepresentation(bpy.types.Operator): def _execute(self, context): self.file = IfcStore.get_file() representation = self.file.by_id(self.representation_id) - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object is_mapped_representation = representation.RepresentationType == "MappedRepresentation" if is_mapped_representation: mesh_name = "{}/{}".format( @@ -261,7 +289,7 @@ class RemoveRepresentation(bpy.types.Operator): "geometry.unassign_representation", self.file, **{"product": product, "representation": representation} ) ifcopenshell.api.run("geometry.remove_representation", self.file, **{"representation": representation}) - Data.load(IfcStore.get_file(), product.id()) + Data.load(self.file, product.id()) return {"FINISHED"} @@ -272,6 +300,10 @@ class UpdateRepresentation(bpy.types.Operator): obj: bpy.props.StringProperty() ifc_representation_class: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return context.active_object.mode == "OBJECT" + def execute(self, context): return IfcStore.execute_ifc_operator(self, context) @@ -279,7 +311,7 @@ class UpdateRepresentation(bpy.types.Operator): if not ContextData.is_loaded: ContextData.load(IfcStore.get_file()) - objs = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects + objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects self.file = IfcStore.get_file() for obj in objs: @@ -301,7 +333,7 @@ class UpdateRepresentation(bpy.types.Operator): gprop = context.scene.BIMGeoreferenceProperties coordinate_offset = None - if gprop.has_blender_offset and gprop.blender_offset_type == "CARTESIAN_POINT": + if gprop.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT": coordinate_offset = Vector( ( float(gprop.blender_eastings), @@ -323,19 +355,26 @@ class UpdateRepresentation(bpy.types.Operator): new_representation = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data) - ifcopenshell.api.run( - "geometry.assign_styles", - self.file, - **{ - "shape_representation": new_representation, - "styles": [ - self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id) - for s in obj.material_slots - if s.material - ], - "should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, - }, - ) + [ + bpy.ops.bim.add_style(material=s.material.name) + for s in obj.material_slots + if s.material and not s.material.BIMMaterialProperties.ifc_style_id + ] + + if isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons): + ifcopenshell.api.run( + "style.assign_representation_styles", + self.file, + **{ + "shape_representation": new_representation, + "styles": [ + self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id) + for s in obj.material_slots + if s.material + ], + "should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, + }, + ) # TODO: move this into a replace_representation usecase or something for inverse in self.file.get_inverse(old_representation): @@ -344,7 +383,9 @@ class UpdateRepresentation(bpy.types.Operator): obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id()) obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}" bpy.ops.bim.remove_representation(representation_id=old_representation.id(), obj=obj.name) - Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + Data.load(self.file, obj.BIMObjectProperties.ifc_definition_id) + if obj.data.BIMMeshProperties.ifc_parameters: + bpy.ops.bim.get_representation_ifc_parameters() class UpdateParametricRepresentation(bpy.types.Operator): @@ -353,13 +394,22 @@ class UpdateParametricRepresentation(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} index: bpy.props.IntProperty() + @classmethod + def poll(cls, context): + return context.active_object.mode == "OBJECT" + def execute(self, context): self.file = IfcStore.get_file() - obj = bpy.context.active_object + obj = context.active_object props = obj.data.BIMMeshProperties parameter = props.ifc_parameters[self.index] - element = IfcStore.get_file().by_id(parameter.step_id)[parameter.index] = parameter.value - bpy.ops.bim.switch_representation(ifc_definition_id=props.ifc_definition_id, should_reload=True) + self.file.by_id(parameter.step_id)[parameter.index] = parameter.value + show_representation_parameters = bool(props.ifc_parameters) + bpy.ops.bim.switch_representation( + ifc_definition_id=props.ifc_definition_id, should_reload=True, should_switch_all_meshes=True + ) + if show_representation_parameters: + bpy.ops.bim.get_representation_ifc_parameters() return {"FINISHED"} @@ -370,9 +420,10 @@ class GetRepresentationIfcParameters(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - obj = bpy.context.active_object + obj = context.active_object props = obj.data.BIMMeshProperties - elements = IfcStore.get_file().traverse(IfcStore.get_file().by_id(props.ifc_definition_id)) + elements = self.file.traverse(self.file.by_id(props.ifc_definition_id)) + props.ifc_parameters.clear() for element in elements: if element.is_a("IfcRepresentationItem") or element.is_a("IfcParameterizedProfileDef"): for i in range(0, len(element)): diff --git a/src/blenderbim/blenderbim/bim/module/geometry/prop.py b/src/blenderbim/blenderbim/bim/module/geometry/prop.py index 983754d987..289a81a6ea 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/prop.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py index 784e2ff739..0888ef27a5 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Panel from ifcopenshell.api.geometry.data import Data @@ -30,7 +48,7 @@ class BIM_PT_representations(Panel): layout.label(text="No representations found") row = layout.row(align=True) - row.prop(bpy.context.scene.BIMProperties, "contexts", text="") + row.prop(context.scene.BIMProperties, "contexts", text="") row.operator("bim.add_representation", icon="ADD", text="") for ifc_definition_id in representations: @@ -41,6 +59,7 @@ class BIM_PT_representations(Panel): row.label(text=representation["ContextOfItems"]["TargetView"]) row.label(text=representation["RepresentationType"]) op = row.operator("bim.switch_representation", icon="OUTLINER_DATA_MESH", text="") + op.should_switch_all_meshes = True op.should_reload = True op.ifc_definition_id = ifc_definition_id op.disable_opening_subtractions = False @@ -71,10 +90,12 @@ class BIM_PT_mesh(Panel): row = layout.row(align=True) op = row.operator("bim.switch_representation", text="Bake Voids", icon="SELECT_SUBTRACT") + op.should_switch_all_meshes = True op.should_reload = True op.ifc_definition_id = props.ifc_definition_id op.disable_opening_subtractions = False op = row.operator("bim.switch_representation", text="Dynamic Voids", icon="SELECT_INTERSECT") + op.should_switch_all_meshes = True op.should_reload = True op.ifc_definition_id = props.ifc_definition_id op.disable_opening_subtractions = True @@ -109,6 +130,10 @@ class BIM_PT_mesh(Panel): def BIM_PT_transform(self, context): if context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id: + row = self.layout.row(align=True) + row.label(text="Blender Offset") + row.label(text=context.active_object.BIMObjectProperties.blender_offset_type) + row = self.layout.row() row.operator("bim.edit_object_placement") diff --git a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py index 48ec27e3c9..7d644a30df 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index 4d0979bfd0..5b3e09037d 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -1,68 +1,54 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.attribute import ifcopenshell.api +import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.georeference.data import Data +from ifcopenshell.api.unit.data import Data as UnitData from math import radians, degrees, atan, tan, cos, sin class EnableEditingGeoreferencing(bpy.types.Operator): bl_idname = "bim.enable_editing_georeferencing" bl_label = "Enable Editing Georeferencing" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMGeoreferenceProperties + self.props = props - while len(props.projected_crs) > 0: - props.projected_crs.remove(0) + props.projected_crs.clear() - for attribute in IfcStore.get_schema().declaration_by_name("IfcProjectedCRS").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": - continue - new = props.projected_crs.add() - new.name = attribute.name() - new.is_null = Data.projected_crs[attribute.name()] is None - new.is_optional = attribute.optional() - new.data_type = data_type - if data_type == "string": - new.string_value = "" if new.is_null else Data.projected_crs[attribute.name()] - elif data_type == "float": - new.float_value = 0.0 if new.is_null else Data.projected_crs[attribute.name()] - elif data_type == "integer": - new.int_value = 0 if new.is_null else Data.projected_crs[attribute.name()] - elif data_type == "boolean": - new.bool_value = False if new.is_null else Data.projected_crs[attribute.name()] + blenderbim.bim.helper.import_attributes( + "IfcProjectedCRS", props.projected_crs, Data.projected_crs, self.import_projected_crs_attributes + ) - props.is_map_unit_null = Data.projected_crs["MapUnit"] is None - if not props.is_map_unit_null: - props.map_unit_type = Data.projected_crs["MapUnit"]["type"] - if props.map_unit_type == "IfcSIUnit": - prefix = ifcopenshell.util.unit.get_prefix(Data.projected_crs["MapUnit"]["Prefix"]) or "" - name = ifcopenshell.util.unit.get_unit_name(Data.projected_crs["MapUnit"]["Name"]) - props.map_unit_si = prefix + name - elif props.map_unit_type == "IfcConversionBasedUnit": - props.map_unit_imperial = Data.projected_crs["MapUnit"]["Name"] - - while len(props.map_conversion) > 0: - props.map_conversion.remove(0) - - for attribute in IfcStore.get_schema().declaration_by_name("IfcMapConversion").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity" or data_type == "select": - continue - print(attribute.name(), data_type) - new = props.map_conversion.add() - new.name = attribute.name() - new.is_null = Data.map_conversion[attribute.name()] is None - new.is_optional = attribute.optional() - # Enforce a string data type to prevent data loss in singpe-precision Blender props - new.data_type = "string" - new.string_value = "" if new.is_null else str(Data.map_conversion[attribute.name()]) + props.map_conversion.clear() + blenderbim.bim.helper.import_attributes( + "IfcMapConversion", props.map_conversion, Data.map_conversion, self.import_map_conversion_attributes + ) props.has_true_north = bool(Data.true_north) if Data.true_north: @@ -72,10 +58,32 @@ class EnableEditingGeoreferencing(bpy.types.Operator): props.is_editing = True return {"FINISHED"} + def import_projected_crs_attributes(self, name, prop, data): + if name == "MapUnit": + new = self.props.projected_crs.add() + new.name = name + new.data_type = "enum" + new.is_null = data[name] is None + new.is_optional = True + new.enum_items = json.dumps( + {u["id"]: u["Name"] for u in UnitData.units.values() if u["UnitType"] == "LENGTHUNIT"} + ) + if data["MapUnit"]: + new.enum_value = str(data["MapUnit"]["id"]) + return True + + def import_map_conversion_attributes(self, name, prop, data): + if name not in ["SourceCRS", "TargetCRS"]: + # Enforce a string data type to prevent data loss in single-precision Blender props + prop.data_type = "string" + prop.string_value = "" if prop.is_null else str(data[name]) + return True + class DisableEditingGeoreferencing(bpy.types.Operator): bl_idname = "bim.disable_editing_georeferencing" bl_label = "Disable Editing Georeferencing" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): props = context.scene.BIMGeoreferenceProperties @@ -86,50 +94,24 @@ class DisableEditingGeoreferencing(bpy.types.Operator): class EditGeoreferencing(bpy.types.Operator): bl_idname = "bim.edit_georeferencing" bl_label = "Edit Georeferencing" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMGeoreferenceProperties - projected_crs = {} - for attribute in IfcStore.get_schema().declaration_by_name("IfcProjectedCRS").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": - continue - blender_attribute = props.projected_crs.get(attribute.name()) - if blender_attribute.is_null: - projected_crs[attribute.name()] = None - elif blender_attribute.data_type == "string": - projected_crs[attribute.name()] = blender_attribute.string_value - elif blender_attribute.data_type == "float": - projected_crs[attribute.name()] = blender_attribute.float_value - elif blender_attribute.data_type == "integer": - projected_crs[attribute.name()] = blender_attribute.int_value - elif blender_attribute.data_type == "boolean": - projected_crs[attribute.name()] = blender_attribute.bool_value - - map_unit = "" - if not props.is_map_unit_null: - map_unit = props.map_unit_si if props.map_unit_type == "IfcSIUnit" else props.map_unit_imperial - - map_conversion = {} - for attribute in IfcStore.get_schema().declaration_by_name("IfcMapConversion").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity" or data_type == "select": - continue - blender_attribute = props.map_conversion.get(attribute.name()) - if blender_attribute.is_null: - map_conversion[attribute.name()] = None - elif blender_attribute.data_type == "string": - # We store our floats as string to prevent single precision data loss - map_conversion[attribute.name()] = float(blender_attribute.string_value) + projected_crs = blenderbim.bim.helper.export_attributes(props.projected_crs, self.export_crs_attributes) + map_conversion = blenderbim.bim.helper.export_attributes(props.map_conversion, self.export_map_attributes) true_north = None if props.has_true_north: try: true_north = [float(props.true_north_abscissa), float(props.true_north_ordinate)] - except: - pass + except ValueError: + self.report({"ERROR"}, "True North Abscissa and Ordinate expect a number") ifcopenshell.api.run( "georeference.edit_georeferencing", @@ -137,18 +119,29 @@ class EditGeoreferencing(bpy.types.Operator): **{ "map_conversion": map_conversion, "projected_crs": projected_crs, - "map_unit": map_unit, "true_north": true_north, } ) - Data.load(IfcStore.get_file()) + Data.load(self.file) bpy.ops.bim.disable_editing_georeferencing() return {"FINISHED"} + def export_map_attributes(self, attributes, prop): + if not prop.is_null and prop.data_type == "string": + # We store our floats as string to prevent single precision data loss + attributes[prop.name] = float(prop.string_value) + return True + + def export_crs_attributes(self, attributes, prop): + if not prop.is_null and prop.name == "MapUnit": + attributes[prop.name] = self.file.by_id(int(prop.enum_value)) + return True + class SetBlenderGridNorth(bpy.types.Operator): bl_idname = "bim.set_blender_grid_north" bl_label = "Set Blender Grid North" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.sun_pos_properties.north_offset = -radians( @@ -163,6 +156,7 @@ class SetBlenderGridNorth(bpy.types.Operator): class SetIfcGridNorth(bpy.types.Operator): bl_idname = "bim.set_ifc_grid_north" bl_label = "Set IFC Grid North" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): x_angle = -context.scene.sun_pos_properties.north_offset @@ -174,6 +168,7 @@ class SetIfcGridNorth(bpy.types.Operator): class SetBlenderTrueNorth(bpy.types.Operator): bl_idname = "bim.set_blender_true_north" bl_label = "Set Blender True North" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.sun_pos_properties.north_offset = -radians( @@ -188,6 +183,7 @@ class SetBlenderTrueNorth(bpy.types.Operator): class SetIfcTrueNorth(bpy.types.Operator): bl_idname = "bim.set_ifc_true_north" bl_label = "Set IFC True North" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): y_angle = -context.scene.sun_pos_properties.north_offset + radians(90) @@ -199,8 +195,12 @@ class SetIfcTrueNorth(bpy.types.Operator): class RemoveGeoreferencing(bpy.types.Operator): bl_idname = "bim.remove_georeferencing" bl_label = "Remove Georeferencing" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run("georeference.remove_georeferencing", IfcStore.get_file()) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -209,8 +209,12 @@ class RemoveGeoreferencing(bpy.types.Operator): class AddGeoreferencing(bpy.types.Operator): bl_idname = "bim.add_georeferencing" bl_label = "Add Georeferencing" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run("georeference.add_georeferencing", IfcStore.get_file()) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -219,6 +223,13 @@ class AddGeoreferencing(bpy.types.Operator): class ConvertLocalToGlobal(bpy.types.Operator): bl_idname = "bim.convert_local_to_global" bl_label = "Convert Local To Global" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and props.coordinate_input.count(",") == 2 def execute(self, context): if not Data.is_loaded: @@ -226,11 +237,7 @@ class ConvertLocalToGlobal(bpy.types.Operator): props = context.scene.BIMGeoreferenceProperties x, y, z = [float(co) for co in props.coordinate_input.split(",")] - if props.has_blender_offset and props.blender_offset_type == "CARTESIAN_POINT": - x -= float(props.blender_eastings) - y -= float(props.blender_northings) - z -= float(props.blender_orthogonal_height) - elif props.has_blender_offset and props.blender_offset_type == "OBJECT_PLACEMENT": + if props.has_blender_offset: results = ifcopenshell.util.geolocation.xyz2enh( x, y, @@ -262,13 +269,20 @@ class ConvertLocalToGlobal(bpy.types.Operator): results = (x, y, z) props.coordinate_output = ",".join([str(r) for r in results]) - bpy.context.scene.cursor.location = results + context.scene.cursor.location = results return {"FINISHED"} class ConvertGlobalToLocal(bpy.types.Operator): bl_idname = "bim.convert_global_to_local" bl_label = "Convert Global To Local" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and file.by_type("IfcUnitAssignment") and props.coordinate_input.count(",") == 2 def execute(self, context): if not Data.is_loaded: @@ -291,11 +305,7 @@ class ConvertGlobalToLocal(bpy.types.Operator): else: results = (x, y, z) - if props.has_blender_offset and props.blender_offset_type == "CARTESIAN_POINT": - results[0] += float(props.blender_eastings) - results[1] += float(props.blender_northings) - results[2] += float(props.blender_orthogonal_height) - elif props.has_blender_offset and props.blender_offset_type == "OBJECT_PLACEMENT": + if props.has_blender_offset: results = ifcopenshell.util.geolocation.enh2xyz( results[0], results[1], @@ -311,18 +321,24 @@ class ConvertGlobalToLocal(bpy.types.Operator): props.coordinate_output = ",".join([str(r) for r in results]) scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) - bpy.context.scene.cursor.location = [o * scale for o in results] + context.scene.cursor.location = [o * scale for o in results] return {"FINISHED"} class GetCursorLocation(bpy.types.Operator): bl_idname = "bim.get_cursor_location" bl_label = "Get Cursor Location" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + return file and file.by_type("IfcUnitAssignment") def execute(self, context): props = context.scene.BIMGeoreferenceProperties scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) - project_coordinates = [o / scale for o in bpy.context.scene.cursor.location] + project_coordinates = [o / scale for o in context.scene.cursor.location] props.coordinate_input = ",".join([str(o) for o in project_coordinates]) return {"FINISHED"} @@ -330,9 +346,16 @@ class GetCursorLocation(bpy.types.Operator): class SetCursorLocation(bpy.types.Operator): bl_idname = "bim.set_cursor_location" bl_label = "Set Cursor Location" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and file.by_type("IfcUnitAssignment") and props.coordinate_output.count(",") == 2 def execute(self, context): props = context.scene.BIMGeoreferenceProperties scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) - bpy.context.scene.cursor.location = [float(co) * scale for co in props.coordinate_output.split(",")] + context.scene.cursor.location = [float(co) * scale for co in props.coordinate_output.split(",")] return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/georeference/prop.py b/src/blenderbim/blenderbim/bim/module/georeference/prop.py index 0bcaafdc7d..b85cb01275 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/prop.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup @@ -17,30 +35,9 @@ class BIMGeoreferenceProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") map_conversion: CollectionProperty(name="Map Conversion", type=Attribute) projected_crs: CollectionProperty(name="Projected CRS", type=Attribute) - map_unit_type: EnumProperty( - items=[(n, n, "") for n in ["IfcSIUnit", "IfcConversionBasedUnit"]], - name="Map Unit Type", - default="IfcSIUnit", - ) - map_unit_si: EnumProperty( - items=[(n, n.lower().capitalize(), "") for n in ["MILLIMETRE", "CENTIMETRE", "METRE", "KILOMETRE"]], - name="Map Unit SI", - default="METRE", - ) - map_unit_imperial: EnumProperty( - items=[(n, n.lower().capitalize(), "") for n in ["inch", "foot", "yard", "mile"]], - name="Map Unit SI", - default="foot", - ) - is_map_unit_null: BoolProperty(name="Is Map Unit Null") - coordinate_input: StringProperty(name="Coordinate Input") - coordinate_output: StringProperty(name="Coordinate Output") + coordinate_input: StringProperty(name="Coordinate Input", description='Formatted "x,y,z" (without quotes)') + coordinate_output: StringProperty(name="Coordinate Output", description='Formatted "x,y,z" (without quotes)') has_blender_offset: BoolProperty(name="Has Blender Offset") - blender_offset_type: EnumProperty( - items=[(o, o, "") for o in ["OBJECT_PLACEMENT", "CARTESIAN_POINT"]], - name="Blender Offset", - default="OBJECT_PLACEMENT", - ) blender_eastings: StringProperty(name="Blender Eastings", default="0") blender_northings: StringProperty(name="Blender Northings", default="0") blender_orthogonal_height: StringProperty(name="Blender Orthogonal Height", default="0") diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index be2f01a1f3..19b1a8b728 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -1,7 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import ifcopenshell.util.geolocation from bpy.types import Panel from ifcopenshell.api.georeference.data import Data from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes, draw_attribute class BIM_PT_gis(Panel): @@ -32,27 +51,9 @@ class BIM_PT_gis(Panel): row = self.layout.row(align=True) row.label(text="Projected CRS", icon="WORLD") row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_georeferencing", icon="X", text="") + row.operator("bim.disable_editing_georeferencing", icon="CANCEL", text="") - for attribute in props.projected_crs: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - - row = self.layout.row(align=True) - row.prop(props, "map_unit_type", text="MapUnit") - if props.map_unit_type == "IfcSIUnit": - row.prop(props, "map_unit_si", text="") - elif props.map_unit_type == "IfcConversionBasedUnit": - row.prop(props, "map_unit_imperial", text="") - row.prop(props, "is_map_unit_null", icon="RADIOBUT_OFF" if props.is_map_unit_null else "RADIOBUT_ON", text="") + draw_attributes(props.projected_crs, self.layout) row = self.layout.row() row.label(text="Map Conversion", icon="GRID") @@ -62,17 +63,7 @@ class BIM_PT_gis(Panel): row = self.layout.row(align=True) row.operator("bim.set_ifc_grid_north", text="Set IFC North") row.operator("bim.set_blender_grid_north", text="Set Blender North") - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attribute(attribute, self.layout.row()) row = self.layout.row() row.label(text="True North", icon="LIGHT_SUN") @@ -90,18 +81,16 @@ class BIM_PT_gis(Panel): def draw_ui(self, context): props = context.scene.BIMGeoreferenceProperties - if not Data.projected_crs and IfcStore.get_file().schema != "IFC2X3": + if not Data.projected_crs: row = self.layout.row(align=True) row.label(text="Not Georeferenced") - row.operator("bim.add_georeferencing", icon="ADD", text="") + if IfcStore.get_file().schema != "IFC2X3": + row.operator("bim.add_georeferencing", icon="ADD", text="") if props.has_blender_offset: row = self.layout.row() row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS") - row = self.layout.row(align=True) - row.label(text="Type") - row.label(text=props.blender_offset_type) row = self.layout.row(align=True) row.label(text="Eastings") row.label(text=props.blender_eastings) diff --git a/src/blenderbim/blenderbim/bim/module/group/__init__.py b/src/blenderbim/blenderbim/bim/module/group/__init__.py index d8871b4131..ddfc4ff48c 100644 --- a/src/blenderbim/blenderbim/bim/module/group/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/group/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -7,6 +25,7 @@ classes = ( operator.AddGroup, operator.EditGroup, operator.RemoveGroup, + operator.ToggleAssigningGroup, operator.AssignGroup, operator.UnassignGroup, operator.EnableEditingGroup, @@ -15,7 +34,9 @@ classes = ( prop.Group, prop.BIMGroupProperties, ui.BIM_PT_groups, + ui.BIM_PT_object_groups, ui.BIM_UL_groups, + ui.BIM_UL_object_groups, ) diff --git a/src/blenderbim/blenderbim/bim/module/group/operator.py b/src/blenderbim/blenderbim/bim/module/group/operator.py index 3b7ed59b70..fdf7548826 100644 --- a/src/blenderbim/blenderbim/bim/module/group/operator.py +++ b/src/blenderbim/blenderbim/bim/module/group/operator.py @@ -1,6 +1,25 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.util.attribute import ifcopenshell.api +import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.group.data import Data @@ -12,8 +31,7 @@ class LoadGroups(bpy.types.Operator): def execute(self, context): props = context.scene.BIMGroupProperties - while len(props.groups) > 0: - props.groups.remove(0) + props.groups.clear() for ifc_definition_id, group in Data.groups.items(): new = props.groups.add() new.ifc_definition_id = ifc_definition_id @@ -30,6 +48,7 @@ class DisableGroupEditingUI(bpy.types.Operator): def execute(self, context): context.scene.BIMGroupProperties.is_editing = False + context.scene.BIMGroupProperties.active_group_id = 0 return {"FINISHED"} @@ -100,20 +119,10 @@ class EnableEditingGroup(bpy.types.Operator): def execute(self, context): props = context.scene.BIMGroupProperties - while len(props.group_attributes) > 0: - props.group_attributes.remove(0) + props.group_attributes.clear() - data = Data.groups[self.group] + blenderbim.bim.helper.import_attributes("IfcGroup", props.group_attributes, Data.groups[self.group]) - for attribute in IfcStore.get_schema().declaration_by_name("IfcGroup").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity": - continue - new = props.group_attributes.add() - new.name = attribute.name() - new.is_null = data[attribute.name()] is None - new.is_optional = attribute.optional() - new.string_value = "" if new.is_null else data[attribute.name()] props.active_group_id = self.group return {"FINISHED"} @@ -128,6 +137,16 @@ class DisableEditingGroup(bpy.types.Operator): return {"FINISHED"} +class ToggleAssigningGroup(bpy.types.Operator): + bl_idname = "bim.toggle_assigning_group" + bl_label = "Toggle Assigning Group" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMGroupProperties.is_adding = not context.scene.BIMGroupProperties.is_adding + return {"FINISHED"} + + class AssignGroup(bpy.types.Operator): bl_idname = "bim.assign_group" bl_label = "Assign Group" @@ -139,17 +158,20 @@ class AssignGroup(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - product = bpy.data.objects.get(self.product) if self.product else context.active_object self.file = IfcStore.get_file() - ifcopenshell.api.run( - "group.assign_group", - self.file, - **{ - "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), - "group": self.file.by_id(self.group), - } - ) - Data.load(IfcStore.get_file()) + products = [bpy.data.objects.get(self.product)] if self.product else context.selected_objects + for product in products: + if not product.BIMObjectProperties.ifc_definition_id: + continue + ifcopenshell.api.run( + "group.assign_group", + self.file, + **{ + "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), + "group": self.file.by_id(self.group), + } + ) + Data.load(self.file) return {"FINISHED"} @@ -164,16 +186,19 @@ class UnassignGroup(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - product = bpy.data.objects.get(self.product) if self.product else context.active_object self.file = IfcStore.get_file() - ifcopenshell.api.run( - "group.unassign_group", - self.file, - **{ - "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), - "group": self.file.by_id(self.group), - } - ) + products = [bpy.data.objects.get(self.product)] if self.product else context.selected_objects + for product in products: + if not product.BIMObjectProperties.ifc_definition_id: + continue + ifcopenshell.api.run( + "group.unassign_group", + self.file, + **{ + "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), + "group": self.file.by_id(self.group), + } + ) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -186,7 +211,7 @@ class SelectGroupProducts(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: obj.select_set(False) if not obj.BIMObjectProperties.ifc_definition_id: continue diff --git a/src/blenderbim/blenderbim/bim/module/group/prop.py b/src/blenderbim/blenderbim/bim/module/group/prop.py index f754cd37b7..03011af513 100644 --- a/src/blenderbim/blenderbim/bim/module/group/prop.py +++ b/src/blenderbim/blenderbim/bim/module/group/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup @@ -21,6 +39,7 @@ class Group(PropertyGroup): class BIMGroupProperties(PropertyGroup): group_attributes: CollectionProperty(name="Group Attributes", type=Attribute) is_editing: BoolProperty(name="Is Editing", default=False) + is_adding: BoolProperty(name="Is Adding", default=False) groups: CollectionProperty(name="Groups", type=Group) active_group_index: IntProperty(name="Active Group Index") active_group_id: IntProperty(name="Active Group Id") diff --git a/src/blenderbim/blenderbim/bim/module/group/ui.py b/src/blenderbim/blenderbim/bim/module/group/ui.py index 21cd9a9870..a806564db5 100644 --- a/src/blenderbim/blenderbim/bim/module/group/ui.py +++ b/src/blenderbim/blenderbim/bim/module/group/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.group.data import Data @@ -49,37 +67,78 @@ class BIM_PT_groups(Panel): row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") +class BIM_PT_object_groups(Panel): + bl_label = "IFC Groups" + bl_idname = "BIM_PT_object_groups" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id + + def draw(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + self.props = context.scene.BIMGroupProperties + row = self.layout.row(align=True) + if self.props.is_adding: + row.label(text="Adding Groups", icon="OUTLINER") + row.operator("bim.toggle_assigning_group", text="", icon="CANCEL") + self.layout.template_list( + "BIM_UL_object_groups", + "", + self.props, + "groups", + self.props, + "active_group_index", + ) + else: + row.label(text=f"{len(Data.groups)} Groups in IFC Project", icon="OUTLINER") + row.operator("bim.toggle_assigning_group", text="", icon="ADD") + + groups_object = Data.products.get(context.active_object.BIMObjectProperties.ifc_definition_id, []) + for group_id in groups_object: + row = self.layout.row(align=True) + row.label(text=Data.groups[group_id].get("Name", "Unnamed")) + op = row.operator("bim.unassign_group", text="", icon="X") + op.group = group_id + + if not groups_object: + self.layout.label(text="No Group associated with Active Object") + class BIM_UL_groups(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: row = layout.row(align=True) row.label(text=item.name) - - if context.active_object: - oprops = context.active_object.BIMObjectProperties - if ( - oprops.ifc_definition_id in Data.products - and item.ifc_definition_id in Data.products[oprops.ifc_definition_id] - ): - op = row.operator("bim.unassign_group", text="", icon="KEYFRAME_HLT", emboss=False) - op.group = item.ifc_definition_id - else: - op = row.operator("bim.assign_group", text="", icon="KEYFRAME", emboss=False) - op.group = item.ifc_definition_id - - if context.scene.BIMGroupProperties.active_group_id == item.ifc_definition_id: + group_id = item.ifc_definition_id + if context.scene.BIMGroupProperties.active_group_id == group_id: op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF") - op.group = item.ifc_definition_id + op.group = group_id row.operator("bim.edit_group", text="", icon="CHECKMARK") row.operator("bim.disable_editing_group", text="", icon="CANCEL") elif context.scene.BIMGroupProperties.active_group_id: op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF") - op.group = item.ifc_definition_id - row.operator("bim.remove_group", text="", icon="X").group = item.ifc_definition_id + op.group = group_id + op = row.operator("bim.remove_group", text="", icon="X") + op.group = group_id else: op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF") - op.group = item.ifc_definition_id + op.group = group_id op = row.operator("bim.enable_editing_group", text="", icon="GREASEPENCIL") - op.group = item.ifc_definition_id - row.operator("bim.remove_group", text="", icon="X").group = item.ifc_definition_id + op.group = group_id + op = row.operator("bim.remove_group", text="", icon="X") + op.group = group_id + + +class BIM_UL_object_groups(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + op = row.operator("bim.assign_group", text="", icon="ADD") + op.group = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/module/layer/__init__.py b/src/blenderbim/blenderbim/bim/module/layer/__init__.py index ea302841c6..80cedcd07c 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/layer/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/layer/operator.py b/src/blenderbim/blenderbim/bim/module/layer/operator.py index 4ec6daa8c2..2c20351d20 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/operator.py +++ b/src/blenderbim/blenderbim/bim/module/layer/operator.py @@ -1,7 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell.util.attribute import ifcopenshell.api +import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.layer.data import Data @@ -14,8 +33,7 @@ class LoadLayers(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMLayerProperties - while len(props.layers) > 0: - props.layers.remove(0) + props.layers.clear() for layer_id, layer in Data.layers.items(): new = props.layers.add() new.name = layer["Name"] or "Unnamed" @@ -43,20 +61,12 @@ class EnableEditingLayer(bpy.types.Operator): def execute(self, context): props = context.scene.BIMLayerProperties - while len(props.layer_attributes) > 0: - props.layer_attributes.remove(0) + props.layer_attributes.clear() - data = Data.layers[self.layer] + blenderbim.bim.helper.import_attributes( + "IfcPresentationLayerAssignment", props.layer_attributes, Data.layers[self.layer] + ) - for attribute in IfcStore.get_schema().declaration_by_name("IfcPresentationLayerAssignment").all_attributes(): - data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) - if data_type == "entity" or data_type == "select": - continue - new = props.layer_attributes.add() - new.name = attribute.name() - new.is_null = data[attribute.name()] is None - new.is_optional = attribute.optional() - new.string_value = "" if new.is_null else data[attribute.name()] props.active_layer_id = self.layer return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/layer/prop.py b/src/blenderbim/blenderbim/bim/module/layer/prop.py index c832e45c22..c9c0c1fd28 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/prop.py +++ b/src/blenderbim/blenderbim/bim/module/layer/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup diff --git a/src/blenderbim/blenderbim/bim/module/layer/ui.py b/src/blenderbim/blenderbim/bim/module/layer/ui.py index 7ed1cc4da6..1508f370d4 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/ui.py +++ b/src/blenderbim/blenderbim/bim/module/layer/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel, UIList, Mesh from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.layer.data import Data @@ -25,7 +43,7 @@ class BIM_PT_layers(Panel): row.label(text="{} Layers Found".format(len(Data.layers.keys()))) if self.props.is_editing: row.operator("bim.add_presentation_layer", text="", icon="ADD") - row.operator("bim.disable_layer_editing_ui", text="", icon="X") + row.operator("bim.disable_layer_editing_ui", text="", icon="CANCEL") else: row.operator("bim.load_layers", text="", icon="GREASEPENCIL") @@ -73,7 +91,7 @@ class BIM_UL_layers(UIList): if context.scene.BIMLayerProperties.active_layer_id == item.ifc_definition_id: row.operator("bim.edit_presentation_layer", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_layer", text="", icon="X") + row.operator("bim.disable_editing_layer", text="", icon="CANCEL") elif context.scene.BIMLayerProperties.active_layer_id: row.operator("bim.remove_presentation_layer", text="", icon="X").layer = item.ifc_definition_id else: diff --git a/src/blenderbim/blenderbim/bim/module/lca/__init__.py b/src/blenderbim/blenderbim/bim/module/lca/__init__.py new file mode 100644 index 0000000000..b32db0bacd --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/lca/__init__.py @@ -0,0 +1,34 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +from . import ui, prop, operator + +classes = ( + operator.CalculateLCA, + prop.BIMLCAProperties, + ui.BIM_PT_lca, +) + + +def register(): + bpy.types.Scene.BIMLCAProperties = bpy.props.PointerProperty(type=prop.BIMLCAProperties) + + +def unregister(): + del bpy.types.Scene.BIMLCAProperties diff --git a/src/blenderbim/blenderbim/bim/module/lca/operator.py b/src/blenderbim/blenderbim/bim/module/lca/operator.py new file mode 100644 index 0000000000..06926f5009 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/lca/operator.py @@ -0,0 +1,45 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import olca +import ifcopenshell + + +class CalculateLCA(bpy.types.Operator): + bl_idname = "bim.calculate_lca" + bl_label = "Calculate LCA" + bl_options = {"REGISTER", "UNDO"} + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.xlsx", options={"HIDDEN"}) + + def execute(self, context): + props = context.scene.BIMLCAProperties + client = olca.Client(context.preferences.addons["blenderbim"].preferences.openlca_port) + setup = olca.CalculationSetup() + setup.calculation_type = olca.CalculationType.SIMPLE_CALCULATION + setup.impact_method = client.find(olca.ImpactMethod, "CML-IA baseline") + setup.product_system = client.find(olca.ProductSystem, props.product_systems) + setup.amount = props.amount + result = client.calculate(setup) + client.excel_export(result, self.filepath) + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} diff --git a/src/blenderbim/blenderbim/bim/module/lca/prop.py b/src/blenderbim/blenderbim/bim/module/lca/prop.py new file mode 100644 index 0000000000..d612b3c666 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/lca/prop.py @@ -0,0 +1,55 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import olca +from blenderbim.bim.prop import StrProperty, Attribute +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + +productsystems_enum = [] + + +def purge(): + global productsystems_enum + productsystems_enum = [] + + +def get_product_systems(self, context): + global productsystems_enum + if not len(productsystems_enum): + client = olca.Client(context.preferences.addons["blenderbim"].preferences.openlca_port) + try: + productsystems_enum = [(ps.name, ps.name, "") for ps in client.get_all(olca.ProductSystem)] + except: + pass + return productsystems_enum + + +class BIMLCAProperties(PropertyGroup): + amount: FloatProperty(name="Amount") + product_systems: EnumProperty(items=get_product_systems, name="Product Systems") diff --git a/src/blenderbim/blenderbim/bim/module/lca/ui.py b/src/blenderbim/blenderbim/bim/module/lca/ui.py new file mode 100644 index 0000000000..a48bf668b1 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/lca/ui.py @@ -0,0 +1,38 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +from bpy.types import Panel + + +class BIM_PT_lca(Panel): + bl_label = "BIM Life Cycle Assessment" + bl_idname = "BIM_PT_lca" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + def draw(self, context): + props = context.scene.BIMLCAProperties + row = self.layout.row() + row.prop(props, "product_systems", text="") + row = self.layout.row() + row.prop(props, "amount", text="") + row = self.layout.row() + row.operator("bim.calculate_lca") diff --git a/src/blenderbim/blenderbim/bim/module/material/__init__.py b/src/blenderbim/blenderbim/bim/module/material/__init__.py index 83ef17c184..aac236d890 100644 --- a/src/blenderbim/blenderbim/bim/module/material/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/material/__init__.py @@ -1,7 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator classes = ( + operator.CopyMaterial, operator.AddMaterial, operator.RemoveMaterial, operator.AssignMaterial, diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index d834166586..d72b86bbe8 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -1,7 +1,27 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell.api +import ifcopenshell.util.element import ifcopenshell.util.attribute +import ifcopenshell.util.representation import blenderbim.bim.helper from blenderbim.bim.module.material.prop import purge as material_prop_purge from blenderbim.bim.ifc import IfcStore @@ -12,12 +32,16 @@ from ifcopenshell.api.profile.data import Data as ProfileData class AssignParameterizedProfile(bpy.types.Operator): bl_idname = "bim.assign_parameterized_profile" bl_label = "Assign Parameterized Profile" + bl_options = {"REGISTER", "UNDO"} ifc_class: bpy.props.StringProperty() material_profile: bpy.props.IntProperty() obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() profile = ifcopenshell.api.run( "profile.add_parameterized_profile", @@ -38,13 +62,29 @@ class AssignParameterizedProfile(bpy.types.Operator): class AddMaterial(bpy.types.Operator): bl_idname = "bim.add_material" bl_label = "Add Material" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.materials.get(self.obj) if self.obj else bpy.context.active_object.active_material + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.materials.get(self.obj) if self.obj else context.active_object.active_material self.file = IfcStore.get_file() result = ifcopenshell.api.run("material.add_material", self.file, **{"name": obj.name}) - obj.BIMObjectProperties.ifc_definition_id = result.id() + IfcStore.link_element(result, obj) + if obj.BIMMaterialProperties.ifc_style_id: + context = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW") + if context: + ifcopenshell.api.run( + "style.assign_material_style", + self.file, + **{ + "material": result, + "style": self.file.by_id(obj.BIMMaterialProperties.ifc_style_id), + "context": context, + }, + ) Data.load(IfcStore.get_file()) material_prop_purge() return {"FINISHED"} @@ -53,10 +93,14 @@ class AddMaterial(bpy.types.Operator): class RemoveMaterial(bpy.types.Operator): bl_idname = "bim.remove_material" bl_label = "Remove Material" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.materials.get(self.obj) if self.obj else bpy.context.active_object.active_material + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.materials.get(self.obj) if self.obj else context.active_object.active_material self.file = IfcStore.get_file() result = ifcopenshell.api.run( "material.remove_material", @@ -71,18 +115,23 @@ class RemoveMaterial(bpy.types.Operator): class AssignMaterial(bpy.types.Operator): bl_idname = "bim.assign_material" bl_label = "Assign Material" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() material_type: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object material_type = self.material_type or obj.BIMObjectMaterialProperties.material_type self.file = IfcStore.get_file() + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) ifcopenshell.api.run( "material.assign_material", self.file, **{ - "product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), + "product": element, "type": material_type, "material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material)), }, @@ -95,28 +144,36 @@ class AssignMaterial(bpy.types.Operator): class UnassignMaterial(bpy.types.Operator): bl_idname = "bim.unassign_material" bl_label = "Unassign Material" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "material.unassign_material", self.file, **{"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)}, ) - Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) + Data.purge() return {"FINISHED"} class AddConstituent(bpy.types.Operator): bl_idname = "bim.add_constituent" bl_label = "Add Constituent" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() constituent_set: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "material.add_constituent", @@ -133,11 +190,15 @@ class AddConstituent(bpy.types.Operator): class RemoveConstituent(bpy.types.Operator): bl_idname = "bim.remove_constituent" bl_label = "Remove Constituent" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() constituent: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "material.remove_constituent", self.file, **{"constituent": self.file.by_id(self.constituent)} @@ -149,11 +210,15 @@ class RemoveConstituent(bpy.types.Operator): class AddProfile(bpy.types.Operator): bl_idname = "bim.add_profile" bl_label = "Add Profile" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() profile_set: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "material.add_profile", @@ -164,31 +229,41 @@ class AddProfile(bpy.types.Operator): }, ) Data.load_profiles() + ProfileData.load(self.file) return {"FINISHED"} class RemoveProfile(bpy.types.Operator): bl_idname = "bim.remove_profile" bl_label = "Remove Profile" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() profile: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run("material.remove_profile", self.file, **{"profile": self.file.by_id(self.profile)}) Data.load_profiles() + ProfileData.load(self.file) return {"FINISHED"} class AddLayer(bpy.types.Operator): bl_idname = "bim.add_layer" bl_label = "Add Layer" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() layer_set: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "material.add_layer", @@ -205,13 +280,17 @@ class AddLayer(bpy.types.Operator): class ReorderMaterialSetItem(bpy.types.Operator): bl_idname = "bim.reorder_material_set_item" bl_label = "Reorder Material Set Item" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() old_index: bpy.props.IntProperty() new_index: bpy.props.IntProperty() material_set: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() material_set = self.file.by_id(self.material_set) ifcopenshell.api.run( @@ -229,6 +308,7 @@ class ReorderMaterialSetItem(bpy.types.Operator): Data.load_layers() elif material_set.is_a("IfcMaterialProfileSet"): Data.load_profiles() + ProfileData.load(self.file) elif material_set.is_a("IfcMaterialList"): Data.load_lists() return {"FINISHED"} @@ -237,11 +317,15 @@ class ReorderMaterialSetItem(bpy.types.Operator): class RemoveLayer(bpy.types.Operator): bl_idname = "bim.remove_layer" bl_label = "Remove Layer" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() layer: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run("material.remove_layer", self.file, **{"layer": self.file.by_id(self.layer)}) Data.load_layers() @@ -251,11 +335,15 @@ class RemoveLayer(bpy.types.Operator): class AddListItem(bpy.types.Operator): bl_idname = "bim.add_list_item" bl_label = "Add List Item" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() list_item_set: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "material.add_list_item", @@ -272,19 +360,24 @@ class AddListItem(bpy.types.Operator): class RemoveListItem(bpy.types.Operator): bl_idname = "bim.remove_list_item" bl_label = "Remove List Item" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() list_item_set: bpy.props.IntProperty() list_item: bpy.props.IntProperty() + list_item_index: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "material.remove_list_item", self.file, **{ "material_list": self.file.by_id(self.list_item_set), - "material": self.file.by_id(self.list_item), + "material_index": self.list_item_index, }, ) Data.load_lists() @@ -294,10 +387,11 @@ class RemoveListItem(bpy.types.Operator): class EnableEditingAssignedMaterial(bpy.types.Operator): bl_idname = "bim.enable_editing_assigned_material" bl_label = "Enable Editing Assigned Material" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object props = obj.BIMObjectMaterialProperties props.is_editing = True product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] @@ -326,25 +420,16 @@ class EnableEditingAssignedMaterial(bpy.types.Operator): else: material_set_data = {} - while len(props.material_set_usage_attributes) > 0: - props.material_set_usage_attributes.remove(0) + props.material_set_usage_attributes.clear() if "Usage" in product_data["type"]: blenderbim.bim.helper.import_attributes( product_data["type"], props.material_set_usage_attributes, material_set_usage, self.import_attributes ) - while len(props.material_set_attributes) > 0: - props.material_set_attributes.remove(0) + props.material_set_attributes.clear() - for attribute in IfcStore.get_schema().declaration_by_name(material_set_class).all_attributes(): - if "" not in str(attribute.type_of_attribute): - continue - if attribute.name() in material_set_data: - new = props.material_set_attributes.add() - new.name = attribute.name() - new.is_null = material_set_data[attribute.name()] is None - new.string_value = "" if new.is_null else material_set_data[attribute.name()] + blenderbim.bim.helper.import_attributes(material_set_class, props.material_set_attributes, material_set_data) return {"FINISHED"} def import_attributes(self, name, prop, data): @@ -381,10 +466,11 @@ class EnableEditingAssignedMaterial(bpy.types.Operator): class DisableEditingAssignedMaterial(bpy.types.Operator): bl_idname = "bim.disable_editing_assigned_material" bl_label = "Disable Editing Assigned Material" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object props = obj.BIMObjectMaterialProperties props.is_editing = False return {"FINISHED"} @@ -393,16 +479,19 @@ class DisableEditingAssignedMaterial(bpy.types.Operator): class EditAssignedMaterial(bpy.types.Operator): bl_idname = "bim.edit_assigned_material" bl_label = "Edit Assigned Material" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() material_set: bpy.props.IntProperty() material_set_usage: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object props = obj.BIMObjectMaterialProperties product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] - material_set = self.file.by_id(self.material_set) if product_data["type"] == "IfcMaterial": bpy.ops.bim.unassign_material(obj=obj.name) @@ -411,9 +500,8 @@ class EditAssignedMaterial(bpy.types.Operator): bpy.ops.bim.disable_editing_assigned_material(obj=obj.name) return {"FINISHED"} - attributes = {} - for attribute in props.material_set_attributes: - attributes[attribute.name] = None if attribute.is_null else attribute.string_value + material_set = self.file.by_id(self.material_set) + attributes = blenderbim.bim.helper.export_attributes(props.material_set_attributes) ifcopenshell.api.run( "material.edit_assigned_material", self.file, @@ -424,15 +512,21 @@ class EditAssignedMaterial(bpy.types.Operator): if self.material_set_usage: material_set_usage = self.file.by_id(self.material_set_usage) attributes = blenderbim.bim.helper.export_attributes(props.material_set_usage_attributes) - attributes["CardinalPoint"] = int(attributes["CardinalPoint"]) if attributes["CardinalPoint"] else None - ifcopenshell.api.run( - "material.edit_profile_usage", - self.file, - **{"usage": material_set_usage, "attributes": attributes}, - ) if material_set_usage.is_a("IfcMaterialLayerSetUsage"): + ifcopenshell.api.run( + "material.edit_layer_usage", + self.file, + **{"usage": material_set_usage, "attributes": attributes}, + ) Data.load_layer_usages() elif material_set_usage.is_a("IfcMaterialProfileSetUsage"): + if attributes.get("CardinalPoint", None): + attributes["CardinalPoint"] = int(attributes["CardinalPoint"]) + ifcopenshell.api.run( + "material.edit_profile_usage", + self.file, + **{"usage": material_set_usage, "attributes": attributes}, + ) Data.load_profile_usages() if material_set.is_a("IfcMaterialConstituentSet"): @@ -448,12 +542,13 @@ class EditAssignedMaterial(bpy.types.Operator): class EnableEditingMaterialSetItem(bpy.types.Operator): bl_idname = "bim.enable_editing_material_set_item" bl_label = "Enable Editing Material Set Item" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() material_set_item: bpy.props.IntProperty() def execute(self, context): self.file = IfcStore.get_file() - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.props = obj.BIMObjectMaterialProperties self.props.active_material_set_item_id = self.material_set_item product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] @@ -477,8 +572,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): return {"FINISHED"} def load_set_item_attributes(self, material_set_item, material_set_item_data): - while len(self.props.material_set_item_attributes) > 0: - self.props.material_set_item_attributes.remove(0) + self.props.material_set_item_attributes.clear() for attribute in IfcStore.get_schema().declaration_by_name(material_set_item.is_a()).all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) @@ -499,8 +593,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): new.bool_value = False if new.is_null else material_set_item_data[attribute.name()] def load_profile_attributes(self, material_set_item, material_set_item_data): - while len(self.props.material_set_item_profile_attributes) > 0: - self.props.material_set_item_profile_attributes.remove(0) + self.props.material_set_item_profile_attributes.clear() if not material_set_item_data["Profile"]: return @@ -541,10 +634,11 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): class DisableEditingMaterialSetItem(bpy.types.Operator): bl_idname = "bim.disable_editing_material_set_item" bl_label = "Disable Editing Material Set Item" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object props = obj.BIMObjectMaterialProperties props.active_material_set_item_id = 0 return {"FINISHED"} @@ -553,26 +647,20 @@ class DisableEditingMaterialSetItem(bpy.types.Operator): class EditMaterialSetItem(bpy.types.Operator): bl_idname = "bim.edit_material_set_item" bl_label = "Edit Material Set Item" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() material_set_item: bpy.props.IntProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() props = obj.BIMObjectMaterialProperties product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] - attributes = {} - for attribute in props.material_set_item_attributes: - if attribute.data_type == "string": - value = attribute.string_value - elif attribute.data_type == "float": - value = attribute.float_value - elif attribute.data_type == "integer": - value = attribute.int_value - elif attribute.data_type == "boolean": - value = attribute.bool_value - attributes[attribute.name] = None if attribute.is_null else value + attributes = blenderbim.bim.helper.export_attributes(props.material_set_item_attributes) if product_data["type"] == "IfcMaterialConstituentSet": ifcopenshell.api.run( @@ -597,19 +685,7 @@ class EditMaterialSetItem(bpy.types.Operator): ) Data.load_layers() elif product_data["type"] == "IfcMaterialProfileSet" or product_data["type"] == "IfcMaterialProfileSetUsage": - profile_attributes = {} - for attribute in props.material_set_item_profile_attributes: - if attribute.data_type == "string": - value = attribute.string_value - elif attribute.data_type == "float": - value = attribute.float_value - elif attribute.data_type == "integer": - value = attribute.int_value - elif attribute.data_type == "boolean": - value = attribute.bool_value - elif attribute.data_type == "enum": - value = attribute.enum_value - profile_attributes[attribute.name] = None if attribute.is_null else value + profile_attributes = blenderbim.bim.helper.export_attributes(props.material_set_item_profile_attributes) ifcopenshell.api.run( "material.edit_profile", self.file, @@ -627,3 +703,45 @@ class EditMaterialSetItem(bpy.types.Operator): bpy.ops.bim.disable_editing_material_set_item(obj=obj.name) return {"FINISHED"} + + +class CopyMaterial(bpy.types.Operator): + bl_idname = "bim.copy_material" + bl_label = "Copy Material" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + material = ifcopenshell.util.element.get_material( + self.file.by_id(context.active_object.BIMObjectProperties.ifc_definition_id) + ) + for obj in context.selected_objects: + if obj == context.active_object: + continue + if not obj.BIMObjectProperties.ifc_definition_id: + continue + ifcopenshell.api.run( + "material.copy_material", + self.file, + **{ + "material": material, + "element": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), + }, + ) + Data.load(self.file, obj.BIMObjectProperties.ifc_definition_id) + self.set_default_material(obj, material) + return {"FINISHED"} + + def set_default_material(self, obj, material): + object_material_ids = [ + om.BIMObjectProperties.ifc_definition_id + for om in obj.data.materials + if om is not None and om.BIMObjectProperties.ifc_definition_id + ] + + if material.id() in object_material_ids: + return + obj.data.materials.append(IfcStore.get_element(material.id())) diff --git a/src/blenderbim/blenderbim/bim/module/material/prop.py b/src/blenderbim/blenderbim/bim/module/material/prop.py index 088a5c7962..660303d471 100644 --- a/src/blenderbim/blenderbim/bim/module/material/prop.py +++ b/src/blenderbim/blenderbim/bim/module/material/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from ifcopenshell.api.material.data import Data from blenderbim.bim.ifc import IfcStore @@ -49,6 +67,13 @@ def getParameterizedProfileClasses(self, context): (t.name(), t.name(), "") for t in IfcStore.get_schema().declaration_by_name("IfcParameterizedProfileDef").subtypes() ] + for ifc_class in parameterizedprofileclasses_enum: + parameterizedprofileclasses_enum.extend( + [ + (t.name(), t.name(), "") + for t in IfcStore.get_schema().declaration_by_name(ifc_class[0]).subtypes() or [] + ] + ) return parameterizedprofileclasses_enum @@ -87,7 +112,9 @@ class BIMObjectMaterialProperties(PropertyGroup): material_set_attributes: CollectionProperty(name="Material Set Attributes", type=Attribute) active_material_set_item_id: IntProperty(name="Active Material Set ID") material_set_item_attributes: CollectionProperty(name="Material Set Item Attributes", type=Attribute) - material_set_item_profile_attributes: CollectionProperty(name="Material Set Item Profile Attributes", type=Attribute) + material_set_item_profile_attributes: CollectionProperty( + name="Material Set Item Profile Attributes", type=Attribute + ) material_set_item_material: EnumProperty(items=getMaterials, name="Material") profile_classes: EnumProperty(items=getProfileClasses, name="Profile Classes") parameterized_profile_classes: EnumProperty( diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 0c21497055..d89f48a2ac 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -1,8 +1,27 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import blenderbim.bim.helper from bpy.types import Panel from ifcopenshell.api.material.data import Data from ifcopenshell.api.profile.data import Data as ProfileData from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes class BIM_PT_material(Panel): @@ -97,6 +116,8 @@ class BIM_PT_object_material(Panel): self.material_set_data = Data.lists[self.material_set_id] self.set_items = self.material_set_data["Materials"] or [] self.set_item_name = "list_item" + else: + self.material_set_id = 0 return self.draw_material_ui() row = self.layout.row(align=True) @@ -116,6 +137,8 @@ class BIM_PT_object_material(Panel): op.material_set_usage = self.product_data["id"] row.operator("bim.disable_editing_assigned_material", icon="CANCEL", text="") else: + if self.product_data["type"] == "IfcMaterial": + row.operator("bim.copy_material", icon="COPYDOWN", text="") row.operator("bim.enable_editing_assigned_material", icon="GREASEPENCIL", text="") row.operator("bim.unassign_material", icon="X", text="") @@ -177,17 +200,7 @@ class BIM_PT_object_material(Panel): op.material_set_item = set_item_id row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="") - for attribute in self.props.material_set_item_attributes: - row = box.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.material_set_item_attributes, self.layout) if self.set_item_name == "profile": self.draw_assign_profile_ui(box, item) @@ -198,7 +211,9 @@ class BIM_PT_object_material(Panel): row.prop(self.props, "profile_classes", text="") if self.props.profile_classes == "IfcParameterizedProfileDef": row.prop(self.props, "parameterized_profile_classes", text="") - op = row.operator("bim.assign_parameterized_profile", icon="GREASEPENCIL" if item["Profile"] else "ADD", text="") + op = row.operator( + "bim.assign_parameterized_profile", icon="GREASEPENCIL" if item["Profile"] else "ADD", text="" + ) op.ifc_class = self.props.parameterized_profile_classes op.material_profile = item["id"] else: @@ -207,20 +222,7 @@ class BIM_PT_object_material(Panel): row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="") def draw_editable_profile_ui(self, layout, item): - for attribute in self.props.material_set_item_profile_attributes: - row = layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.material_set_item_profile_attributes, self.layout) def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False): if self.product_data["type"] == "IfcMaterialList": @@ -255,6 +257,8 @@ class BIM_PT_object_material(Panel): if self.product_data["type"] == "IfcMaterialList": setattr(op, "list_item_set", self.material_set_id) setattr(op, self.set_item_name, item["id"]) + if hasattr(op, f"{self.set_item_name}_index"): + setattr(op, f"{self.set_item_name}_index", index) def draw_read_only_set_ui(self): if ( @@ -316,11 +320,11 @@ class BIM_PT_object_material(Panel): item_name = item.get("Name", "Unnamed") or "Unnamed" thickness = item.get("LayerThickness") if thickness: - item_name += f" ({thickness})" + item_name += f" ({thickness:.3f})" total_thickness += thickness row.label(text=item_name, icon="ALIGN_CENTER") row.label(text=Data.materials[item["Material"]]["Name"], icon="MATERIAL") if total_thickness: row = self.layout.row(align=True) - row.label(text=f"Total Thickness: {total_thickness}") + row.label(text=f"Total Thickness: {total_thickness:.3f}") diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 1e80ecb859..00f3ab557c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -1,13 +1,36 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import handler, prop, ui, grid, product, wall, slab, stair, door, window, opening, pie, workspace classes = ( + product.AddEmptyType, product.AddTypeInstance, - wall.AddWall, + product.AlignProduct, + product.DynamicallyVoidProduct, + workspace.Hotkey, wall.JoinWall, wall.AlignWall, wall.FlipWall, wall.SplitWall, + opening.AddElementOpening, + profile.ExtendProfile, prop.BIMModelProperties, ui.BIM_PT_authoring, ui.BIM_PT_authoring_architectural, @@ -20,13 +43,6 @@ classes = ( pie.OpenPieClass, pie.PieUpdateContainer, pie.PieAddOpening, - pie.AssignIfcWall, - pie.AssignIfcSlab, - pie.AssignIfcStair, - pie.AssignIfcDoor, - pie.AssignIfcWindow, - pie.AssignIfcColumn, - pie.AssignIfcBeam, pie.VIEW3D_MT_PIE_bim, pie.VIEW3D_MT_PIE_bim_class, ) @@ -35,13 +51,15 @@ addon_keymaps = [] def register(): - bpy.utils.register_tool(workspace.BimTool, after={"builtin.scale_cage"}, separator=True, group=True) + if not bpy.app.background: + bpy.utils.register_tool(workspace.BimTool, after={"builtin.scale_cage"}, separator=True, group=True) bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties) bpy.types.VIEW3D_MT_mesh_add.append(grid.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(stair.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(door.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(window.add_object_button) bpy.types.VIEW3D_MT_mesh_add.append(opening.add_object_button) + bpy.types.VIEW3D_MT_add.append(product.add_empty_type_button) bpy.app.handlers.load_post.append(handler.load_post) wm = bpy.context.window_manager if wm.keyconfigs.addon: @@ -52,7 +70,8 @@ def register(): def unregister(): - bpy.utils.unregister_tool(workspace.BimTool) + if not bpy.app.background: + bpy.utils.unregister_tool(workspace.BimTool) del bpy.types.Scene.BIMModelProperties bpy.app.handlers.load_post.remove(handler.load_post) bpy.types.VIEW3D_MT_mesh_add.remove(grid.add_object_button) @@ -60,6 +79,7 @@ def unregister(): bpy.types.VIEW3D_MT_mesh_add.remove(door.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(window.add_object_button) bpy.types.VIEW3D_MT_mesh_add.remove(opening.add_object_button) + bpy.types.VIEW3D_MT_add.remove(product.add_empty_type_button) wm = bpy.context.window_manager kc = wm.keyconfigs.addon if kc: diff --git a/src/blenderbim/blenderbim/bim/module/model/column.py b/src/blenderbim/blenderbim/bim/module/model/column.py deleted file mode 100644 index 9adf87eb59..0000000000 --- a/src/blenderbim/blenderbim/bim/module/model/column.py +++ /dev/null @@ -1,168 +0,0 @@ -import bpy -import bmesh -import math -import ifcopenshell -import ifcopenshell.util.type -import ifcopenshell.util.unit -import ifcopenshell.util.element -import mathutils.geometry -import blenderbim.bim.handler -from blenderbim.bim.ifc import IfcStore -from math import pi, degrees -from mathutils import Vector, Matrix -from ifcopenshell.api.pset.data import Data as PsetData -from ifcopenshell.api.material.data import Data as MaterialData -from blenderbim.bim.module.geometry.helper import Helper - - -def element_listener(element, obj): - blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback) - - -def mode_callback(obj, data): - for obj in bpy.context.selected_objects + [bpy.context.active_object]: - if ( - obj.mode != "EDIT" - or not obj.data - or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve)) - or not obj.BIMObjectProperties.ifc_definition_id - or not bpy.context.scene.BIMProjectProperties.is_authoring - ): - return - product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) - parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbColumn": - return - IfcStore.edited_objs.add(obj) - bm = bmesh.from_edit_mesh(obj.data) - bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) - bmesh.update_edit_mesh(obj.data) - bm.free() - - -def ensure_solid(usecase_path, ifc_file, settings): - product = ifc_file.by_id(settings["blender_object"].BIMObjectProperties.ifc_definition_id) - parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbColumn": - return - material = ifcopenshell.util.element.get_material(product) - if material and material.is_a("IfcMaterialProfileSetUsage"): - settings["profile_set_usage"] = material - else: - return - settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage" - - -class DumbColumnGenerator: - def __init__(self, relating_type): - self.relating_type = relating_type - - def generate(self): - self.file = IfcStore.get_file() - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) - material = ifcopenshell.util.element.get_material(self.relating_type) - if material and material.is_a("IfcMaterialProfileSet"): - self.profile_set = material - else: - return - - self.collection = bpy.context.view_layer.active_layer_collection.collection - self.collection_obj = bpy.data.objects.get(self.collection.name) - self.length = 3 - self.rotation = 0 - self.location = Vector((0, 0, 0)) - return self.derive_from_cursor() - - def derive_from_cursor(self): - self.location = bpy.context.scene.cursor.location - return self.create_column() - - def create_column(self): - # A cube - verts = [ - Vector((-1, -1, -1)), - Vector((-1, -1, 1)), - Vector((-1, 1, -1)), - Vector((-1, 1, 1)), - Vector((1, -1, -1)), - Vector((1, -1, 1)), - Vector((1, 1, -1)), - Vector((1, 1, 1)), - ] - edges = [] - faces = [ - [0, 2, 3, 1], - [2, 3, 7, 6], - [4, 5, 7, 6], - [0, 1, 5, 4], - [1, 3, 7, 5], - [0, 2, 6, 4], - ] - - mesh = bpy.data.meshes.new(name="Dumb Column") - mesh.from_pydata(verts, edges, faces) - obj = bpy.data.objects.new("Column", mesh) - obj.name = "Column" - obj.location = self.location - if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id: - obj.location[2] = self.collection_obj.location[2] - self.collection.objects.link(obj) - bpy.ops.bim.assign_class( - obj=obj.name, ifc_class="IfcColumn", predefined_type="COLUMN", should_add_representation=False - ) - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name) - profile_set_usage = ifcopenshell.util.element.get_material(element) - bpy.ops.bim.add_representation( - obj=obj.name, - context_id=ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW").id(), - ifc_representation_class="IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage", - profile_set_usage=profile_set_usage.id(), - ) - representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - bpy.ops.bim.switch_representation(obj=obj.name, ifc_definition_id=representation.id(), should_reload=True) - pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") - ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbColumn"}) - MaterialData.load(self.file) - obj.select_set(True) - return obj - - -class DumbColumnRegenerator: - def regenerate_from_profile(self, usecase_path, ifc_file, settings): - self.file = IfcStore.get_file() - self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - profile = settings["profile"].Profile - if not profile: - return - for profile_set in [ - mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile") - ]: - for inverse in ifc_file.get_inverse(profile_set): - if not inverse.is_a("IfcMaterialProfileSetUsage"): - continue - if ifc_file.schema == "IFC2X3": - for rel in ifc_file.get_inverse(inverse): - if not rel.is_a("IfcRelAssociatesMaterial"): - continue - for element in rel.RelatedObjects: - self.change_profile(element) - else: - for rel in inverse.AssociatedTo: - for element in rel.RelatedObjects: - self.change_profile(element) - - def regenerate_from_type(self, usecase_path, ifc_file, settings): - self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - new_material = ifcopenshell.util.element.get_material(settings["relating_type"]) - if not new_material or not new_material.is_a("IfcMaterialProfileSet"): - return - self.change_profile(settings["related_object"]) - - def change_profile(self, element): - obj = IfcStore.get_element(element.id()) - if not obj: - return - representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - if representation: - bpy.ops.bim.switch_representation(obj=obj.name, ifc_definition_id=representation.id(), should_reload=True) diff --git a/src/blenderbim/blenderbim/bim/module/model/door.py b/src/blenderbim/blenderbim/bim/module/model/door.py index ee8fbcb6c1..fb89e134e6 100644 --- a/src/blenderbim/blenderbim/bim/module/model/door.py +++ b/src/blenderbim/blenderbim/bim/module/model/door.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import math import ifcopenshell @@ -11,7 +29,7 @@ def add_object(self, context): guid = ifcopenshell.guid.new() leaf_width = self.overall_width - 0.045 - 0.045 # TODO reimplement 2D. See #1222. - #verts = [ + # verts = [ # # Left lining # Vector((0, 0, 0)), # Vector((0, self.depth, 0)), @@ -31,8 +49,8 @@ def add_object(self, context): # Vector((0.045, self.depth + leaf_width, 0)), # Vector((0.080, self.depth + leaf_width, 0)), # Vector((0.080, self.depth, 0)), - #] - #edges = [ + # ] + # edges = [ # [0, 1], # [1, 2], # [2, 3], @@ -47,9 +65,9 @@ def add_object(self, context): # [13, 14], # [14, 15], # [15, 12], # Door panel - #] + # ] ## Door swing - #for i in range(0, 9): + # for i in range(0, 9): # verts.append( # Vector( # ( @@ -60,11 +78,11 @@ def add_object(self, context): # ) # ) # edges.append([16 + i, 17 + i]) - #edges.pop() - #faces = [] - #mesh = bpy.data.meshes.new(name="Plan/Annotation/PLAN_VIEW/" + guid) - #mesh.use_fake_user = True - #mesh.from_pydata(verts, edges, faces) + # edges.pop() + # faces = [] + # mesh = bpy.data.meshes.new(name="Plan/Annotation/PLAN_VIEW/" + guid) + # mesh.use_fake_user = True + # mesh.from_pydata(verts, edges, faces) # Door lining profile verts = [ @@ -82,7 +100,7 @@ def add_object(self, context): obj = bpy.data.objects.new("Door Profile", mesh) context.view_layer.active_layer_collection.collection.objects.link(obj) - bpy.context.view_layer.objects.active = obj + context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.convert(target="CURVE") @@ -100,7 +118,7 @@ def add_object(self, context): obj2 = bpy.data.objects.new("Door", mesh) context.view_layer.active_layer_collection.collection.objects.link(obj2) - bpy.context.view_layer.objects.active = obj2 + context.view_layer.objects.active = obj2 obj2.select_set(True) bpy.ops.object.convert(target="CURVE") @@ -130,11 +148,11 @@ def add_object(self, context): modifier.thickness = self.overall_height - 0.045 context.view_layer.active_layer_collection.collection.objects.link(obj3) - bpy.context.view_layer.objects.active = obj3 + context.view_layer.objects.active = obj3 obj3.select_set(True) bpy.ops.object.convert(target="MESH") - ctx = bpy.context.copy() + ctx = context.copy() ctx["active_object"] = obj2 ctx["selected_editable_objects"] = [obj2, obj3] bpy.ops.object.join(ctx) @@ -156,7 +174,7 @@ def add_object(self, context): modifier.thickness = self.overall_height + 0.1 context.view_layer.active_layer_collection.collection.objects.link(obj4) - bpy.context.view_layer.objects.active = obj4 + context.view_layer.objects.active = obj4 obj4.select_set(True) bpy.ops.object.convert(target="MESH") @@ -172,30 +190,34 @@ def add_object(self, context): bpy.ops.bim.assign_class(obj=obj2.name, ifc_class="IfcDoor") obj2.location = context.scene.cursor.location - #obj2.data.name = "Model/Body/MODEL_VIEW/" + guid - #obj2.data.use_fake_user = True + # obj2.data.name = "Model/Body/MODEL_VIEW/" + guid + # obj2.data.use_fake_user = True # TODO: reimplement. See #1222. - #rep = obj2.BIMObjectProperties.representation_contexts.add() - #rep.context = "Model" - #rep.name = "Body" - #rep.target_view = "MODEL_VIEW" + # rep = obj2.BIMObjectProperties.representation_contexts.add() + # rep.context = "Model" + # rep.name = "Body" + # rep.target_view = "MODEL_VIEW" - #rep = obj2.BIMObjectProperties.representation_contexts.add() - #rep.context = "Plan" - #rep.name = "Annotation" - #rep.target_view = "PLAN_VIEW" + # rep = obj2.BIMObjectProperties.representation_contexts.add() + # rep.context = "Plan" + # rep.name = "Annotation" + # rep.target_view = "PLAN_VIEW" class BIM_OT_add_object(Operator): bl_idname = "mesh.add_door" bl_label = "Dumb Door" + bl_options = {"REGISTER", "UNDO"} overall_width: FloatProperty(name="Overall Width", default=0.85) overall_height: FloatProperty(name="Overall Height", default=2.1) depth: FloatProperty(name="Depth", default=0.2) def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): add_object(self, context) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/grid.py b/src/blenderbim/blenderbim/bim/module/model/grid.py index c94c68e6be..08b424bb6b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/grid.py +++ b/src/blenderbim/blenderbim/bim/module/model/grid.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api from bpy.types import Operator @@ -12,7 +30,7 @@ def add_object(self, context): collection = bpy.data.collections.new(obj.name) has_site_collection = False - for child in bpy.context.view_layer.layer_collection.children: + for child in context.view_layer.layer_collection.children: if "IfcProject/" not in child.name: continue for grandchild in child.children: @@ -22,7 +40,7 @@ def add_object(self, context): grandchild.collection.children.link(collection) break if not has_site_collection: - bpy.context.view_layer.active_layer_collection.collection.children.link(collection) + context.view_layer.active_layer_collection.collection.children.link(collection) collection.objects.link(obj) self.file = IfcStore.get_file() @@ -101,6 +119,9 @@ class BIM_OT_add_object(Operator): total_v: IntProperty(name="Number of V Grids", default=3) def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): add_object(self, context) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/handler.py b/src/blenderbim/blenderbim/bim/module/model/handler.py index 91f6182871..10abf5b2a4 100644 --- a/src/blenderbim/blenderbim/bim/module/model/handler.py +++ b/src/blenderbim/blenderbim/bim/module/model/handler.py @@ -1,23 +1,65 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell import ifcopenshell.api -from blenderbim.bim.module.model import product, wall, slab, column +from blenderbim.bim.module.model import root, product, wall, slab, profile, opening, task from blenderbim.bim.ifc import IfcStore from bpy.app.handlers import persistent @persistent def load_post(*args): + ifcopenshell.api.add_pre_listener("attribute.edit_attributes", "BlenderBIM.Root.SyncName", root.sync_name) + ifcopenshell.api.add_post_listener( "geometry.add_representation", "BlenderBIM.Product.GenerateBox", product.generate_box ) + + ifcopenshell.api.add_post_listener( + "sequence.edit_task_time", "BlenderBIM.Task.CalculateQuantities", task.calculate_quantities + ) + + for usecase in [ + "material.assign_material", + "material.edit_constituent", + "material.edit_layer", + "material.edit_profile", + "material.add_constituent", + "material.add_layer", + "material.add_profile", + ]: + ifcopenshell.api.add_post_listener( + usecase, "BlenderBIM.Product.EnsureMaterialAssigned", product.ensure_material_assigned + ) + ifcopenshell.api.add_post_listener( "material.edit_profile_usage", "BlenderBIM.Product.RegenerateProfileUsage", product.regenerate_profile_usage, ) + IfcStore.add_element_listener(opening.element_listener) IfcStore.add_element_listener(wall.element_listener) + IfcStore.add_element_listener(slab.element_listener) + IfcStore.add_element_listener(profile.element_listener) + ifcopenshell.api.add_pre_listener( "geometry.add_representation", "BlenderBIM.DumbWall.EnsureSolid", wall.ensure_solid ) @@ -34,7 +76,6 @@ def load_post(*args): "type.assign_type", "BlenderBIM.DumbWall.RegenerateFromType", wall.DumbWallPlaner().regenerate_from_type ) - IfcStore.add_element_listener(slab.element_listener) ifcopenshell.api.add_pre_listener( "geometry.add_representation", "BlenderBIM.DumbSlab.EnsureSolid", slab.ensure_solid ) @@ -51,17 +92,21 @@ def load_post(*args): "type.assign_type", "BlenderBIM.DumbSlab.RegenerateFromType", slab.DumbSlabPlaner().regenerate_from_type ) - IfcStore.add_element_listener(column.element_listener) ifcopenshell.api.add_pre_listener( - "geometry.add_representation", "BlenderBIM.DumbColumn.EnsureSolid", column.ensure_solid + "geometry.add_representation", "BlenderBIM.DumbProfile.EnsureSolid", profile.ensure_solid + ) + ifcopenshell.api.add_pre_listener( + "material.edit_profile", + "BlenderBIM.DumbProfile.SyncObjectFromProfile", + profile.DumbProfileRegenerator().sync_object_from_profile, ) ifcopenshell.api.add_post_listener( "material.edit_profile", - "BlenderBIM.DumbColumn.RegenerateFromProfile", - column.DumbColumnRegenerator().regenerate_from_profile, + "BlenderBIM.DumbProfile.RegenerateFromProfile", + profile.DumbProfileRegenerator().regenerate_from_profile, ) ifcopenshell.api.add_post_listener( "type.assign_type", - "BlenderBIM.DumbColumn.RegenerateFromType", - column.DumbColumnRegenerator().regenerate_from_type, + "BlenderBIM.DumbProfile.RegenerateFromType", + profile.DumbProfileRegenerator().regenerate_from_type, ) diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index e2dcdd33c2..c075a8046f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -1,10 +1,202 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import bmesh +import blenderbim.bim.handler +import ifcopenshell +import ifcopenshell.util.representation +from blenderbim.bim.ifc import IfcStore +from math import pi +from mathutils import Vector from bpy.types import Operator from bpy.props import FloatProperty from bpy_extras.object_utils import AddObjectHelper, object_data_add +def element_listener(element, obj): + blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback) + + +def mode_callback(obj, data): + for obj in set(bpy.context.selected_objects + [bpy.context.active_object]): + if ( + obj.mode != "EDIT" + or not obj.data + or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve)) + or not obj.BIMObjectProperties.ifc_definition_id + or not bpy.context.scene.BIMProjectProperties.is_authoring + ): + return + product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + if not product.is_a("IfcOpeningElement"): + return + for rel in product.VoidsElements: + building_element_obj = IfcStore.get_element(rel.RelatingBuildingElement.id()) + if not building_element_obj: + continue + if [m for m in building_element_obj.modifiers if m.type == "BOOLEAN"]: + continue + representation = ifcopenshell.util.representation.get_representation( + rel.RelatingBuildingElement, "Model", "Body", "MODEL_VIEW" + ) + if not representation: + continue + bpy.ops.bim.switch_representation( + obj=building_element_obj.name, + should_switch_all_meshes=True, + should_reload=True, + ifc_definition_id=representation.id(), + disable_opening_subtractions=True, + ) + IfcStore.edited_objs.add(obj) + bm = bmesh.from_edit_mesh(obj.data) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) + bmesh.update_edit_mesh(obj.data) + bm.free() + + +class AddElementOpening(bpy.types.Operator): + bl_idname = "bim.add_element_opening" + bl_label = "Add Element Opening" + bl_options = {"REGISTER", "UNDO"} + voided_building_element: bpy.props.StringProperty() + filling_building_element: bpy.props.StringProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + voided_obj = self.get_voided_building_element(context) + filling_obj = self.get_filling_building_element(context) + + if not voided_obj: + return {"FINISHED"} + + element = IfcStore.get_file().by_id(voided_obj.BIMObjectProperties.ifc_definition_id) + local_location = voided_obj.matrix_world.inverted() @ context.scene.cursor.location + raycast = voided_obj.closest_point_on_mesh(local_location, distance=0.01) + if not raycast[0]: + return {"FINISHED"} + + if filling_obj: + opening = self.generate_opening_from_filling(filling_obj, voided_obj) + else: + # The opening shall be based on the smallest bounding dimension of the element + dimension = min(voided_obj.dimensions) + bpy.ops.mesh.primitive_cube_add(size=dimension * 2) + opening = context.selected_objects[0] + + # Place the opening in the middle of the element + global_location = voided_obj.matrix_world @ raycast[1] + normal = raycast[2] + normal.negate() + global_normal = voided_obj.matrix_world.to_quaternion() @ normal + opening.location = global_location + (global_normal * (dimension / 2)) + opening.rotation_euler = voided_obj.rotation_euler + opening.name = "Opening" + + bpy.ops.bim.add_opening(opening=opening.name, obj=voided_obj.name) + if filling_obj: + bpy.ops.bim.add_filling(opening=opening.name, obj=filling_obj.name) + return {"FINISHED"} + + def get_voided_building_element(self, context): + obj = None + if self.voided_building_element: + obj = bpy.data.objects.get(self.voided_building_element) + else: + total_selected = len(context.selected_objects) + if total_selected == 1 or total_selected == 2: + obj = context.active_object + if obj and obj.BIMObjectProperties.ifc_definition_id: + return obj + + def get_filling_building_element(self, context): + obj = None + if self.filling_building_element: + obj = bpy.data.objects.get(self.filling_building_element) + else: + total_selected = len(context.selected_objects) + if total_selected == 2: + if context.selected_objects[0] == context.active_object: + obj = context.selected_objects[1] + else: + obj = context.selected_objects[0] + if obj and obj.BIMObjectProperties.ifc_definition_id: + return obj + + def generate_opening_from_filling(self, filling_obj, voided_obj): + x, y, z = filling_obj.dimensions + dimension = min(voided_obj.dimensions) + if dimension == voided_obj.dimensions[0]: + verts = [ + Vector((-0.1, 0, 0)), + Vector((-0.1, 0, z)), + Vector((-0.1, y, 0)), + Vector((-0.1, y, z)), + Vector((dimension + 0.1, 0, 0)), + Vector((dimension + 0.1, 0, z)), + Vector((dimension + 0.1, y, 0)), + Vector((dimension + 0.1, y, z)), + ] + elif dimension == voided_obj.dimensions[1]: + verts = [ + Vector((0, -0.1, 0)), + Vector((0, -0.1, z)), + Vector((0, dimension + 0.1, 0)), + Vector((0, dimension + 0.1, z)), + Vector((x, -0.1, 0)), + Vector((x, -0.1, z)), + Vector((x, dimension + 0.1, 0)), + Vector((x, dimension + 0.1, z)), + ] + elif dimension == voided_obj.dimensions[2]: + verts = [ + Vector((0, 0, -0.1)), + Vector((0, 0, dimension + 0.1)), + Vector((0, y, -0.1)), + Vector((0, y, dimension + 0.1)), + Vector((x, 0, -0.1)), + Vector((x, 0, dimension + 0.1)), + Vector((x, y, -0.1)), + Vector((x, y, dimension + 0.1)), + ] + edges = [] + faces = [ + [0, 1, 3, 2], + [2, 3, 7, 6], + [6, 7, 5, 4], + [4, 5, 1, 0], + [2, 6, 4, 0], + [7, 3, 1, 5], + ] + mesh = bpy.data.meshes.new(name="Opening") + mesh.from_pydata(verts, edges, faces) + obj = bpy.data.objects.new("Opening", mesh) + obj.matrix_world = filling_obj.matrix_world + + filling_obj.rotation_euler = voided_obj.rotation_euler + obj.parent = filling_obj + obj.matrix_parent_inverse = filling_obj.matrix_world.inverted() + return obj + + def add_object(self, context): bm = bmesh.new() bmesh.ops.create_cube(bm, size=self.size) diff --git a/src/blenderbim/blenderbim/bim/module/model/pie.py b/src/blenderbim/blenderbim/bim/module/model/pie.py index 10188e2b18..2d4195eb1c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/pie.py +++ b/src/blenderbim/blenderbim/bim/module/model/pie.py @@ -1,4 +1,23 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy +from blenderbim.bim.ifc import IfcStore class OpenPieClass(bpy.types.Operator): @@ -10,74 +29,15 @@ class OpenPieClass(bpy.types.Operator): return {"FINISHED"} -class AssignIfcWall(bpy.types.Operator): - bl_idname = "bim.assign_ifc_wall" - bl_label = "IfcWall" - - def execute(self, context): - bpy.ops.bim.assign_class(ifc_class="IfcWall") - return {"FINISHED"} - - -class AssignIfcSlab(bpy.types.Operator): - bl_idname = "bim.assign_ifc_slab" - bl_label = "IfcSlab" - - def execute(self, context): - bpy.ops.bim.assign_class(ifc_class="IfcSlab") - return {"FINISHED"} - - -class AssignIfcStair(bpy.types.Operator): - bl_idname = "bim.assign_ifc_stair" - bl_label = "IfcStair" - - def execute(self, context): - bpy.ops.bim.assign_class(ifc_class="IfcStair") - return {"FINISHED"} - - -class AssignIfcDoor(bpy.types.Operator): - bl_idname = "bim.assign_ifc_door" - bl_label = "IfcDoor" - - def execute(self, context): - bpy.ops.bim.assign_class(ifc_class="IfcDoor") - return {"FINISHED"} - - -class AssignIfcWindow(bpy.types.Operator): - bl_idname = "bim.assign_ifc_window" - bl_label = "IfcWindow" - - def execute(self, context): - bpy.ops.bim.assign_class(ifc_class="IfcWindow") - return {"FINISHED"} - - -class AssignIfcColumn(bpy.types.Operator): - bl_idname = "bim.assign_ifc_column" - bl_label = "IfcColumn" - - def execute(self, context): - bpy.ops.bim.assign_class(ifc_class="IfcColumn") - return {"FINISHED"} - - -class AssignIfcBeam(bpy.types.Operator): - bl_idname = "bim.assign_ifc_beam" - bl_label = "IfcBeam" - - def execute(self, context): - bpy.ops.bim.assign_class(ifc_class="IfcBeam") - return {"FINISHED"} - - class PieAddOpening(bpy.types.Operator): bl_idname = "bim.pie_add_opening" bl_label = "Add Opening" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): if len(context.selected_objects) == 2: opening_name = None obj_name = None @@ -87,16 +47,20 @@ class PieAddOpening(bpy.types.Operator): elif len(obj.children) == 1 and not obj.children[0].BIMObjectProperties.ifc_definition_id: opening_name = obj.children[0].name else: - opj_name = obj.name - bpy.ops.bim.add_opening(obj=opj_name, opening=opening_name) + obj_name = obj.name + bpy.ops.bim.add_opening(obj=obj_name, opening=opening_name) return {"FINISHED"} class PieUpdateContainer(bpy.types.Operator): bl_idname = "bim.pie_update_container" bl_label = "Update Spatial Container" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): for obj in context.selected_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue @@ -127,10 +91,10 @@ class VIEW3D_MT_PIE_bim_class(bpy.types.Menu): def draw(self, context): pie = self.layout.menu_pie() - pie.operator("bim.assign_ifc_wall") - pie.operator("bim.assign_ifc_slab") - pie.operator("bim.assign_ifc_stair") - pie.operator("bim.assign_ifc_door") - pie.operator("bim.assign_ifc_window") - pie.operator("bim.assign_ifc_column") - pie.operator("bim.assign_ifc_beam") + pie.operator("bim.assign_class", text="IfcWall").ifc_class = "IfcWall" + pie.operator("bim.assign_class", text="IfcSlab").ifc_class = "IfcSlab" + pie.operator("bim.assign_class", text="IfcStair").ifc_class = "IfcStair" + pie.operator("bim.assign_class", text="IfcDoor").ifc_class = "IfcDoor" + pie.operator("bim.assign_class", text="IfcWindow").ifc_class = "IfcWindow" + pie.operator("bim.assign_class", text="IfcColumn").ifc_class = "IfcColumn" + pie.operator("bim.assign_class", text="IfcBeam").ifc_class = "IfcBeam" diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index d25d62134e..fc7ed03907 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -1,12 +1,58 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy +import mathutils import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element import ifcopenshell.util.representation -from . import wall, slab, column +from . import wall, slab, profile from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.pset.data import Data as PsetData -from mathutils import Vector +from mathutils import Vector, Matrix +from bpy_extras.object_utils import AddObjectHelper + + +class AddEmptyType(bpy.types.Operator, AddObjectHelper): + bl_idname = "bim.add_empty_type" + bl_label = "Add Empty Type" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + obj = bpy.data.objects.new("TYPEX", None) + for project in [c for c in context.view_layer.layer_collection.children if "IfcProject" in c.name]: + if not [c for c in project.children if "Types" in c.name]: + types = bpy.data.collections.new("Types") + project.collection.children.link(types) + for collection in [c for c in project.children if "Types" in c.name]: + collection.collection.objects.link(obj) + break + break + context.scene.BIMRootProperties.ifc_product = "IfcElementType" + bpy.ops.object.select_all(action="DESELECT") + context.view_layer.objects.active = obj + obj.select_set(True) + return {"FINISHED"} + + +def add_empty_type_button(self, context): + self.layout.operator(AddEmptyType.bl_idname, icon="FILE_3D") class AddTypeInstance(bpy.types.Operator): @@ -17,25 +63,32 @@ class AddTypeInstance(bpy.types.Operator): relating_type: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): tprops = context.scene.BIMTypeProperties ifc_class = self.ifc_class or tprops.ifc_class - relating_type = self.relating_type or tprops.relating_type - if not ifc_class or not relating_type: + relating_type_id = self.relating_type or tprops.relating_type + + if not ifc_class or not relating_type_id: return {"FINISHED"} + self.file = IfcStore.get_file() instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema)[0] - if ifc_class == "IfcWallType": - obj = wall.DumbWallGenerator(self.file.by_id(int(relating_type))).generate() - if obj: + relating_type = self.file.by_id(int(relating_type_id)) + material = ifcopenshell.util.element.get_material(relating_type) + + if material and material.is_a("IfcMaterialProfileSet"): + if profile.DumbProfileGenerator(relating_type).generate(): return {"FINISHED"} - elif ifc_class == "IfcSlabType": - obj = slab.DumbSlabGenerator(self.file.by_id(int(relating_type))).generate() - if obj: - return {"FINISHED"} - elif ifc_class == "IfcColumnType": - obj = column.DumbColumnGenerator(self.file.by_id(int(relating_type))).generate() - if obj: + elif material and material.is_a("IfcMaterialLayerSet"): + if self.generate_layered_element(ifc_class, relating_type): return {"FINISHED"} + + building_obj = None + if len(context.selected_objects) == 1 and context.active_object: + building_obj = context.active_object + # A cube verts = [ Vector((-1, -1, -1)), @@ -49,24 +102,142 @@ class AddTypeInstance(bpy.types.Operator): ] edges = [] faces = [ - [0, 2, 3, 1], + [0, 1, 3, 2], [2, 3, 7, 6], - [4, 5, 7, 6], - [0, 1, 5, 4], - [1, 3, 7, 5], - [0, 2, 6, 4], + [6, 7, 5, 4], + [4, 5, 1, 0], + [2, 6, 4, 0], + [7, 3, 1, 5], ] mesh = bpy.data.meshes.new(name="Instance") mesh.from_pydata(verts, edges, faces) obj = bpy.data.objects.new("Instance", mesh) obj.location = context.scene.cursor.location - collection = bpy.context.view_layer.active_layer_collection.collection + collection = context.view_layer.active_layer_collection.collection collection.objects.link(obj) collection_obj = bpy.data.objects.get(collection.name) bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class) bpy.ops.bim.assign_type(relating_type=int(tprops.relating_type), related_object=obj.name) - if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id: - obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box]) + + if building_obj: + if instance_class in ["IfcWindow", "IfcDoor"]: + # TODO For now we are hardcoding windows and doors as a prototype + bpy.ops.bim.add_element_opening( + voided_building_element=building_obj.name, filling_building_element=obj.name + ) + if instance_class == "IfcDoor": + obj.location[2] = building_obj.location[2] - min([v[2] for v in obj.bound_box]) + else: + if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id: + obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box]) + + bpy.ops.object.select_all(action="DESELECT") + obj.select_set(True) + context.view_layer.objects.active = obj + return {"FINISHED"} + + def generate_layered_element(self, ifc_class, relating_type): + layer_set_direction = None + + parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric") + if parametric: + layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction) + if layer_set_direction is None: + if ifc_class in ["IfcSlabType", "IfcRoofType", "IfcRampType", "IfcPlateType"]: + layer_set_direction = "AXIS3" + else: + layer_set_direction = "AXIS2" + + if layer_set_direction == "AXIS3": + if slab.DumbSlabGenerator(relating_type).generate(): + return True + elif layer_set_direction == "AXIS2": + if wall.DumbWallGenerator(relating_type).generate(): + return True + else: + pass # Dumb block generator? Eh? :) + + +class AlignProduct(bpy.types.Operator): + bl_idname = "bim.align_product" + bl_label = "Align Product" + bl_options = {"REGISTER", "UNDO"} + align_type: bpy.props.StringProperty() + + def execute(self, context): + selected_objs = context.selected_objects + if len(selected_objs) < 2 or not context.active_object: + return {"FINISHED"} + if self.align_type == "CENTERLINE": + point = context.active_object.matrix_world @ ( + Vector(context.active_object.bound_box[0]) + (context.active_object.dimensions / 2) + ) + elif self.align_type == "POSITIVE": + point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[6]) + elif self.align_type == "NEGATIVE": + point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[0]) + + active_x_axis = context.active_object.matrix_world.to_quaternion() @ Vector((1, 0, 0)) + active_y_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 1, 0)) + active_z_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 0, 1)) + + x_distances = self.get_axis_distances(point, active_x_axis, context) + y_distances = self.get_axis_distances(point, active_y_axis, context) + if abs(sum(x_distances)) < abs(sum(y_distances)): + for i, obj in enumerate(selected_objs): + obj.matrix_world = Matrix.Translation(active_x_axis * -x_distances[i]) @ obj.matrix_world + else: + for i, obj in enumerate(selected_objs): + obj.matrix_world = Matrix.Translation(active_y_axis * -y_distances[i]) @ obj.matrix_world + return {"FINISHED"} + + def get_axis_distances(self, point, axis, context): + results = [] + for obj in context.selected_objects: + if self.align_type == "CENTERLINE": + obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2)) + elif self.align_type == "POSITIVE": + obj_point = obj.matrix_world @ Vector(obj.bound_box[6]) + elif self.align_type == "NEGATIVE": + obj_point = obj.matrix_world @ Vector(obj.bound_box[0]) + results.append(mathutils.geometry.distance_point_to_plane(obj_point, point, axis)) + return results + + +class DynamicallyVoidProduct(bpy.types.Operator): + bl_idname = "bim.dynamically_void_product" + bl_label = "Dynamically Void Product" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) + if obj is None: + return {"FINISHED"} + product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + if not product.HasOpenings: + return {"FINISHED"} + if [m for m in obj.modifiers if m.type == "BOOLEAN"]: + return {"FINISHED"} + representation = ifcopenshell.util.representation.get_representation(product, "Model", "Body", "MODEL_VIEW") + if not representation: + return {"FINISHED"} + was_edit_mode = obj.mode == "EDIT" + if was_edit_mode: + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.bim.switch_representation( + obj=obj.name, + should_switch_all_meshes=True, + should_reload=True, + ifc_definition_id=representation.id(), + disable_opening_subtractions=True, + ) + if was_edit_mode: + bpy.ops.object.mode_set(mode="EDIT") return {"FINISHED"} @@ -95,6 +266,7 @@ def generate_box(usecase_path, ifc_file, settings): **{"product": product, "representation": new_box} ) + def regenerate_profile_usage(usecase_path, ifc_file, settings): elements = [] if ifc_file.schema == "IFC2X3": @@ -114,4 +286,39 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings): continue representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if representation: - bpy.ops.bim.switch_representation(obj=obj.name, ifc_definition_id=representation.id(), should_reload=True) + bpy.ops.bim.switch_representation( + obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True + ) + + +def ensure_material_assigned(usecase_path, ifc_file, settings): + if usecase_path == "material.assign_material": + if not settings.get("Material", None): + return + elements = [self.settings["product"]] + else: + elements = [] + for rel in ifc_file.by_type("IfcRelAssociatesMaterial"): + if rel.RelatingMaterial == settings["material"] or [ + e for e in ifc_file.traverse(rel.RelatingMaterial) if e == settings["material"] + ]: + elements.extend(rel.RelatedObjects) + + for element in elements: + obj = IfcStore.get_element(element.GlobalId) + if not obj or not obj.data: + continue + + element_material = ifcopenshell.util.element.get_material(element) + material = [m for m in ifc_file.traverse(element_material) if m.is_a("IfcMaterial")] + + object_material_ids = [ + om.BIMObjectProperties.ifc_definition_id + for om in obj.data.materials + if om is not None and om.BIMObjectProperties.ifc_definition_id + ] + + if material[0].id() in object_material_ids: + continue + + obj.data.materials.append(IfcStore.get_element(material[0].id())) diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py new file mode 100644 index 0000000000..cfa7ed47ce --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -0,0 +1,377 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import bmesh +import math +import ifcopenshell +import ifcopenshell.util.type +import ifcopenshell.util.unit +import ifcopenshell.util.element +import mathutils.geometry +import blenderbim.bim.handler +from blenderbim.bim.ifc import IfcStore +from math import pi, degrees, inf +from mathutils import Vector, Matrix +from ifcopenshell.api.pset.data import Data as PsetData +from ifcopenshell.api.material.data import Data as MaterialData +from blenderbim.bim.module.geometry.helper import Helper + + +def element_listener(element, obj): + blenderbim.bim.handler.subscribe_to(obj, "mode", mode_callback) + + +def mode_callback(obj, data): + for obj in set(bpy.context.selected_objects + [bpy.context.active_object]): + if ( + not obj.data + or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve)) + or not obj.BIMObjectProperties.ifc_definition_id + or not bpy.context.scene.BIMProjectProperties.is_authoring + ): + return + product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") + if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile": + return + if obj.mode == "EDIT": + IfcStore.edited_objs.add(obj) + bm = bmesh.from_edit_mesh(obj.data) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) + bmesh.update_edit_mesh(obj.data) + bm.free() + else: + material_usage = ifcopenshell.util.element.get_material(product) + x, y = obj.dimensions[0:2] + if not material_usage.CardinalPoint: + new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2)) + elif material_usage.CardinalPoint == 1: + new_origin = obj.matrix_world @ Vector(obj.bound_box[4]) + elif material_usage.CardinalPoint == 2: + new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, 0, 0)) / 2)) + elif material_usage.CardinalPoint == 3: + new_origin = obj.matrix_world @ Vector(obj.bound_box[0]) + elif material_usage.CardinalPoint == 4: + new_origin = obj.matrix_world @ (Vector(obj.bound_box[4]) + (Vector((0, y, 0)) / 2)) + elif material_usage.CardinalPoint == 5: + new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((x, y, 0)) / 2)) + elif material_usage.CardinalPoint == 6: + new_origin = obj.matrix_world @ (Vector(obj.bound_box[0]) + (Vector((0, y, 0)) / 2)) + elif material_usage.CardinalPoint == 7: + new_origin = obj.matrix_world @ Vector(obj.bound_box[7]) + elif material_usage.CardinalPoint == 8: + new_origin = obj.matrix_world @ (Vector(obj.bound_box[3]) + (Vector((x, 0, 0)) / 2)) + elif material_usage.CardinalPoint == 9: + new_origin = obj.matrix_world @ Vector(obj.bound_box[3]) + if (obj.matrix_world.translation - new_origin).length < 0.001: + return + obj.data.transform( + Matrix.Translation( + (obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin)) + ) + ) + obj.matrix_world.translation = new_origin + + +def ensure_solid(usecase_path, ifc_file, settings): + product = ifc_file.by_id(settings["blender_object"].BIMObjectProperties.ifc_definition_id) + parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") + if not parametric or parametric["Engine"] != "BlenderBIM.DumbProfile": + return + material = ifcopenshell.util.element.get_material(product) + if material and material.is_a("IfcMaterialProfileSetUsage"): + settings["profile_set_usage"] = material + else: + return + settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage" + + +class DumbProfileGenerator: + def __init__(self, relating_type): + self.relating_type = relating_type + + def generate(self): + self.file = IfcStore.get_file() + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) + material = ifcopenshell.util.element.get_material(self.relating_type) + if material and material.is_a("IfcMaterialProfileSet"): + self.profile_set = material + else: + return + + self.collection = bpy.context.view_layer.active_layer_collection.collection + self.collection_obj = bpy.data.objects.get(self.collection.name) + self.length = 3 + self.rotation = 0 + self.location = Vector((0, 0, 0)) + return self.derive_from_cursor() + + def derive_from_cursor(self): + self.location = bpy.context.scene.cursor.location + return self.create_profile() + + def create_profile(self): + # A cube + verts = [ + Vector((-1, -1, -1)), + Vector((-1, -1, 1)), + Vector((-1, 1, -1)), + Vector((-1, 1, 1)), + Vector((1, -1, -1)), + Vector((1, -1, 1)), + Vector((1, 1, -1)), + Vector((1, 1, 1)), + ] + edges = [] + faces = [ + [0, 2, 3, 1], + [2, 3, 7, 6], + [4, 5, 7, 6], + [0, 1, 5, 4], + [1, 3, 7, 5], + [0, 2, 6, 4], + ] + + mesh = bpy.data.meshes.new(name="Dumb Profile") + mesh.from_pydata(verts, edges, faces) + obj = bpy.data.objects.new("Profile", mesh) + obj.location = self.location + if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id: + obj.location[2] = self.collection_obj.location[2] + self.collection.objects.link(obj) + + ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema) + # Standard cases are deprecated, so let's cull them + ifc_class = [c for c in ifc_classes if "StandardCase" not in c][0] + obj.name = ifc_class[3:] + bpy.ops.bim.assign_class(obj=obj.name, ifc_class=ifc_class, should_add_representation=False) + + if self.relating_type.is_a() in ["IfcBeamType", "IfcMemberType"]: + obj.rotation_euler[0] = math.pi / 2 + obj.rotation_euler[2] = math.pi / 2 + + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name) + profile_set_usage = ifcopenshell.util.element.get_material(element) + bpy.ops.bim.add_representation( + obj=obj.name, + context_id=ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW").id(), + ifc_representation_class="IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage", + profile_set_usage=profile_set_usage.id(), + ) + representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + bpy.ops.bim.switch_representation( + obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True + ) + pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"}) + MaterialData.load(self.file) + obj.select_set(True) + return obj + + +class DumbProfileRegenerator: + def regenerate_from_profile(self, usecase_path, ifc_file, settings): + self.file = ifc_file + profile = settings["profile"].Profile + if not profile: + return + for element in self.get_elements_using_profile(profile): + self.change_profile(element) + + def sync_object_from_profile(self, usecase_path, ifc_file, settings): + self.file = ifc_file + profile = settings["profile"].Profile + if not profile: + return + for element in self.get_elements_using_profile(profile): + self.sync_object(element) + + def get_elements_using_profile(self, profile): + results = [] + for profile_set in [ + mp.ToMaterialProfileSet[0] for mp in self.file.get_inverse(profile) if mp.is_a("IfcMaterialProfile") + ]: + for inverse in self.file.get_inverse(profile_set): + if not inverse.is_a("IfcMaterialProfileSetUsage"): + continue + if self.file.schema == "IFC2X3": + for rel in self.file.get_inverse(inverse): + if not rel.is_a("IfcRelAssociatesMaterial"): + continue + results.extend(rel.RelatedObjects) + else: + for rel in inverse.AssociatedTo: + results.extend(rel.RelatedObjects) + return results + + def regenerate_from_type(self, usecase_path, ifc_file, settings): + new_material = ifcopenshell.util.element.get_material(settings["relating_type"]) + if not new_material or not new_material.is_a("IfcMaterialProfileSet"): + return + self.change_profile(settings["related_object"]) + + def change_profile(self, element): + obj = IfcStore.get_element(element.id()) + if not obj: + return + representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + if representation: + bpy.ops.bim.switch_representation( + obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True + ) + + def sync_object(self, element): + obj = IfcStore.get_element(element.id()) + if not obj or obj not in IfcStore.edited_objs: + return + bpy.ops.bim.update_representation(obj=obj.name) + + +class ExtendProfile(bpy.types.Operator): + bl_idname = "bim.extend_profile" + bl_label = "Extend Profile" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + selected_objs = context.selected_objects + for obj in selected_objs: + bpy.ops.bim.dynamically_void_product(obj=obj.name) + if len(selected_objs) == 0: + return {"FINISHED"} + if not context.active_object: + return {"FINISHED"} + if len(selected_objs) == 1: + DumbProfileExtender(context.active_object, target_coordinate=context.scene.cursor.location).extend() + IfcStore.edited_objs.add(context.active_object) + return {"FINISHED"} + if len(selected_objs) < 2: + return {"FINISHED"} + for obj in selected_objs: + if obj == context.active_object: + continue + DumbProfileExtender(obj, context.active_object).extend() + IfcStore.edited_objs.add(obj) + return {"FINISHED"} + + +class DumbProfileExtender: + # A profile is a prismatic extrusion along its local Z axis. + def __init__(self, profile, target=None, target_coordinate=None): + self.profile = profile + self.target = target + self.target_coordinate = target_coordinate + + # An extension of a profile is determined by casting a ray from the cardinal + # point of either extreme along the profiles local Z axis to a target + # object. The nearest result from this raycast will be the target extension + # point. + def extend(self): + extension_data = self.get_closest_extension_point() + if not extension_data: + return + if extension_data["end"] == "bottom": + self.profile.matrix_world.translation = extension_data["contact"] + if extension_data["direction"] == "up": + self.shift_top_faces(-extension_data["distance"]) + elif extension_data["direction"] == "down": + self.shift_top_faces(extension_data["distance"]) + if extension_data["end"] == "top": + if extension_data["direction"] == "up": + self.shift_top_faces(extension_data["distance"]) + elif extension_data["direction"] == "down": + self.shift_top_faces(-extension_data["distance"]) + + def shift_top_faces(self, z): + vertices = [] + for f in self.get_profile_top_faces(): + vertices.extend(f.vertices) + for v in set(vertices): + self.profile.data.vertices[v].co.z += z + + # An top face is defined as having at least one vertex on the max + # Z-axis, and a non-insignificant Z component of its face normal + def get_profile_top_faces(self): + faces = [] + max_z = max([v[2] for v in self.profile.bound_box]) + for f in self.profile.data.polygons: + if abs(f.normal.z) < 0.1: + continue + for v in f.vertices: + if self.profile.data.vertices[v].co.z == max_z: + faces.append(f) + break + return faces + + def get_closest_extension_point(self): + top = self.profile.matrix_world @ Vector((0, 0, self.profile.dimensions[2])) + bottom = self.profile.matrix_world.translation + up = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1)) + down = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, -1)) + + if self.target: + return self.get_closest_extension_point_from_target_obj(top, bottom, up, down) + elif self.target_coordinate: + return self.get_closest_extension_point_from_target_coordinate(top, bottom, up, down) + + def get_closest_extension_point_from_target_coordinate(self, top, bottom, up, down): + point = mathutils.geometry.intersect_line_plane( + bottom, top, self.target_coordinate, self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1)) + ) + if not point: + return + top_distance = (point - top).length + bottom_distance = (point - bottom).length + if top_distance < bottom_distance: + intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, top, bottom) + if signed_distance > 0: + return {"contact": point, "distance": top_distance, "end": "top", "direction": "down"} + else: + return {"contact": point, "distance": top_distance, "end": "top", "direction": "up"} + else: + intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, bottom, top) + if signed_distance > 0: + return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "up"} + else: + return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "down"} + + def get_closest_extension_point_from_target_obj(self, top, bottom, up, down): + t_top = self.target.matrix_world.inverted() @ top + t_bottom = self.target.matrix_world.inverted() @ bottom + t_up = self.target.matrix_world.inverted().to_quaternion() @ up + t_down = self.target.matrix_world.inverted().to_quaternion() @ down + + results = [] + results.append((self.target.ray_cast(t_top, t_up, distance=50), top, "top", "up")) + results.append((self.target.ray_cast(t_top, t_down, distance=50), top, "top", "down")) + results.append((self.target.ray_cast(t_bottom, t_up, distance=50), bottom, "bottom", "up")) + results.append((self.target.ray_cast(t_bottom, t_down, distance=50), bottom, "bottom", "down")) + + min_distance = float(inf) + final = None + + for result in results: + if result[0][0]: + contact = self.target.matrix_world @ result[0][1] + distance = (contact - result[1]).length + if distance < min_distance: + min_distance = distance + final = {"contact": contact, "distance": distance, "end": result[2], "direction": result[3]} + + return final diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index bc7bf5d5f7..1d5f985fb8 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.util.type from blenderbim.bim.prop import StrProperty, Attribute @@ -21,6 +39,7 @@ def purge(): global relating_types_enum relating_types_enum = [] + def getRelatingTypes(self, context): global relating_types_enum if len(relating_types_enum) < 1: @@ -28,5 +47,6 @@ def getRelatingTypes(self, context): relating_types_enum.extend((str(e.id()), e.Name, "") for e in elements) return relating_types_enum + class BIMModelProperties(PropertyGroup): relating_type: EnumProperty(items=getRelatingTypes, name="Relating Type") diff --git a/src/blenderbim/blenderbim/bim/module/model/root.py b/src/blenderbim/blenderbim/bim/module/model/root.py new file mode 100644 index 0000000000..65c72152f1 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/root.py @@ -0,0 +1,40 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.style.data import Data as StyleData + + +def sync_name(usecase_path, ifc_file, settings): + if "Name" not in settings["attributes"]: + return + obj = IfcStore.get_element(settings["product"].id()) + if not obj: + return + if isinstance(obj, bpy.types.Object): + new_name = "{}/{}".format(settings["product"].is_a(), settings["attributes"]["Name"] or "Unnamed") + elif isinstance(obj, bpy.types.Material): + new_name = settings["attributes"]["Name"] or "Unnamed" + if obj.BIMMaterialProperties.ifc_style_id: + IfcStore.get_file().by_id(obj.BIMMaterialProperties.ifc_style_id).Name = new_name + StyleData.load(IfcStore.get_file(), obj.BIMMaterialProperties.ifc_style_id) + collection = bpy.data.collections.get(obj.name) + if collection: + collection.name = new_name + obj.name = new_name diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index 26b47def02..c9847d1a21 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import bmesh import math @@ -20,10 +38,9 @@ def element_listener(element, obj): def mode_callback(obj, data): - for obj in bpy.context.selected_objects + [bpy.context.active_object]: + for obj in set(bpy.context.selected_objects + [bpy.context.active_object]): if ( - obj.mode != "EDIT" - or not obj.data + not obj.data or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve)) or not obj.BIMObjectProperties.ifc_definition_id or not bpy.context.scene.BIMProjectProperties.is_authoring @@ -31,35 +48,60 @@ def mode_callback(obj, data): return product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer3": return - IfcStore.edited_objs.add(obj) - modifier = [m for m in obj.modifiers if m.type == "SOLIDIFY"] - if modifier: - return - depth = obj.dimensions.z + if obj.mode == "EDIT": + IfcStore.edited_objs.add(obj) + ensure_solidify_modifier(obj) + else: + new_origin = obj.matrix_world @ Vector(obj.bound_box[0]) + obj.data.transform( + Matrix.Translation( + (obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin)) + ) + ) + obj.matrix_world.translation = new_origin + + +def ensure_solidify_modifier(obj): + modifier = [m for m in obj.modifiers if m.type == "SOLIDIFY"] + if modifier: + return modifier[0] + depth = obj.dimensions.z + + if obj.mode == "EDIT": bm = bmesh.from_edit_mesh(obj.data) - bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) - bm.faces.ensure_lookup_table() - non_bottom_faces = [] - for face in bm.faces: - if face.normal.z > -0.9: - non_bottom_faces.append(face) - else: - face.normal_flip() - bmesh.ops.delete(bm, geom=non_bottom_faces, context="FACES") + else: + bm = bmesh.new() + bm.from_mesh(obj.data) + + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) + bm.faces.ensure_lookup_table() + non_bottom_faces = [] + for face in bm.faces: + if face.normal.z > -0.9: + non_bottom_faces.append(face) + else: + face.normal_flip() + bmesh.ops.delete(bm, geom=non_bottom_faces, context="FACES") + + if obj.mode == "EDIT": bmesh.update_edit_mesh(obj.data) - bm.free() - modifier = obj.modifiers.new("Slab Depth", "SOLIDIFY") - modifier.use_even_offset = True - modifier.offset = 1 - modifier.thickness = depth + else: + bm.to_mesh(obj.data) + + bm.free() + modifier = obj.modifiers.new("Slab Depth", "SOLIDIFY") + modifier.use_even_offset = True + modifier.offset = 1 + modifier.thickness = depth + return modifier def ensure_solid(usecase_path, ifc_file, settings): product = ifc_file.by_id(settings["blender_object"].BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer3": return settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" @@ -71,7 +113,7 @@ def generate_footprint(usecase_path, ifc_file, settings): obj = settings["blender_object"] product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer3": return old_footprint = ifcopenshell.util.representation.get_representation(product, "Plan", "FootPrint", "SKETCH_VIEW") if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body": @@ -89,7 +131,7 @@ def generate_footprint(usecase_path, ifc_file, settings): profile_edges = [] def append_profile_edges(profile_edges, indices): - indices.append(indices[0]) # Close the loop + indices.append(indices[0]) # Close the loop edge_vert_pairs = list(zip(indices, indices[1:])) for p in edge_vert_pairs: profile_edges.append( @@ -127,7 +169,7 @@ def calculate_quantities(usecase_path, ifc_file, settings): obj = settings["blender_object"] product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer3": return qto = ifcopenshell.api.run( "pset.add_qto", ifc_file, should_run_listeners=False, product=product, name="Qto_SlabBaseQuantities" @@ -145,7 +187,7 @@ def calculate_quantities(usecase_path, ifc_file, settings): bm.verts.ensure_lookup_table() def calculate_profile_length(indices): - indices.append(indices[0]) # Close the loop + indices.append(indices[0]) # Close the loop edge_vert_pairs = list(zip(indices, indices[1:])) return sum([(bm.verts[p[1]].co - bm.verts[p[0]].co).length for p in edge_vert_pairs]) @@ -171,7 +213,7 @@ def calculate_quantities(usecase_path, ifc_file, settings): net_volume = gross_volume bm.free() - properties={ + properties = { "Depth": round(depth, 2), "Perimeter": round(perimeter, 2), "GrossArea": round(gross_area, 2), @@ -181,17 +223,21 @@ def calculate_quantities(usecase_path, ifc_file, settings): } if round(obj.dimensions[0] * obj.dimensions[1] * obj.dimensions[2], 2) == round(gross_volume, 2): - properties.update({ - "Length": round(length, 2), - "Width": round(width, 2), - }) + properties.update( + { + "Length": round(length, 2), + "Width": round(width, 2), + } + ) else: - properties.update({ - "Length": None, - "Width": None, - }) + properties.update( + { + "Length": None, + "Width": None, + } + ) - ifcopenshell.api.run( "pset.edit_qto", ifc_file, should_run_listeners=False, qto=qto, properties=properties) + ifcopenshell.api.run("pset.edit_qto", ifc_file, should_run_listeners=False, qto=qto, properties=properties) PsetData.load(ifc_file, obj.BIMObjectProperties.ifc_definition_id) @@ -209,7 +255,7 @@ class DumbSlabGenerator: if material.is_a("IfcMaterialLayerSet"): thicknesses = [l.LayerThickness for l in material.MaterialLayers] break - if not thicknesses: + if not sum(thicknesses): return self.collection = bpy.context.view_layer.active_layer_collection.collection @@ -242,7 +288,12 @@ class DumbSlabGenerator: modifier.use_even_offset = True modifier.offset = 1 modifier.thickness = self.depth - obj.name = "Slab" + + ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema) + # Standard cases are deprecated, so let's cull them + ifc_class = [c for c in ifc_classes if "StandardCase" not in c][0] + + obj.name = ifc_class[3:] obj.location = self.location if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id: obj.location[2] = self.collection_obj.location[2] - self.depth @@ -251,14 +302,13 @@ class DumbSlabGenerator: self.collection.objects.link(obj) bpy.ops.bim.assign_class( obj=obj.name, - ifc_class="IfcSlab", - predefined_type="FLOOR", + ifc_class=ifc_class, ifc_representation_class="IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids", ) bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name) element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") - ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbSlab"}) + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbLayer3"}) MaterialData.load(self.file) obj.select_set(True) return obj @@ -299,7 +349,7 @@ class DumbSlabPlaner: def change_thickness(self, element, thickness): parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbSlab": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer3": return obj = IfcStore.get_element(element.id()) @@ -310,10 +360,6 @@ class DumbSlabPlaner: if round(delta_thickness, 2) == 0: return - modifier = [m for m in obj.modifiers if m.type == "SOLIDIFY"] - if modifier: - modifier = modifier[0] - else: - pass + modifier = ensure_solidify_modifier(obj) modifier.thickness += delta_thickness obj.location[2] -= delta_thickness diff --git a/src/blenderbim/blenderbim/bim/module/model/stair.py b/src/blenderbim/blenderbim/bim/module/model/stair.py index b0074f34c2..3b06c52334 100644 --- a/src/blenderbim/blenderbim/bim/module/model/stair.py +++ b/src/blenderbim/blenderbim/bim/module/model/stair.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Operator from bpy.props import FloatProperty, IntProperty diff --git a/src/blenderbim/blenderbim/bim/module/model/task.py b/src/blenderbim/blenderbim/bim/module/model/task.py new file mode 100644 index 0000000000..3bc4ed748c --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/model/task.py @@ -0,0 +1,46 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.date +from ifcopenshell.api.pset.data import Data as PsetData + + +def calculate_quantities(usecase_path, ifc_file, settings): + if not set(["ScheduleStart", "ScheduleFinish", "ScheduleDuration"]).intersection( + set(settings["attributes"].keys()) + ): + return + element = settings["task_time"] + if not element.ScheduleDuration: + return + task = [e for e in ifc_file.get_inverse(element) if e.is_a("IfcTask")][0] + qto = ifcopenshell.api.run( + "pset.add_qto", ifc_file, should_run_listeners=False, product=task, name="Qto_TaskBaseQuantities" + ) + ifcopenshell.api.run( + "pset.edit_qto", + ifc_file, + should_run_listeners=False, + qto=qto, + properties={ + "StandardWork": ifcopenshell.util.date.ifc2datetime(element.ScheduleDuration).days, + }, + ) + PsetData.load(ifc_file, task.id()) diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 0eae3f114e..68891e71e6 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -1,3 +1,22 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.bim.module.type.prop as type_prop from bpy.types import Panel from blenderbim.bim.ifc import IfcStore @@ -9,12 +28,28 @@ class BIM_PT_authoring(Panel): bl_region_type = "UI" bl_category = "BlenderBIM" + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def draw(self, context): tprops = context.scene.BIMTypeProperties col = self.layout.column(align=True) - col.prop(tprops, "ifc_class", text="", icon="FILE_VOLUME") - col.prop(tprops, "relating_type", text="", icon="FILE_3D") - col.operator("bim.add_type_instance", icon="ADD") + enabled = True + + if type_prop.getIfcTypes(tprops, context): + col.prop(tprops, "ifc_class", text="", icon="FILE_VOLUME") + else: + col.label(text="No IFC Class", icon="FILE_VOLUME") + enabled = False + if type_prop.getAvailableTypes(tprops, context): + col.prop(tprops, "relating_type", text="", icon="FILE_3D") + else: + col.label(text="No Relating Type", icon="FILE_3D") + enabled = False + row = col.row() + row.operator("bim.add_type_instance", icon="ADD") + row.enabled = enabled class BIM_PT_authoring_architectural(Panel): @@ -53,9 +88,8 @@ class BIM_PT_misc_utilities(Panel): layout = self.layout props = context.scene.BIMProperties - row = layout.row() + row = layout.split(factor=0.2, align=True) row.prop(props, "override_colour", text="") - row = layout.row(align=True) row.operator("bim.set_override_colour") row = layout.row(align=True) row.operator("bim.set_viewport_shadow_from_sun") diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index 893e98363c..9e3dc5c82d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import math import bmesh @@ -20,10 +38,9 @@ def element_listener(element, obj): def mode_callback(obj, data): - for obj in bpy.context.selected_objects + [bpy.context.active_object]: + for obj in set(bpy.context.selected_objects + [bpy.context.active_object]): if ( - obj.mode != "EDIT" - or not obj.data + not obj.data or not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve, bpy.types.TextCurve)) or not obj.BIMObjectProperties.ifc_definition_id or not bpy.context.scene.BIMProjectProperties.is_authoring @@ -31,21 +48,23 @@ def mode_callback(obj, data): return product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2": return - IfcStore.edited_objs.add(obj) - - -class AddWall(bpy.types.Operator): - bl_idname = "bim.add_wall" - bl_label = "Add Wall" - bl_options = {"REGISTER", "UNDO"} - join_type: bpy.props.StringProperty() - - def execute(self, context): - props = context.scene.BIMModelProperties - bpy.ops.bim.add_type_instance(ifc_class="IfcWallType", relating_type=int(props.relating_type)) - return {"FINISHED"} + if obj.mode == "EDIT": + bpy.ops.bim.dynamically_void_product(obj=obj.name) + IfcStore.edited_objs.add(obj) + bm = bmesh.from_edit_mesh(obj.data) + bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges) + bmesh.update_edit_mesh(obj.data) + bm.free() + else: + new_origin = obj.matrix_world @ Vector(obj.bound_box[0]) + obj.data.transform( + Matrix.Translation( + (obj.matrix_world.inverted().to_quaternion() @ (obj.matrix_world.translation - new_origin)) + ) + ) + obj.matrix_world.translation = new_origin class JoinWall(bpy.types.Operator): @@ -54,15 +73,25 @@ class JoinWall(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} join_type: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): - selected_objs = context.selected_objects - if len(selected_objs) == 0: - return {"FINISHED"} + selected_objs = [o for o in context.selected_objects if o.BIMObjectProperties.ifc_definition_id] + for obj in selected_objs: + bpy.ops.bim.dynamically_void_product(obj=obj.name) if not self.join_type: for obj in selected_objs: DumbWallJoiner(obj, obj).unjoin() return {"FINISHED"} - if len(selected_objs) < 2 or not context.active_object: + if not context.active_object: + return {"FINISHED"} + if len(selected_objs) == 1: + DumbWallJoiner(context.active_object, target_coordinate=context.scene.cursor.location).extend() + IfcStore.edited_objs.add(context.active_object) + return {"FINISHED"} + if len(selected_objs) < 2: return {"FINISHED"} for obj in selected_objs: if obj == context.active_object: @@ -86,11 +115,14 @@ class AlignWall(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} align_type: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + selected_valid_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")] + return context.active_object and len(selected_valid_objects) > 1 + def execute(self, context): - selected_objs = context.selected_objects - if len(selected_objs) < 2 or not context.active_object: - return {"FINISHED"} - for obj in selected_objs: + selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")] + for obj in selected_objects: if obj == context.active_object: continue aligner = DumbWallAligner(obj, context.active_object) @@ -109,10 +141,12 @@ class FlipWall(bpy.types.Operator): bl_label = "Flip Wall" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): - selected_objs = context.selected_objects - if len(selected_objs) == 0: - return {"FINISHED"} + selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")] for obj in selected_objs: DumbWallFlipper(obj).flip() IfcStore.edited_objs.add(obj) @@ -124,12 +158,14 @@ class SplitWall(bpy.types.Operator): bl_label = "Split Wall" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): - selected_objs = context.selected_objects - if len(selected_objs) == 0: - return {"FINISHED"} + selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")] for obj in selected_objs: - DumbWallSplitter(obj, bpy.context.scene.cursor.location).split() + DumbWallSplitter(obj, context.scene.cursor.location).split() IfcStore.edited_objs.add(obj) return {"FINISHED"} @@ -145,6 +181,8 @@ def recalculate_dumb_wall_origin(wall, new_origin=None): ) ) wall.matrix_world.translation = new_origin + for child in wall.children: + child.matrix_parent_inverse = wall.matrix_world.inverted() class DumbWallSplitter: @@ -220,6 +258,9 @@ class DumbWallFlipper: ).length < 0.001: recalculate_dumb_wall_origin(self.wall, self.wall.matrix_world @ Vector(self.wall.bound_box[7])) self.rotate_wall_180() + bpy.context.view_layer.update() + for child in self.wall.children: + child.matrix_parent_inverse = self.wall.matrix_world.inverted() else: recalculate_dumb_wall_origin(self.wall) @@ -324,16 +365,19 @@ class DumbWallJoiner: # 2. Given an "end face", identify a side "target face" of the other wall # to project towards. # 3. Project the vertices of an "end face" to the "target face". - def __init__(self, wall1, wall2): + # Alternatively, a target coordinate may be provided as an imaginary point for the wall to join to + def __init__(self, wall1, wall2=None, target_coordinate=None): self.wall1 = wall1 self.wall2 = wall2 + self.target_coordinate = target_coordinate self.should_project_to_frontface = True self.should_attempt_v_junction_projection = False self.initialise_convenience_variables() def initialise_convenience_variables(self): self.wall1_matrix = self.wall1.matrix_world - self.wall2_matrix = self.wall2.matrix_world + if self.wall2: + self.wall2_matrix = self.wall2.matrix_world self.pos_x = self.wall1_matrix.to_quaternion() @ Vector((1, 0, 0)) self.neg_x = self.wall1_matrix.to_quaternion() @ Vector((-1, 0, 0)) @@ -351,6 +395,30 @@ class DumbWallJoiner: self.wall1.data.vertices[v].co[0] = max_x self.recalculate_origins() + # An extension is where a single end of wall1 is projected to an imaginary + # plane denoted by the target coordinate. + def extend(self): + wall1_min_faces, wall1_max_faces = self.get_wall_end_faces(self.wall1) + ef1_distance = abs( + mathutils.geometry.distance_point_to_plane( + self.wall1_matrix @ self.wall1.data.vertices[wall1_min_faces[0].vertices[0]].co, + self.target_coordinate, + self.pos_x, + ) + ) + ef2_distance = abs( + mathutils.geometry.distance_point_to_plane( + self.wall1_matrix @ self.wall1.data.vertices[wall1_max_faces[0].vertices[0]].co, + self.target_coordinate, + self.neg_x, + ) + ) + if ef1_distance < ef2_distance: + self.project_end_faces_to_target(wall1_min_faces) + else: + self.project_end_faces_to_target(wall1_max_faces) + self.recalculate_origins() + # A T-junction is an ordered operation where a single end of wall1 is joined # to wall2 if possible (i.e. walls aren't parallel). Wall2 is not modified. # First, wall1 end faces are identified. We attempt to project an end face @@ -436,7 +504,8 @@ class DumbWallJoiner: def recalculate_origins(self): bpy.context.view_layer.update() recalculate_dumb_wall_origin(self.wall1) - recalculate_dumb_wall_origin(self.wall2) + if self.wall2: + recalculate_dumb_wall_origin(self.wall2) def swap_walls(self): self.wall1, self.wall2 = self.wall2, self.wall1 @@ -464,6 +533,14 @@ class DumbWallJoiner: local_point = wall_matrix.inverted() @ point wall.data.vertices[v].co = local_point + def project_end_faces_to_target(self, end_faces): + for end_face in end_faces: + for v in end_face.vertices: + vertex = self.wall1_matrix @ self.wall1.data.vertices[v].co + self.wall1.data.vertices[v].co = self.wall1_matrix.inverted() @ mathutils.geometry.intersect_line_plane( + vertex, vertex + self.pos_x, self.target_coordinate, self.pos_x + ) + # A projection target face is a side face on the target wall that has a # significant local Y component to its normal (i.e. is not pointing up or # down or something). In addition, its plane must intersect with the @@ -533,6 +610,8 @@ class DumbWallJoiner: min_x = min([v[0] for v in wall.bound_box]) max_x = max([v[0] for v in wall.bound_box]) for f in wall.data.polygons: + if abs(f.normal.x) < 0.1: + continue end_face_index = self.get_wall_face_end(wall, f, min_x, max_x) if end_face_index == 1: min_faces.append(f) @@ -543,9 +622,9 @@ class DumbWallJoiner: # 1 is the leftmost (minimum local X axis) end, and 2 is the rightmost end def get_wall_face_end(self, wall, face, min_x, max_x): for v in face.vertices: - if wall.data.vertices[v].co.x == min_x and abs(face.normal.x) > 0.1: + if wall.data.vertices[v].co.x == min_x: return 1 - if wall.data.vertices[v].co.x == max_x and abs(face.normal.x) > 0.1: + if wall.data.vertices[v].co.x == max_x: return 2 @@ -563,7 +642,7 @@ class DumbWallGenerator: if material.is_a("IfcMaterialLayerSet"): thicknesses = [l.LayerThickness for l in material.MaterialLayers] break - if not thicknesses: + if not sum(thicknesses): return self.collection = bpy.context.view_layer.active_layer_collection.collection @@ -695,21 +774,28 @@ class DumbWallGenerator: ] mesh = bpy.data.meshes.new(name="Wall") mesh.from_pydata(verts, [], faces) - obj = bpy.data.objects.new("Wall", mesh) + + ifc_classes = ifcopenshell.util.type.get_applicable_entities(self.relating_type.is_a(), self.file.schema) + # Standard cases are deprecated, so let's cull them + ifc_class = [c for c in ifc_classes if "StandardCase" not in c][0] + + obj = bpy.data.objects.new(ifc_class[3:], mesh) obj.location = self.location obj.rotation_euler[2] = self.rotation if self.collection_obj and self.collection_obj.BIMObjectProperties.ifc_definition_id: obj.location[2] = self.collection_obj.location[2] self.collection.objects.link(obj) + bpy.ops.bim.assign_class( obj=obj.name, - ifc_class="IfcWall", + ifc_class=ifc_class, ifc_representation_class="IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef", ) + bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name) element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") - ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbWall"}) + ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbLayer2"}) MaterialData.load(self.file) obj.select_set(True) return obj @@ -718,7 +804,7 @@ class DumbWallGenerator: def ensure_solid(usecase_path, ifc_file, settings): product = ifc_file.by_id(settings["blender_object"].BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2": return settings["ifc_representation_class"] = "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef" @@ -730,7 +816,7 @@ def generate_axis(usecase_path, ifc_file, settings): obj = settings["blender_object"] product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2": return old_axis = ifcopenshell.util.representation.get_representation(product, "Model", "Axis", "GRAPH_VIEW") if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body": @@ -763,7 +849,7 @@ def calculate_quantities(usecase_path, ifc_file, settings): obj = settings["blender_object"] product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id) parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2": return qto = ifcopenshell.api.run( "pset.add_qto", ifc_file, should_run_listeners=False, product=product, name="Qto_WallBaseQuantities" @@ -847,7 +933,7 @@ class DumbWallPlaner: def change_thickness(self, element, thickness): parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric") - if not parametric or parametric["Engine"] != "BlenderBIM.DumbWall": + if not parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2": return obj = IfcStore.get_element(element.id()) @@ -873,15 +959,19 @@ class DumbWallPlaner: IfcStore.edited_objs.add(obj) def thicken_face(self, face, delta_thickness): - slide_magnitude = abs(delta_thickness) / 2 + slide_magnitude = abs(delta_thickness) for vert in face.verts: slide_vector = None for edge in vert.link_edges: other_vert = edge.verts[1] if edge.verts[0] == vert else edge.verts[0] if delta_thickness > 0: - potential_slide_vector = vert.co - other_vert.co + potential_slide_vector = (vert.co - other_vert.co).normalized() + if potential_slide_vector.y < 0: + continue else: - potential_slide_vector = other_vert.co - vert.co + potential_slide_vector = (other_vert.co - vert.co).normalized() + if potential_slide_vector.y > 0: + continue if abs(potential_slide_vector.x) > 0.9 or abs(potential_slide_vector.z) > 0.9: continue slide_vector = potential_slide_vector diff --git a/src/blenderbim/blenderbim/bim/module/model/window.py b/src/blenderbim/blenderbim/bim/module/model/window.py index a0f99082c4..fd081ab0a8 100644 --- a/src/blenderbim/blenderbim/bim/module/model/window.py +++ b/src/blenderbim/blenderbim/bim/module/model/window.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import math import ifcopenshell @@ -10,7 +28,7 @@ from blenderbim.bim.ifc import IfcStore def add_object(self, context): guid = ifcopenshell.guid.new() leaf_width = self.overall_width - 0.045 - 0.045 - #verts = [ + # verts = [ # # Left lining # Vector((0, 0, 0)), # Vector((0, self.depth, 0)), @@ -31,8 +49,8 @@ def add_object(self, context): # Vector((0.04, (self.depth / 2) - 0.005, 0)), # Vector((self.overall_width - 0.04, (self.depth / 2) - 0.005, 0)), # Vector((self.overall_width - 0.04, (self.depth / 2) + 0.005, 0)), - #] - #edges = [ + # ] + # edges = [ # [0, 1], # [1, 2], # [2, 3], @@ -47,11 +65,11 @@ def add_object(self, context): # [13, 14], # [14, 15], # [15, 12], # Window panel - #] - #faces = [] - #mesh = bpy.data.meshes.new(name="Plan/Annotation/PLAN_VIEW/" + guid) - #mesh.use_fake_user = True - #mesh.from_pydata(verts, edges, faces) + # ] + # faces = [] + # mesh = bpy.data.meshes.new(name="Plan/Annotation/PLAN_VIEW/" + guid) + # mesh.use_fake_user = True + # mesh.from_pydata(verts, edges, faces) # Window lining profile verts = [ @@ -67,7 +85,7 @@ def add_object(self, context): obj = bpy.data.objects.new("Window Profile", mesh) context.view_layer.active_layer_collection.collection.objects.link(obj) - bpy.context.view_layer.objects.active = obj + context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.convert(target="CURVE") @@ -85,7 +103,7 @@ def add_object(self, context): obj2 = bpy.data.objects.new("Window", mesh) context.view_layer.active_layer_collection.collection.objects.link(obj2) - bpy.context.view_layer.objects.active = obj2 + context.view_layer.objects.active = obj2 obj2.select_set(True) bpy.ops.object.convert(target="CURVE") obj2.data.splines[0].use_cyclic_u = True @@ -116,11 +134,11 @@ def add_object(self, context): modifier.thickness = self.overall_height - 0.08 context.view_layer.active_layer_collection.collection.objects.link(obj3) - bpy.context.view_layer.objects.active = obj3 + context.view_layer.objects.active = obj3 obj3.select_set(True) bpy.ops.object.convert(target="MESH") - ctx = bpy.context.copy() + ctx = context.copy() ctx["active_object"] = obj2 ctx["selected_editable_objects"] = [obj2, obj3] bpy.ops.object.join(ctx) @@ -142,7 +160,7 @@ def add_object(self, context): modifier.thickness = self.overall_height context.view_layer.active_layer_collection.collection.objects.link(obj4) - bpy.context.view_layer.objects.active = obj4 + context.view_layer.objects.active = obj4 obj4.select_set(True) bpy.ops.object.convert(target="MESH") obj4.display_type = "WIRE" @@ -155,29 +173,33 @@ def add_object(self, context): bpy.ops.bim.assign_class(obj=obj2.name, ifc_class="IfcWindow") obj2.name = "Window" obj2.location = context.scene.cursor.location - #obj2.data.name = "Model/Body/MODEL_VIEW/" + guid - #obj2.data.use_fake_user = True + # obj2.data.name = "Model/Body/MODEL_VIEW/" + guid + # obj2.data.use_fake_user = True - #rep = obj2.BIMObjectProperties.representation_contexts.add() - #rep.context = "Model" - #rep.name = "Body" - #rep.target_view = "MODEL_VIEW" + # rep = obj2.BIMObjectProperties.representation_contexts.add() + # rep.context = "Model" + # rep.name = "Body" + # rep.target_view = "MODEL_VIEW" - #rep = obj2.BIMObjectProperties.representation_contexts.add() - #rep.context = "Plan" - #rep.name = "Annotation" - #rep.target_view = "PLAN_VIEW" + # rep = obj2.BIMObjectProperties.representation_contexts.add() + # rep.context = "Plan" + # rep.name = "Annotation" + # rep.target_view = "PLAN_VIEW" class BIM_OT_add_object(Operator): bl_idname = "mesh.add_window" bl_label = "Dumb Window" + bl_options = {"REGISTER", "UNDO"} overall_width: FloatProperty(name="Overall Width", default=0.7) overall_height: FloatProperty(name="Overall Height", default=1) depth: FloatProperty(name="Depth", default=0.1) def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): add_object(self, context) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 741276a526..4d271a6f57 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -1,6 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy +import blenderbim.bim.module.type.prop as type_prop from bpy.types import WorkSpaceTool +from blenderbim.bim.ifc import IfcStore class BimTool(WorkSpaceTool): @@ -16,44 +36,153 @@ class BimTool(WorkSpaceTool): bl_keymap = ( # ("bim.wall_tool_op", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}), # ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}), - ("bim.add_wall", {"type": "A", "value": "PRESS", "shift": True}, {"properties": []}), - ("bim.join_wall", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("join_type", "T")]}), + ("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}), + ("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}), ("bim.join_wall", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("join_type", "L")]}), ("bim.join_wall", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("join_type", "V")]}), ("bim.flip_wall", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}), ("bim.split_wall", {"type": "S", "value": "PRESS", "shift": True}, {"properties": []}), - ( - "bim.align_wall", - {"type": "X", "value": "PRESS", "shift": True}, - {"properties": [("align_type", "EXTERIOR")]}, - ), - ( - "bim.align_wall", - {"type": "C", "value": "PRESS", "shift": True}, - {"properties": [("align_type", "CENTERLINE")]}, - ), - ( - "bim.align_wall", - {"type": "V", "value": "PRESS", "shift": True}, - {"properties": [("align_type", "INTERIOR")]}, - ), + ("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}), + ("bim.hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}), + ("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}), + ("bim.hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}), + ("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}), + ("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}), ) def draw_settings(context, layout, tool): - props = context.scene.BIMModelProperties row = layout.row(align=True) - row.prop(props, "relating_type", text="") + if not IfcStore.get_file(): + row.label(text="No IFC Project", icon="ERROR") + return + props = context.scene.BIMTypeProperties + ifc_classes_is_empty = not bool(type_prop.getIfcTypes(props, context)) + if ifc_classes_is_empty: + row.label(text="No IFC Class") + else: + row.prop(props, "ifc_class", text="") + if type_prop.getAvailableTypes(props, context): + row.prop(props, "relating_type", text="") + else: + row.label(text="No Relating Type") row.label(text="", icon="BLANK1") + row = layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") - row.label(text="Add", icon="EVENT_A") - row.label(text="Extend", icon="EVENT_E") - row.label(text="Butt", icon="EVENT_T") - row.label(text="Mitre", icon="EVENT_Y") - row.label(text="Flip", icon="EVENT_F") - row.label(text="Split", icon="EVENT_S") - row.label(text="", icon="EVENT_X") - row.label(text="", icon="EVENT_C") - row.label(text="", icon="EVENT_V") + row.label(text="Add Type Instance", icon="EVENT_A") + + if not ifc_classes_is_empty: + if props.ifc_class == "IfcWallType": + row = layout.row() + row.label(text="Join") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Extend", icon="EVENT_E") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Butt", icon="EVENT_T") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Mitre", icon="EVENT_Y") + + row = layout.row() + row.label(text="Wall Tools") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Flip", icon="EVENT_F") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Split", icon="EVENT_S") + + if props.ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"): + row = layout.row() + row.label(text="Join") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Extend", icon="EVENT_E") + + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Opening", icon="EVENT_O") + + row = layout.row(align=True) row.label(text="Align") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Align Exterior", icon="EVENT_X") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Align Centerline", icon="EVENT_C") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Align Interior", icon="EVENT_V") + row = layout.row(align=True) + + row = layout.row(align=True) + row.label(text="Mode") + row = layout.row(align=True) + row.label(text="", icon="EVENT_ALT") + row.label(text="Opening", icon="EVENT_O") + row = layout.row(align=True) + row.label(text="", icon="EVENT_ALT") + row.label(text="Decomposition", icon="EVENT_D") + + +class Hotkey(bpy.types.Operator): + bl_idname = "bim.hotkey" + bl_label = "Hotkey" + bl_options = {"REGISTER", "UNDO"} + hotkey: bpy.props.StringProperty() + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.props = context.scene.BIMTypeProperties + self.ifc_classes_is_empty = not bool(type_prop.getIfcTypes(self.props, context)) + getattr(self, f"hotkey_{self.hotkey}")() + return {"FINISHED"} + + def hotkey_S_A(self): + bpy.ops.bim.add_type_instance() + + def hotkey_S_C(self): + if not self.ifc_classes_is_empty and self.props.ifc_class == "IfcWallType": + bpy.ops.bim.align_wall(align_type="CENTERLINE") + else: + bpy.ops.bim.align_product(align_type="CENTERLINE") + + def hotkey_S_E(self): + if self.ifc_classes_is_empty: + return + if self.props.ifc_class == "IfcWallType": + bpy.ops.bim.join_wall(join_type="T") + elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]: + bpy.ops.bim.extend_profile() + + def hotkey_S_V(self): + if not self.ifc_classes_is_empty and self.props.ifc_class == "IfcWallType": + bpy.ops.bim.align_wall(align_type="INTERIOR") + else: + bpy.ops.bim.align_product(align_type="POSITIVE") + + def hotkey_S_X(self): + if not self.ifc_classes_is_empty and self.props.ifc_class == "IfcWallType": + if bpy.ops.bim.align_wall.poll(): + bpy.ops.bim.align_wall(align_type="EXTERIOR") + else: + bpy.ops.bim.align_product(align_type="NEGATIVE") + + def hotkey_S_O(self): + bpy.ops.bim.add_element_opening(voided_building_element="", filling_building_element="") + + def hotkey_A_D(self): + bpy.ops.bim.toggle_decomposition_parenting() + + def hotkey_A_O(self): + bpy.ops.bim.toggle_opening_visibility() diff --git a/src/blenderbim/blenderbim/bim/module/owner/__init__.py b/src/blenderbim/blenderbim/bim/module/owner/__init__.py index a097f0fb07..e33cb27af8 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/owner/__init__.py @@ -1,7 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator classes = ( + operator.AddOrRemoveElementFromCollection, operator.EnableEditingPerson, operator.DisableEditingPerson, operator.AddPerson, diff --git a/src/blenderbim/blenderbim/bim/module/owner/operator.py b/src/blenderbim/blenderbim/bim/module/owner/operator.py index 8accd17301..9c5eb5c3b1 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/operator.py +++ b/src/blenderbim/blenderbim/bim/module/owner/operator.py @@ -1,13 +1,68 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy -import json import ifcopenshell.api from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.owner.data import Data +def flatten_collection(collection): + return [v.name for v in collection] if collection else None + + +def populate_collection(collection, collection_data): + collection.clear() + if collection_data: + for value in collection_data: + collection.add().name = value + else: + collection.add() + + +class AddOrRemoveElementFromCollection(bpy.types.Operator): + bl_idname = "bim.add_or_remove_element_from_collection" + bl_label = "Add or Remove Element From Collection" + bl_options = {"REGISTER", "UNDO"} + operation: bpy.props.EnumProperty( + items=(("+", "Add", "Add item to collection"), ("-", "Remove", "Remove item from collection")), + default="+", + ) + collection_path: bpy.props.StringProperty() + selected_item_idx: bpy.props.IntProperty(default=-1) + + def execute(self, context): + # Ugly but I hate using eval() + collection = context.scene + for attr in self.collection_path.split("."): + if hasattr(collection, attr): + collection = getattr(collection, attr) + if self.operation == "+" and hasattr(collection, "add"): + collection.add() + elif hasattr(collection, "remove") and 0 <= self.selected_item_idx < len(collection): + collection.remove(self.selected_item_idx) + return {"FINISHED"} + + class EnableEditingPerson(bpy.types.Operator): bl_idname = "bim.enable_editing_person" bl_label = "Enable Editing Person" + bl_options = {"REGISTER", "UNDO"} person_id: bpy.props.IntProperty() def execute(self, context): @@ -16,18 +71,20 @@ class EnableEditingPerson(bpy.types.Operator): props.active_person_id = self.person_id data = Data.people[self.person_id] name = data["Id"] if self.file.schema == "IFC2X3" else data["Identification"] - props.person.name = name or "" - props.person.family_name = data["FamilyName"] or "" - props.person.given_name = data["GivenName"] or "" - props.person.middle_names = json.dumps(data["MiddleNames"]) if data["MiddleNames"] else "" - props.person.prefix_titles = json.dumps(data["PrefixTitles"]) if data["PrefixTitles"] else "" - props.person.suffix_titles = json.dumps(data["SuffixTitles"]) if data["SuffixTitles"] else "" + person = props.person + person.name = name or "" + person.family_name = data["FamilyName"] or "" + person.given_name = data["GivenName"] or "" + populate_collection(person.middle_names, data.get("MiddleNames", None)) + populate_collection(person.prefix_titles, data.get("PrefixTitles", None)) + populate_collection(person.suffix_titles, data.get("SuffixTitles", None)) return {"FINISHED"} class DisableEditingPerson(bpy.types.Operator): bl_idname = "bim.disable_editing_person" bl_label = "Disable Editing Person" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMOwnerProperties.active_person_id = 0 @@ -37,8 +94,12 @@ class DisableEditingPerson(bpy.types.Operator): class AddPerson(bpy.types.Operator): bl_idname = "bim.add_person" bl_label = "Add Person" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run("owner.add_person", IfcStore.get_file()) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -47,17 +108,22 @@ class AddPerson(bpy.types.Operator): class EditPerson(bpy.types.Operator): bl_idname = "bim.edit_person" bl_label = "Edit Person" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMOwnerProperties + person = props.person attributes = { - "Identification": props.person.name or None, - "FamilyName": props.person.family_name or None, - "GivenName": props.person.given_name or None, - "MiddleNames": json.loads(props.person.middle_names) if props.person.middle_names else None, - "PrefixTitles": json.loads(props.person.prefix_titles) if props.person.prefix_titles else None, - "SuffixTitles": json.loads(props.person.suffix_titles) if props.person.suffix_titles else None, + "Identification": person.name or None, + "FamilyName": person.family_name or None, + "GivenName": person.given_name or None, + "MiddleNames": flatten_collection(person.middle_names), + "PrefixTitles": flatten_collection(person.prefix_titles), + "SuffixTitles": flatten_collection(person.suffix_titles), } if self.file.schema == "IFC2X3": attributes["Id"] = attributes["Identification"] @@ -75,9 +141,13 @@ class EditPerson(bpy.types.Operator): class RemovePerson(bpy.types.Operator): bl_idname = "bim.remove_person" bl_label = "Remove Person" + bl_options = {"REGISTER", "UNDO"} person_id: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run("owner.remove_person", self.file, **{"person": self.file.by_id(self.person_id)}) Data.load(IfcStore.get_file()) @@ -87,6 +157,7 @@ class RemovePerson(bpy.types.Operator): class EnableEditingRole(bpy.types.Operator): bl_idname = "bim.enable_editing_role" bl_label = "Enable Editing Role" + bl_options = {"REGISTER", "UNDO"} role_id: bpy.props.IntProperty() def execute(self, context): @@ -103,6 +174,7 @@ class EnableEditingRole(bpy.types.Operator): class DisableEditingRole(bpy.types.Operator): bl_idname = "bim.disable_editing_role" bl_label = "Disable Editing Role" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMOwnerProperties.active_role_id = 0 @@ -112,9 +184,13 @@ class DisableEditingRole(bpy.types.Operator): class AddRole(bpy.types.Operator): bl_idname = "bim.add_role" bl_label = "Add Role" + bl_options = {"REGISTER", "UNDO"} assigned_object_id: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "owner.add_role", self.file, **{"assigned_object": self.file.by_id(self.assigned_object_id)} @@ -126,8 +202,12 @@ class AddRole(bpy.types.Operator): class EditRole(bpy.types.Operator): bl_idname = "bim.edit_role" bl_label = "Edit Role" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMOwnerProperties attributes = { @@ -146,9 +226,13 @@ class EditRole(bpy.types.Operator): class RemoveRole(bpy.types.Operator): bl_idname = "bim.remove_role" bl_label = "Remove Role" + bl_options = {"REGISTER", "UNDO"} role_id: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run("owner.remove_role", self.file, **{"role": self.file.by_id(self.role_id)}) Data.load(IfcStore.get_file()) @@ -158,10 +242,14 @@ class RemoveRole(bpy.types.Operator): class AddAddress(bpy.types.Operator): bl_idname = "bim.add_address" bl_label = "Add Address" + bl_options = {"REGISTER", "UNDO"} assigned_object_id: bpy.props.IntProperty() ifc_class: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "owner.add_address", @@ -175,6 +263,7 @@ class AddAddress(bpy.types.Operator): class EnableEditingAddress(bpy.types.Operator): bl_idname = "bim.enable_editing_address" bl_label = "Enable Editing Address" + bl_options = {"REGISTER", "UNDO"} address_id: bpy.props.IntProperty() def execute(self, context): @@ -182,35 +271,35 @@ class EnableEditingAddress(bpy.types.Operator): props = context.scene.BIMOwnerProperties props.active_address_id = self.address_id data = Data.addresses[self.address_id] - props.address.name = data["type"] - props.address.purpose = data["Purpose"] or "None" - props.address.description = data["Description"] or "" - props.address.user_defined_purpose = data["UserDefinedPurpose"] or "" + address = props.address + address.name = data["type"] + address.purpose = data["Purpose"] or "None" + address.description = data["Description"] or "" + address.user_defined_purpose = data["UserDefinedPurpose"] or "" if data["type"] == "IfcTelecomAddress": - props.address.telephone_numbers = json.dumps(data["TelephoneNumbers"]) if data["TelephoneNumbers"] else "" - props.address.facsimile_numbers = json.dumps(data["FacsimileNumbers"]) if data["FacsimileNumbers"] else "" - props.address.pager_number = data["PagerNumber"] or "" - props.address.electronic_mail_addresses = ( - json.dumps(data["ElectronicMailAddresses"]) if data["ElectronicMailAddresses"] else "" - ) - props.address.www_home_page_url = data["WWWHomePageURL"] or "" + populate_collection(address.telephone_numbers, data.get("TelephoneNumbers", None)) + populate_collection(address.facsimile_numbers, data.get("FacsimileNumbers", None)) + address.pager_number = data["PagerNumber"] or "" + populate_collection(address.electronic_mail_addresses, data.get("ElectronicMailAddresses", None)) + address.www_home_page_url = data["WWWHomePageURL"] or "" if self.file.schema != "IFC2X3": - props.address.messaging_ids = json.dumps(data["MessagingIDs"]) if data["MessagingIDs"] else "" + populate_collection(address.messaging_ids, data.get("MessagingIDs", None)) elif data["type"] == "IfcPostalAddress": - props.address.internal_location = data["InternalLocation"] or "" - props.address.address_lines = json.dumps(data["AddressLines"]) if data["AddressLines"] else "" - props.address.postal_box = data["PostalBox"] or "" - props.address.town = data["Town"] or "" - props.address.region = data["Region"] or "" - props.address.postal_code = data["PostalCode"] or "" - props.address.country = data["Country"] or "" + address.internal_location = data["InternalLocation"] or "" + populate_collection(address.address_lines, data.get("AddressLines", None)) + address.postal_box = data["PostalBox"] or "" + address.town = data["Town"] or "" + address.region = data["Region"] or "" + address.postal_code = data["PostalCode"] or "" + address.country = data["Country"] or "" return {"FINISHED"} class DisableEditingAddress(bpy.types.Operator): bl_idname = "bim.disable_editing_address" bl_label = "Disable Editing Address" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMOwnerProperties.active_address_id = 0 @@ -220,8 +309,12 @@ class DisableEditingAddress(bpy.types.Operator): class EditAddress(bpy.types.Operator): bl_idname = "bim.edit_address" bl_label = "Edit Address" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMOwnerProperties attributes = { @@ -235,18 +328,12 @@ class EditAddress(bpy.types.Operator): if address.is_a("IfcTelecomAddress"): attributes.update( { - "TelephoneNumbers": json.loads(props.address.telephone_numbers) - if props.address.telephone_numbers - else None, - "FacsimileNumbers": json.loads(props.address.facsimile_numbers) - if props.address.facsimile_numbers - else None, + "TelephoneNumbers": flatten_collection(props.address.telephone_numbers), + "FacsimileNumbers": flatten_collection(props.address.facsimile_numbers), "PagerNumber": props.address.pager_number or None, - "ElectronicMailAddresses": json.loads(props.address.electronic_mail_addresses) - if props.address.electronic_mail_addresses - else None, + "ElectronicMailAddresses": flatten_collection(props.address.electronic_mail_addresses), "WWWHomePageURL": props.address.www_home_page_url or None, - "MessagingIDs": json.loads(props.address.messaging_ids) if props.address.messaging_ids else None, + "MessagingIDs": flatten_collection(props.address.messaging_ids), } ) if self.file.schema == "IFC2X3": @@ -255,7 +342,7 @@ class EditAddress(bpy.types.Operator): attributes.update( { "InternalLocation": props.address.internal_location or None, - "AddressLines": json.loads(props.address.address_lines) if props.address.address_lines else None, + "AddressLines": flatten_collection(props.address.address_lines), "PostalBox": props.address.postal_box or None, "Town": props.address.town or None, "Region": props.address.region or None, @@ -272,9 +359,13 @@ class EditAddress(bpy.types.Operator): class RemoveAddress(bpy.types.Operator): bl_idname = "bim.remove_address" bl_label = "Remove Address" + bl_options = {"REGISTER", "UNDO"} address_id: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run("owner.remove_address", self.file, **{"address": self.file.by_id(self.address_id)}) Data.load(IfcStore.get_file()) @@ -284,6 +375,7 @@ class RemoveAddress(bpy.types.Operator): class EnableEditingOrganisation(bpy.types.Operator): bl_idname = "bim.enable_editing_organisation" bl_label = "Enable Editing Organisation" + bl_options = {"REGISTER", "UNDO"} organisation_id: bpy.props.IntProperty() def execute(self, context): @@ -301,6 +393,7 @@ class EnableEditingOrganisation(bpy.types.Operator): class DisableEditingOrganisation(bpy.types.Operator): bl_idname = "bim.disable_editing_organisation" bl_label = "Disable Editing Organisation" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMOwnerProperties.active_organisation_id = 0 @@ -310,8 +403,12 @@ class DisableEditingOrganisation(bpy.types.Operator): class AddOrganisation(bpy.types.Operator): bl_idname = "bim.add_organisation" bl_label = "Add Organisation" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run("owner.add_organisation", IfcStore.get_file()) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -320,8 +417,12 @@ class AddOrganisation(bpy.types.Operator): class EditOrganisation(bpy.types.Operator): bl_idname = "bim.edit_organisation" bl_label = "Edit Organisation" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMOwnerProperties attributes = { @@ -345,9 +446,13 @@ class EditOrganisation(bpy.types.Operator): class RemoveOrganisation(bpy.types.Operator): bl_idname = "bim.remove_organisation" bl_label = "Remove Organisation" + bl_options = {"REGISTER", "UNDO"} organisation_id: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "owner.remove_organisation", self.file, **{"organisation": self.file.by_id(self.organisation_id)} diff --git a/src/blenderbim/blenderbim/bim/module/owner/prop.py b/src/blenderbim/blenderbim/bim/module/owner/prop.py index a92a6488fe..867df5e021 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/prop.py +++ b/src/blenderbim/blenderbim/bim/module/owner/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from blenderbim.bim.ifc import IfcStore @@ -14,27 +32,39 @@ from bpy.props import ( CollectionProperty, ) +_persons_enum = [] +_organisations_enum = [] + + +def purge(): + global _persons_enum + global _organisations_enum + _persons_enum.clear() + _organisations_enum.clear() + def getPersons(self, context): + global _persons_enum if not Data.is_loaded: Data.load(IfcStore.get_file()) - results = [] + _persons_enum.clear() for ifc_id, person in Data.people.items(): if "Id" in person: identifier = person["Id"] or "" else: identifier = person["Identification"] or "" - results.append((str(ifc_id), identifier, "")) - return results + _persons_enum.append((str(ifc_id), identifier, "")) + return _persons_enum def getOrganisations(self, context): + global _organisations_enum if not Data.is_loaded: Data.load(IfcStore.get_file()) - results = [] + _organisations_enum.clear() for ifc_id, organisation in Data.organisations.items(): - results.append((str(ifc_id), organisation["Name"], "")) - return results + _organisations_enum.append((str(ifc_id), organisation["Name"], "")) + return _organisations_enum class Address(PropertyGroup): @@ -54,19 +84,19 @@ class Address(PropertyGroup): user_defined_purpose: StringProperty(name="Custom Purpose") internal_location: StringProperty(name="Internal Location") - address_lines: StringProperty(name="Address") + address_lines: CollectionProperty(type=StrProperty, name="Address") postal_box: StringProperty(name="Postal Box") town: StringProperty(name="Town") region: StringProperty(name="Region") postal_code: StringProperty(name="Postal Code") country: StringProperty(name="Country") - telephone_numbers: StringProperty(name="Telephone Numbers") - facsimile_numbers: StringProperty(name="Facsimile Numbers") + telephone_numbers: CollectionProperty(type=StrProperty, name="Telephone Numbers") + facsimile_numbers: CollectionProperty(type=StrProperty, name="Facsimile Numbers") pager_number: StringProperty(name="Pager Number") - electronic_mail_addresses: StringProperty(name="Emails") - www_home_page_url: StringProperty(name="Websites") - messaging_ids: StringProperty(name="IMs") + electronic_mail_addresses: CollectionProperty(type=StrProperty, name="Emails") + www_home_page_url: StringProperty(name="Website") + messaging_ids: CollectionProperty(type=StrProperty, name="IMs") class Role(PropertyGroup): @@ -112,9 +142,9 @@ class Person(PropertyGroup): name: StringProperty(name="Identification") family_name: StringProperty(name="Family Name") given_name: StringProperty(name="Given Name") - middle_names: StringProperty(name="Middle Names") - prefix_titles: StringProperty(name="Prefixes") - suffix_titles: StringProperty(name="Suffixes") + middle_names: CollectionProperty(type=StrProperty, name="Middle Names") + prefix_titles: CollectionProperty(type=StrProperty, name="Prefixes") + suffix_titles: CollectionProperty(type=StrProperty, name="Suffixes") class BIMOwnerProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/owner/ui.py b/src/blenderbim/blenderbim/bim/module/owner/ui.py index d47cc605e5..42565c302e 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/ui.py +++ b/src/blenderbim/blenderbim/bim/module/owner/ui.py @@ -1,11 +1,55 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Panel from ifcopenshell.api.owner.data import Data from blenderbim.bim.ifc import IfcStore +from .operator import AddOrRemoveElementFromCollection -def draw_roles_ui(box, assigned_object_id, roles): - props = bpy.context.scene.BIMOwnerProperties +def draw_string_collection(layout, owner, collection_name): + column = layout.column(align=True) + collection = getattr(owner, collection_name) + for i in range(len(collection)): + if i == 0: + row = draw_prop_on_new_row( + column, collection[i], "name", align=True, text=f"{owner.bl_rna.properties[collection_name].name}" + ) + add_op = row.operator(AddOrRemoveElementFromCollection.bl_idname, icon="ADD", text="") + add_op.operation = "+" + add_op.collection_path = collection.path_from_id() + else: + row = draw_prop_on_new_row(column, collection[i], "name", align=True, text=f"#{i + 1}") + rem_op = row.operator(AddOrRemoveElementFromCollection.bl_idname, icon="REMOVE", text="") + rem_op.operation = "-" + rem_op.collection_path = collection.path_from_id() + rem_op.selected_item_idx = i + + +def draw_prop_on_new_row(layout, owner, attribute, align=False, **kwargs): + row = layout.row(align=align) + row.prop(owner, attribute, **kwargs) + return row + + +def draw_roles_ui(box, assigned_object_id, roles, context): + props = context.scene.BIMOwnerProperties row = box.row(align=True) row.label(text="Roles") row.operator("bim.add_role", icon="ADD", text="").assigned_object_id = assigned_object_id @@ -14,15 +58,12 @@ def draw_roles_ui(box, assigned_object_id, roles): if props.active_role_id == role_id: blender_role = props.role box2 = box.box() - row = box2.row(align=True) - row.prop(blender_role, "name", icon="MOD_CLOTH", text="") + row = draw_prop_on_new_row(box2, blender_role, "name", align=True, icon="MOD_CLOTH", text="") row.operator("bim.edit_role", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_role", icon="X", text="") + row.operator("bim.disable_editing_role", icon="CANCEL", text="") if blender_role.name == "USERDEFINED": - row = box2.row() - row.prop(blender_role, "user_defined_role") - row = box2.row() - row.prop(blender_role, "description") + draw_prop_on_new_row(box2, blender_role, "user_defined_role") + draw_prop_on_new_row(box2, blender_role, "description") else: row = box.row(align=True) row.label(text=role["UserDefinedRole"] or role["Role"]) @@ -30,8 +71,8 @@ def draw_roles_ui(box, assigned_object_id, roles): row.operator("bim.remove_role", icon="X", text="").role_id = role_id -def draw_addresses_ui(box, assigned_object_id, addresses, file): - props = bpy.context.scene.BIMOwnerProperties +def draw_addresses_ui(box, assigned_object_id, addresses, file, context): + props = context.scene.BIMOwnerProperties row = box.row(align=True) row.label(text="Addresses") op = row.operator("bim.add_address", icon="LINK_BLEND", text="") @@ -45,45 +86,29 @@ def draw_addresses_ui(box, assigned_object_id, addresses, file): if props.active_address_id == address_id: blender_address = props.address box2 = box.box() - row = box2.row(align=True) - row.prop(blender_address, "purpose", icon="MOD_CLOTH", text="") + row = draw_prop_on_new_row(box2, blender_address, "purpose", align=True, icon="MOD_CLOTH", text="") row.operator("bim.edit_address", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_address", icon="X", text="") + row.operator("bim.disable_editing_address", icon="CANCEL", text="") if blender_address.purpose == "USERDEFINED": - row = box2.row() - row.prop(blender_address, "user_defined_purpose") - row = box2.row() - row.prop(blender_address, "description") + draw_prop_on_new_row(box2, blender_address, "user_defined_purpose") + draw_prop_on_new_row(box2, blender_address, "description") if address["type"] == "IfcTelecomAddress": - row = box2.row() - row.prop(blender_address, "telephone_numbers") - row = box2.row() - row.prop(blender_address, "facsimile_numbers") - row = box2.row() - row.prop(blender_address, "pager_number") - row = box2.row() - row.prop(blender_address, "electronic_mail_addresses") - row = box2.row() - row.prop(blender_address, "www_home_page_url") + draw_string_collection(box2, blender_address, "telephone_numbers") + draw_string_collection(box2, blender_address, "facsimile_numbers") + draw_prop_on_new_row(box2, blender_address, "pager_number") + draw_string_collection(box2, blender_address, "electronic_mail_addresses") + draw_prop_on_new_row(box2, blender_address, "www_home_page_url") if file.schema != "IFC2X3": - row = box2.row() - row.prop(blender_address, "messaging_ids") + draw_string_collection(box2, blender_address, "messaging_ids") elif address["type"] == "IfcPostalAddress": - row = box2.row() - row.prop(blender_address, "internal_location") - row = box2.row() - row.prop(blender_address, "address_lines") - row = box2.row() - row.prop(blender_address, "postal_box") - row = box2.row() - row.prop(blender_address, "town") - row = box2.row() - row.prop(blender_address, "region") - row = box2.row() - row.prop(blender_address, "postal_code") - row = box2.row() - row.prop(blender_address, "country") + draw_prop_on_new_row(box2, blender_address, "internal_location") + draw_string_collection(box2, blender_address, "address_lines") + draw_prop_on_new_row(box2, blender_address, "postal_box") + draw_prop_on_new_row(box2, blender_address, "town") + draw_prop_on_new_row(box2, blender_address, "region") + draw_prop_on_new_row(box2, blender_address, "postal_code") + draw_prop_on_new_row(box2, blender_address, "country") else: row = box.row(align=True) row.label(text=address["type"]) @@ -91,7 +116,6 @@ def draw_addresses_ui(box, assigned_object_id, addresses, file): row.operator("bim.remove_address", icon="X", text="").address_id = address_id - class BIM_PT_people(Panel): bl_label = "IFC People" bl_idname = "BIM_PT_people" @@ -120,23 +144,16 @@ class BIM_PT_people(Panel): if props.active_person_id == person_id: blender_person = props.person box = self.layout.box() - row = box.row(align=True) - row.prop(blender_person, "name", icon="USER", text="") + row = draw_prop_on_new_row(box, blender_person, "name", align=True, icon="USER", text="") row.operator("bim.edit_person", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_person", icon="X", text="") - row = box.row() - row.prop(blender_person, "family_name") - row = box.row() - row.prop(blender_person, "given_name") - row = box.row() - row.prop(blender_person, "middle_names") - row = box.row() - row.prop(blender_person, "prefix_titles") - row = box.row() - row.prop(blender_person, "suffix_titles") - - draw_roles_ui(box, person_id, person["Roles"]) - draw_addresses_ui(box, person_id, person["Addresses"], self.file) + row.operator("bim.disable_editing_person", icon="CANCEL", text="") + draw_prop_on_new_row(box, blender_person, "family_name") + draw_prop_on_new_row(box, blender_person, "given_name") + draw_string_collection(box, blender_person, "middle_names") + draw_string_collection(box, blender_person, "prefix_titles") + draw_string_collection(box, blender_person, "suffix_titles") + draw_roles_ui(box, person_id, person["Roles"], context) + draw_addresses_ui(box, person_id, person["Addresses"], self.file, context) else: row = self.layout.row(align=True) name = person["Id"] if self.file.schema == "IFC2X3" else person["Identification"] @@ -183,20 +200,20 @@ class BIM_PT_organisations(Panel): row = box.row(align=True) row.prop(blender_organisation, "name", icon="USER", text="") row.operator("bim.edit_organisation", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_organisation", icon="X", text="") - row = box.row() - row.prop(blender_organisation, "identification") - row = box.row() - row.prop(blender_organisation, "description") + row.operator("bim.disable_editing_organisation", icon="CANCEL", text="") + draw_prop_on_new_row(box, blender_organisation, "identification") + draw_prop_on_new_row(box, blender_organisation, "description") - draw_roles_ui(box, organisation_id, organisation["Roles"]) - draw_addresses_ui(box, organisation_id, organisation["Addresses"], self.file) + draw_roles_ui(box, organisation_id, organisation["Roles"], context) + draw_addresses_ui(box, organisation_id, organisation["Addresses"], self.file, context) else: row = self.layout.row(align=True) row.label(text=organisation["Name"]) if organisation["Roles"]: row.label(text=", ".join([Data.roles[r]["Role"] for r in organisation["Roles"]])) - row.operator("bim.enable_editing_organisation", icon="GREASEPENCIL", text="").organisation_id = organisation_id + row.operator( + "bim.enable_editing_organisation", icon="GREASEPENCIL", text="" + ).organisation_id = organisation_id if not organisation["is_engaged"]: row.operator("bim.remove_organisation", icon="X", text="").organisation_id = organisation_id @@ -224,11 +241,9 @@ class BIM_PT_owner(Panel): if not Data.people: self.layout.label(text="No people found.") else: - row = self.layout.row() - row.prop(props, "user_person") + draw_prop_on_new_row(self.layout, props, "user_person") if not Data.organisations: self.layout.label(text="No organisations found.") else: - row = self.layout.row() - row.prop(props, "user_organisation") + draw_prop_on_new_row(self.layout, props, "user_organisation") diff --git a/src/blenderbim/blenderbim/bim/module/parametric/__init__.py b/src/blenderbim/blenderbim/bim/module/parametric/__init__.py deleted file mode 100644 index a5f9cac3fd..0000000000 --- a/src/blenderbim/blenderbim/bim/module/parametric/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -import bpy -from . import ui, operator - -classes = ( - operator.ActivateParametricEngine, - ui.BIM_PT_parametric, -) - - -def register(): - pass - - -def unregister(): - pass diff --git a/src/blenderbim/blenderbim/bim/module/parametric/operator.py b/src/blenderbim/blenderbim/bim/module/parametric/operator.py deleted file mode 100644 index e10ac6ce4b..0000000000 --- a/src/blenderbim/blenderbim/bim/module/parametric/operator.py +++ /dev/null @@ -1,10 +0,0 @@ -import bpy -from blenderbim.bim.ifc import IfcStore - - -class ActivateParametricEngine(bpy.types.Operator): - bl_idname = "bim.activate_parametric_engine" - bl_label = "Activate Parametric Engine" - - def execute(self, context): - return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/parametric/ui.py b/src/blenderbim/blenderbim/bim/module/parametric/ui.py deleted file mode 100644 index 4b2470d10b..0000000000 --- a/src/blenderbim/blenderbim/bim/module/parametric/ui.py +++ /dev/null @@ -1,22 +0,0 @@ -from bpy.types import Panel -from blenderbim.bim.ifc import IfcStore -from ifcopenshell.api.type.data import Data - - -class BIM_PT_parametric(Panel): - bl_label = "IFC Parametric Engines" - bl_idname = "BIM_PT_parametric" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - - @classmethod - def poll(cls, context): - return IfcStore.get_file() - - def draw(self, context): - #props = context.active_object.BIMTypeProperties - row = self.layout.row(align=True) - #row.prop(props, "relating_type_class", text="") - row.operator("bim.activate_parametric_engine", icon="PLUGIN") diff --git a/src/blenderbim/blenderbim/bim/module/patch/__init__.py b/src/blenderbim/blenderbim/bim/module/patch/__init__.py index d8a39535d2..b5c7ef84c6 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/patch/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -5,6 +23,7 @@ classes = ( operator.SelectIfcPatchInput, operator.SelectIfcPatchOutput, operator.ExecuteIfcPatch, + operator.UpdateIfcPatchArguments, prop.BIMPatchProperties, ui.BIM_PT_patch, ) diff --git a/src/blenderbim/blenderbim/bim/module/patch/operator.py b/src/blenderbim/blenderbim/bim/module/patch/operator.py index 484adf3597..e8c4d1b89c 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/operator.py +++ b/src/blenderbim/blenderbim/bim/module/patch/operator.py @@ -1,11 +1,35 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import json +try: + import ifcpatch +except: + print("IfcPatch not available") + class SelectIfcPatchInput(bpy.types.Operator): bl_idname = "bim.select_ifc_patch_input" bl_label = "Select IFC Patch Input" + bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -21,6 +45,7 @@ class SelectIfcPatchInput(bpy.types.Operator): class SelectIfcPatchOutput(bpy.types.Operator): bl_idname = "bim.select_ifc_patch_output" bl_label = "Select IFC Patch Output" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -37,17 +62,55 @@ class ExecuteIfcPatch(bpy.types.Operator): bl_idname = "bim.execute_ifc_patch" bl_label = "Execute IFCPatch" file_format: bpy.props.StringProperty() + use_json_for_args: bpy.props.BoolProperty() + + @classmethod + def poll(cls, context): + input_file = context.scene.BIMPatchProperties.ifc_patch_input + return os.path.isfile(input_file) and "ifc" in os.path.splitext(input_file)[1].lower() def execute(self, context): - import ifcpatch + props = context.scene.BIMPatchProperties + if self.use_json_for_args or not props.ifc_patch_args_attr: + arguments = json.loads(props.ifc_patch_args or "[]") + else: + arguments = [arg.get_value() for arg in props.ifc_patch_args_attr] ifcpatch.execute( { - "input": context.scene.BIMPatchProperties.ifc_patch_input, - "output": context.scene.BIMPatchProperties.ifc_patch_output, - "recipe": context.scene.BIMPatchProperties.ifc_patch_recipes, - "arguments": json.loads(context.scene.BIMPatchProperties.ifc_patch_args or "[]"), + "input": props.ifc_patch_input, + "output": props.ifc_patch_output, + "recipe": props.ifc_patch_recipes, + "arguments": arguments, "log": os.path.join(context.scene.BIMProperties.data_dir, "process.log"), } ) return {"FINISHED"} + + +class UpdateIfcPatchArguments(bpy.types.Operator): + bl_idname = "bim.update_ifc_patch_arguments" + bl_label = "Update IFC Patch arguments" + recipe: bpy.props.StringProperty() + + def execute(self, context): + if self.recipe == "": + print("No Recipe Selected. Impossible to load arguments") + return {"FINISHED"} + patch_args = context.scene.BIMPatchProperties.ifc_patch_args_attr + patch_args.clear() + docs = ifcpatch.extract_docs(self.recipe, "Patcher", "__init__", ("src", "file", "logger", "args")) + if docs and "inputs" in docs: + inputs = docs["inputs"] + for arg_name in inputs: + arg_info = inputs[arg_name] + new_attr = patch_args.add() + new_attr.data_type = { + "str": "string", + "float": "float", + "int": "integer", + "bool": "boolean", + }[arg_info.get("type", "str")] + new_attr.name = arg_name + new_attr.set_value(arg_info.get("default", new_attr.get_value_default())) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/patch/prop.py b/src/blenderbim/blenderbim/bim/module/patch/prop.py index bfee66f4b0..27c0e44ab5 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/prop.py +++ b/src/blenderbim/blenderbim/bim/module/patch/prop.py @@ -1,6 +1,25 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import importlib from pathlib import Path +import ifcpatch from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -23,7 +42,7 @@ def purge(): ifcpatchrecipes_enum = [] -def getIfcPatchRecipes(self, context): +def get_ifcpatch_recipes(self, context): global ifcpatchrecipes_enum if len(ifcpatchrecipes_enum) < 1: ifcpatchrecipes_enum.clear() @@ -32,12 +51,18 @@ def getIfcPatchRecipes(self, context): f = str(filename.stem) if f == "__init__": continue - ifcpatchrecipes_enum.append((f, f, "")) + docs = ifcpatch.extract_docs(f, "Patcher", "__init__", ("src", "file", "logger", "args")) + ifcpatchrecipes_enum.append((f, f, docs.get("description", "") if docs else "")) return ifcpatchrecipes_enum +def update_ifc_patch_recipe(self, context): + bpy.ops.bim.update_ifc_patch_arguments(recipe=self.ifc_patch_recipes) + + class BIMPatchProperties(PropertyGroup): - ifc_patch_recipes: EnumProperty(items=getIfcPatchRecipes, name="Recipes") + ifc_patch_recipes: EnumProperty(items=get_ifcpatch_recipes, name="Recipes", update=update_ifc_patch_recipe) ifc_patch_input: StringProperty(default="", name="IFC Patch Input IFC") ifc_patch_output: StringProperty(default="", name="IFC Patch Output IFC") ifc_patch_args: StringProperty(default="", name="Arguments") + ifc_patch_args_attr: CollectionProperty(type=Attribute, name="Arguments") diff --git a/src/blenderbim/blenderbim/bim/module/patch/ui.py b/src/blenderbim/blenderbim/bim/module/patch/ui.py index 49600d2d1c..ec699ab3e0 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/ui.py +++ b/src/blenderbim/blenderbim/bim/module/patch/ui.py @@ -1,8 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy -from bpy.types import Panel +from blenderbim.bim.helper import draw_attributes -class BIM_PT_patch(Panel): +class BIM_PT_patch(bpy.types.Panel): bl_label = "IFC Patch" bl_idname = "BIM_PT_patch" bl_options = {"DEFAULT_CLOSED"} @@ -13,20 +31,24 @@ class BIM_PT_patch(Panel): def draw(self, context): layout = self.layout layout.use_property_split = True + layout.use_property_decorate = False scene = context.scene props = scene.BIMPatchProperties - row = layout.row() row.prop(props, "ifc_patch_recipes") + row = layout.row(align=True) row.prop(props, "ifc_patch_input") row.operator("bim.select_ifc_patch_input", icon="FILE_FOLDER", text="") + row = layout.row(align=True) row.prop(props, "ifc_patch_output") row.operator("bim.select_ifc_patch_output", icon="FILE_FOLDER", text="") - row = layout.row() - row.prop(props, "ifc_patch_args") - row = layout.row() - op = row.operator("bim.execute_ifc_patch") + if props.ifc_patch_args_attr: + draw_attributes(props.ifc_patch_args_attr, layout) + else: + layout.row().prop(props, "ifc_patch_args") + op = layout.operator("bim.execute_ifc_patch") + op.use_json_for_args = len(props.ifc_patch_args_attr) == 0 diff --git a/src/blenderbim/blenderbim/bim/module/profile/__init__.py b/src/blenderbim/blenderbim/bim/module/profile/__init__.py new file mode 100644 index 0000000000..18c05e7619 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/profile/__init__.py @@ -0,0 +1,41 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +from . import ui, prop, operator + +classes = ( + operator.LoadProfiles, + operator.DisableProfileEditingUI, + operator.RemoveProfileDef, + operator.EnableEditingProfile, + operator.DisableEditingProfile, + operator.EditProfile, + prop.Profile, + prop.BIMProfileProperties, + ui.BIM_PT_profiles, + ui.BIM_UL_profiles, +) + + +def register(): + bpy.types.Scene.BIMProfileProperties = bpy.props.PointerProperty(type=prop.BIMProfileProperties) + + +def unregister(): + del bpy.types.Scene.BIMProfileProperties diff --git a/src/blenderbim/blenderbim/bim/module/profile/operator.py b/src/blenderbim/blenderbim/bim/module/profile/operator.py new file mode 100644 index 0000000000..76ea8fa98a --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/profile/operator.py @@ -0,0 +1,116 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell.api +import blenderbim.bim.helper +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.profile.data import Data + + +class LoadProfiles(bpy.types.Operator): + bl_idname = "bim.load_profiles" + bl_label = "Load Profiles" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMProfileProperties + props.profiles.clear() + + for ifc_definition_id, profile in Data.profiles.items(): + new = props.profiles.add() + new.ifc_definition_id = ifc_definition_id + new.name = profile.get("ProfileName", "") or "Unnamed" + + props.is_editing = True + bpy.ops.bim.disable_editing_profile() + return {"FINISHED"} + + +class DisableProfileEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_profile_editing_ui" + bl_label = "Disable Profile Editing UI" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMProfileProperties.is_editing = False + return {"FINISHED"} + + +class RemoveProfileDef(bpy.types.Operator): + bl_idname = "bim.remove_profile_def" + bl_label = "Remove Profile Definition" + bl_options = {"REGISTER", "UNDO"} + profile: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMProfileProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("profile.remove_profile", self.file, **{"profile": self.file.by_id(self.profile)}) + Data.load(self.file) + bpy.ops.bim.load_profiles() + return {"FINISHED"} + + +class EnableEditingProfile(bpy.types.Operator): + bl_idname = "bim.enable_editing_profile" + bl_label = "Enable Editing Profile" + bl_options = {"REGISTER", "UNDO"} + profile: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMProfileProperties + props.profile_attributes.clear() + + data = Data.profiles[self.profile] + + blenderbim.bim.helper.import_attributes(data["type"], props.profile_attributes, data) + props.active_profile_id = self.profile + return {"FINISHED"} + + +class DisableEditingProfile(bpy.types.Operator): + bl_idname = "bim.disable_editing_profile" + bl_label = "Disable Editing Profile" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMProfileProperties.active_profile_id = 0 + return {"FINISHED"} + + +class EditProfile(bpy.types.Operator): + bl_idname = "bim.edit_profile" + bl_label = "Edit Profile" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMProfileProperties + attributes = blenderbim.bim.helper.export_attributes(props.profile_attributes) + self.file = IfcStore.get_file() + profile = self.file.by_id(props.active_profile_id) + ifcopenshell.api.run("profile.edit_profile", self.file, **{"profile": profile, "attributes": attributes}) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_profiles() + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/profile/prop.py b/src/blenderbim/blenderbim/bim/module/profile/prop.py new file mode 100644 index 0000000000..1f8f06ca33 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/profile/prop.py @@ -0,0 +1,48 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell +import ifcopenshell.util.schema +import ifcopenshell.util.attribute +from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.prop import StrProperty, Attribute +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +class Profile(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + +class BIMProfileProperties(PropertyGroup): + is_editing: BoolProperty(name="Is Editing") + profiles: CollectionProperty(name="Profiles", type=Profile) + active_profile_index: IntProperty(name="Active Profile Index") + active_profile_id: IntProperty(name="Active Profile Id") + profile_attributes: CollectionProperty(name="Profile Attributes", type=Attribute) diff --git a/src/blenderbim/blenderbim/bim/module/profile/ui.py b/src/blenderbim/blenderbim/bim/module/profile/ui.py new file mode 100644 index 0000000000..8044de9f54 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/profile/ui.py @@ -0,0 +1,85 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.bim.helper +from bpy.types import Panel, UIList +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.profile.data import Data + + +class BIM_PT_profiles(Panel): + bl_label = "IFC Profiles" + bl_idname = "BIM_PT_profiles" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + return file + + def draw(self, context): + self.file = IfcStore.get_file() + if not Data.is_loaded: + Data.load(self.file) + self.props = context.scene.BIMProfileProperties + + row = self.layout.row(align=True) + row.label(text="{} Profiles Found".format(len(Data.profiles)), icon="SNAP_GRID") + if self.props.is_editing: + row.operator("bim.disable_profile_editing_ui", text="", icon="CANCEL") + else: + row.operator("bim.load_profiles", text="", icon="GREASEPENCIL") + + if not self.props.is_editing: + return + + self.layout.template_list( + "BIM_UL_profiles", + "", + self.props, + "profiles", + self.props, + "active_profile_index", + ) + + if self.props.active_profile_id: + self.draw_editable_ui(context) + + def draw_editable_ui(self, context): + blenderbim.bim.helper.draw_attributes(self.props.profile_attributes, self.layout) + + +class BIM_UL_profiles(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMProfileProperties + if item: + row = layout.row(align=True) + row.label(text=item.name or "Unnamed") + + if props.active_profile_id == item.ifc_definition_id: + row.operator("bim.edit_profile", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_profile", text="", icon="CANCEL") + elif props.active_profile_id: + row.operator("bim.remove_profile_def", text="", icon="X").profile = item.ifc_definition_id + else: + op = row.operator("bim.enable_editing_profile", text="", icon="GREASEPENCIL") + op.profile = item.ifc_definition_id + row.operator("bim.remove_profile_def", text="", icon="X").profile = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index c01b78d775..a486c8d554 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -1,9 +1,33 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator classes = ( operator.CreateProject, - operator.CreateProjectLibrary, + operator.LoadProject, + operator.UnloadProject, + operator.LoadProjectElements, + operator.LinkIfc, + operator.UnlinkIfc, + operator.UnloadLink, + operator.LoadLink, operator.SelectLibraryFile, operator.ChangeLibraryElement, operator.RefreshLibrary, @@ -16,10 +40,15 @@ classes = ( operator.DisableEditingHeader, operator.EditHeader, prop.LibraryElement, + prop.FilterCategory, + prop.Link, prop.BIMProjectProperties, ui.BIM_PT_project, ui.BIM_PT_project_library, + ui.BIM_PT_links, ui.BIM_UL_library, + ui.BIM_UL_filter_categories, + ui.BIM_UL_links, ) diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index f01e937fdb..9f39040977 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -1,9 +1,30 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import os import bpy +import time import logging +import tempfile import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.selector import ifcopenshell.util.representation -import bpy import blenderbim.bim.handler from blenderbim.bim.ifc import IfcStore from blenderbim.bim import import_ifc @@ -24,22 +45,23 @@ class CreateProject(bpy.types.Operator): return result def _execute(self, context): + active_object = context.view_layer.objects.active self.file = IfcStore.get_file() if self.file: return {"FINISHED"} IfcStore.file = ifcopenshell.api.run( - "project.create_file", **{"version": bpy.context.scene.BIMProperties.export_schema} + "project.create_file", **{"version": context.scene.BIMProperties.export_schema} ) self.file = IfcStore.get_file() bpy.ops.bim.add_person() bpy.ops.bim.add_organisation() - project = bpy.data.objects.new("My Project", None) - site = bpy.data.objects.new("My Site", None) - building = bpy.data.objects.new("My Building", None) - building_storey = bpy.data.objects.new("Ground Floor", None) + project = bpy.data.objects.new(self.get_name("IfcProject", "My Project"), None) + site = bpy.data.objects.new(self.get_name("IfcSite", "My Site"), None) + building = bpy.data.objects.new(self.get_name("IfcBuilding", "My Building"), None) + building_storey = bpy.data.objects.new(self.get_name("IfcBuildingStorey", "My Storey"), None) bpy.ops.bim.assign_class(obj=project.name, ifc_class="IfcProject") bpy.ops.bim.assign_unit() @@ -49,7 +71,7 @@ class CreateProject(bpy.types.Operator): bpy.ops.bim.add_subcontext(context="Plan") bpy.ops.bim.add_subcontext(context="Plan", subcontext="Annotation", target_view="PLAN_VIEW") - bpy.context.scene.BIMProperties.contexts = str( + context.scene.BIMProperties.contexts = str( ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW").id() ) @@ -60,56 +82,21 @@ class CreateProject(bpy.types.Operator): bpy.ops.bim.assign_object(related_object=building.name, relating_object=site.name) bpy.ops.bim.assign_object(related_object=building_storey.name, relating_object=building.name) + context.view_layer.objects.active = active_object return {"FINISHED"} + def get_name(self, ifc_class, name): + if not bpy.data.objects.get(f"{ifc_class}/{name}"): + return name + i = 2 + while bpy.data.objects.get(f"{ifc_class}/{name} {i}"): + i += 1 + return f"{name} {i}" + def rollback(self, data): IfcStore.file = None - blenderbim.bim.handler.purge_module_data() def commit(self, data): - blenderbim.bim.handler.purge_module_data() - IfcStore.file = data["file"] - - -class CreateProjectLibrary(bpy.types.Operator): - bl_idname = "bim.create_project_library" - bl_label = "Create Project Library" - bl_options = {"REGISTER", "UNDO"} - - def execute(self, context): - IfcStore.begin_transaction(self) - IfcStore.add_transaction_operation(self, rollback=self.rollback, commit=lambda data: True) - result = self._execute(context) - self.transaction_data = {"file": self.file} - IfcStore.add_transaction_operation(self, rollback=lambda data: True, commit=self.commit) - IfcStore.end_transaction(self) - return result - - def _execute(self, context): - self.file = IfcStore.get_file() - if self.file: - return {"FINISHED"} - - IfcStore.file = ifcopenshell.api.run( - "project.create_file", **{"version": bpy.context.scene.BIMProperties.export_schema} - ) - self.file = IfcStore.get_file() - - if self.file.schema == "IFC2X3": - bpy.ops.bim.add_person() - bpy.ops.bim.add_organisation() - - project_library = bpy.data.objects.new("My Project Library", None) - bpy.ops.bim.assign_class(obj=project_library.name, ifc_class="IfcProjectLibrary") - bpy.ops.bim.assign_unit() - return {"FINISHED"} - - def rollback(self, data): - IfcStore.file = None - blenderbim.bim.handler.purge_module_data() - - def commit(self, data): - blenderbim.bim.handler.purge_module_data() IfcStore.file = data["file"] @@ -121,16 +108,19 @@ class SelectLibraryFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) def execute(self, context): + IfcStore.begin_transaction(self) old_filepath = IfcStore.library_path result = self._execute(context) self.transaction_data = {"old_filepath": old_filepath, "filepath": self.filepath} IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) return result def _execute(self, context): IfcStore.library_path = self.filepath IfcStore.library_file = ifcopenshell.open(self.filepath) bpy.ops.bim.refresh_library() + context.area.tag_redraw() return {"FINISHED"} def invoke(self, context, event): @@ -157,18 +147,16 @@ class RefreshLibrary(bpy.types.Operator): def execute(self, context): self.props = context.scene.BIMProjectProperties - while len(self.props.library_elements) > 0: - self.props.library_elements.remove(0) - - while len(self.props.library_breadcrumb) > 0: - self.props.library_breadcrumb.remove(0) + self.props.library_elements.clear() + self.props.library_breadcrumb.clear() self.props.active_library_element = "" types = IfcStore.library_file.wrapped_data.types_with_super() - if "IfcTypeProduct" in types: - new = self.props.library_elements.add() - new.name = "IfcTypeProduct" + for importable_type in sorted(["IfcTypeProduct", "IfcMaterial", "IfcCostSchedule", "IfcProfileDef"]): + if importable_type in types: + new = self.props.library_elements.add() + new.name = importable_type return {"FINISHED"} @@ -180,29 +168,54 @@ class ChangeLibraryElement(bpy.types.Operator): def execute(self, context): self.props = context.scene.BIMProjectProperties + self.file = IfcStore.get_file() + self.library_file = IfcStore.library_file ifc_classes = set() self.props.active_library_element = self.element_name crumb = self.props.library_breadcrumb.add() crumb.name = self.element_name - elements = IfcStore.library_file.by_type(self.element_name) + elements = self.library_file.by_type(self.element_name) [ifc_classes.add(e.is_a()) for e in elements] - while len(self.props.library_elements) > 0: - self.props.library_elements.remove(0) + self.props.library_elements.clear() if len(ifc_classes) == 1 and list(ifc_classes)[0] == self.element_name: - for element in elements: - new = self.props.library_elements.add() - new.name = element.Name or "Unnamed" - new.ifc_definition_id = element.id() - if IfcStore.library_file.schema == "IFC2X3" or not IfcStore.library_file.by_type("IfcProjectLibrary"): - new.is_declared = False - elif element.HasContext and element.HasContext[0].RelatingContext.is_a("IfcProjectLibrary"): - new.is_declared = True + for name, ifc_definition_id in sorted([(self.get_name(e), e.id()) for e in elements]): + self.add_library_asset(name, ifc_definition_id) else: - for ifc_class in ifc_classes: + for ifc_class in sorted(ifc_classes): + if ifc_class == self.element_name: + continue new = self.props.library_elements.add() new.name = ifc_class + for name, ifc_definition_id, ifc_class in sorted([(self.get_name(e), e.id(), e.is_a()) for e in elements]): + if ifc_class == self.element_name: + self.add_library_asset(name, ifc_definition_id) return {"FINISHED"} + def get_name(self, element): + if element.is_a("IfcProfileDef"): + return element.ProfileName or "Unnamed" + return element.Name or "Unnamed" + + def add_library_asset(self, name, ifc_definition_id): + new = self.props.library_elements.add() + new.name = name + new.ifc_definition_id = ifc_definition_id + element = self.library_file.by_id(ifc_definition_id) + if self.library_file.schema == "IFC2X3" or not self.library_file.by_type("IfcProjectLibrary"): + new.is_declared = False + elif getattr(element, "HasContext", None) and element.HasContext[0].RelatingContext.is_a("IfcProjectLibrary"): + new.is_declared = True + try: + if element.is_a("IfcMaterial"): + next(e for e in self.file.by_type("IfcMaterial") if e.Name == name) + elif element.is_a("IfcProfileDef"): + next(e for e in self.file.by_type("IfcProfileDef") if e.ProfileName == name) + else: + self.file.by_guid(element.GlobalId) + new.is_appended = True + except (AttributeError, RuntimeError, StopIteration): + new.is_appended = False + class RewindLibrary(bpy.types.Operator): bl_idname = "bim.rewind_library" @@ -229,10 +242,12 @@ class AssignLibraryDeclaration(bpy.types.Operator): definition: bpy.props.IntProperty() def execute(self, context): + IfcStore.begin_transaction(self) IfcStore.library_file.begin_transaction() result = self._execute(context) IfcStore.library_file.end_transaction() IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) return result def _execute(self, context): @@ -263,10 +278,12 @@ class UnassignLibraryDeclaration(bpy.types.Operator): definition: bpy.props.IntProperty() def execute(self, context): + IfcStore.begin_transaction(self) IfcStore.library_file.begin_transaction() result = self._execute(context) IfcStore.library_file.end_transaction() IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) return result def _execute(self, context): @@ -304,6 +321,11 @@ class AppendLibraryElement(bpy.types.Operator): bl_label = "Append Library Element" bl_options = {"REGISTER", "UNDO"} definition: bpy.props.IntProperty() + prop_index: bpy.props.IntProperty() + + @classmethod + def poll(cls, context): + return IfcStore.get_file() def execute(self, context): return IfcStore.execute_ifc_operator(self, context) @@ -316,28 +338,72 @@ class AppendLibraryElement(bpy.types.Operator): library=IfcStore.library_file, element=IfcStore.library_file.by_id(self.definition), ) - self.import_type_from_ifc(element) + if not element: + return {"FINISHED"} + if element.is_a("IfcTypeProduct"): + self.import_type_from_ifc(element, context) + elif element.is_a("IfcMaterial"): + self.import_material_from_ifc(element, context) + context.scene.BIMProjectProperties.library_elements[self.prop_index].is_appended = True blenderbim.bim.handler.purge_module_data() return {"FINISHED"} - def import_type_from_ifc(self, element): + def import_material_from_ifc(self, element, context): self.file = IfcStore.get_file() logger = logging.getLogger("ImportIFC") - ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, logger) + ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) + ifc_importer = import_ifc.IfcImporter(ifc_import_settings) + ifc_importer.file = self.file + blender_material = ifc_importer.create_material(element) + self.import_material_styles(blender_material, element, ifc_importer) + + def import_type_from_ifc(self, element, context): + self.file = IfcStore.get_file() + logger = logging.getLogger("ImportIFC") + ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) type_collection = bpy.data.collections.get("Types") if not type_collection: type_collection = bpy.data.collections.new("Types") - for collection in bpy.data.collections: + for collection in bpy.context.view_layer.layer_collection.children: if "IfcProject/" in collection.name: - collection.children.link(type_collection) + collection.collection.children.link(type_collection) + collection.children["Types"].hide_viewport = True break ifc_importer = import_ifc.IfcImporter(ifc_import_settings) ifc_importer.file = self.file ifc_importer.type_collection = type_collection + self.import_type_materials(element, ifc_importer) + self.import_type_styles(element, ifc_importer) ifc_importer.create_type_product(element) - ifc_importer.place_objects_in_spatial_tree() + ifc_importer.place_objects_in_collections() + + def import_type_materials(self, element, ifc_importer): + for rel in element.HasAssociations: + if not rel.is_a("IfcRelAssociatesMaterial"): + continue + for material in [e for e in self.file.traverse(rel) if e.is_a("IfcMaterial")]: + if IfcStore.get_element(material.id()): + continue + blender_material = ifc_importer.create_material(material) + self.import_material_styles(blender_material, material, ifc_importer) + + def import_type_styles(self, element, ifc_importer): + for representation_map in element.RepresentationMaps or []: + for element in self.file.traverse(representation_map): + if not element.is_a("IfcRepresentationItem") or not element.StyledByItem: + continue + for element2 in self.file.traverse(element.StyledByItem[0]): + if element2.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element2.id()): + ifc_importer.create_style(element2) + + def import_material_styles(self, blender_material, material, ifc_importer): + if not material.HasRepresentation: + return + for element in self.file.traverse(material.HasRepresentation[0]): + if element.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element.id()): + ifc_importer.create_style(element, blender_material) class EnableEditingHeader(bpy.types.Operator): @@ -345,6 +411,10 @@ class EnableEditingHeader(bpy.types.Operator): bl_label = "Enable Editing Header" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMProjectProperties @@ -377,12 +447,18 @@ class EditHeader(bpy.types.Operator): bl_label = "Edit Header" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): + IfcStore.begin_transaction(self) self.transaction_data = {} self.transaction_data["old"] = self.record_state() result = self._execute(context) self.transaction_data["new"] = self.record_state() IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) return result def _execute(self, context): @@ -429,3 +505,203 @@ class DisableEditingHeader(bpy.types.Operator): def execute(self, context): context.scene.BIMProjectProperties.is_editing = False return {"FINISHED"} + + +class LoadProject(bpy.types.Operator): + bl_idname = "bim.load_project" + bl_label = "Load Project" + bl_options = {"REGISTER", "UNDO"} + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) + is_advanced: bpy.props.BoolProperty(name="Enable Advanced Mode", default=False) + + def execute(self, context): + if not os.path.exists(self.filepath) or "ifc" not in os.path.splitext(self.filepath)[1].lower(): + return {"FINISHED"} + context.scene.BIMProperties.ifc_file = self.filepath + context.scene.BIMProjectProperties.is_loading = True + if not self.is_advanced: + bpy.ops.bim.load_project_elements() + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class UnloadProject(bpy.types.Operator): + bl_idname = "bim.unload_project" + bl_label = "Unload Project" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + IfcStore.purge() + context.scene.BIMProperties.ifc_file = "" + context.scene.BIMProjectProperties.is_loading = False + return {"FINISHED"} + + +class LoadProjectElements(bpy.types.Operator): + bl_idname = "bim.load_project_elements" + bl_label = "Load Project Elements" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + self.props = context.scene.BIMProjectProperties + self.file = IfcStore.get_file() + start = time.time() + logger = logging.getLogger("ImportIFC") + path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log") + if not os.access(context.scene.BIMProperties.data_dir, os.W_OK): + path_log = os.path.join(tempfile.mkdtemp(), "process.log") + logging.basicConfig( + filename=path_log, + filemode="a", + level=logging.DEBUG, + ) + settings = import_ifc.IfcImportSettings.factory(context, context.scene.BIMProperties.ifc_file, logger) + settings.has_filter = self.props.filter_mode != "NONE" + if self.props.filter_mode == "DECOMPOSITION": + settings.elements = self.get_decomposition_elements() + elif self.props.filter_mode == "IFC_CLASS": + settings.elements = self.get_ifc_class_elements() + elif self.props.filter_mode == "WHITELIST": + settings.elements = self.get_whitelist_elements() + elif self.props.filter_mode == "BLACKLIST": + settings.elements = self.get_blacklist_elements() + settings.collection_mode = self.props.collection_mode + settings.should_use_cpu_multiprocessing = self.props.should_use_cpu_multiprocessing + settings.should_merge_by_class = self.props.should_merge_by_class + settings.should_merge_by_material = self.props.should_merge_by_material + settings.should_merge_materials_by_colour = self.props.should_merge_materials_by_colour + settings.should_clean_mesh = self.props.should_clean_mesh + settings.deflection_tolerance = self.props.deflection_tolerance + settings.angular_tolerance = self.props.angular_tolerance + settings.should_offset_model = self.props.should_offset_model + settings.model_offset_coordinates = ( + [float(o) for o in self.props.model_offset_coordinates.split(",")] + if self.props.model_offset_coordinates + else (0, 0, 0) + ) + settings.logger.info("Starting import") + ifc_importer = import_ifc.IfcImporter(settings) + ifc_importer.execute() + settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start)) + print("Import finished in {:.2f} seconds".format(time.time() - start)) + context.scene.BIMProjectProperties.is_loading = False + return {"FINISHED"} + + def get_decomposition_elements(self): + containers = set() + for filter_category in self.props.filter_categories: + if not filter_category.is_selected: + continue + container = self.file.by_id(filter_category.ifc_definition_id) + while container: + containers.add(container) + container = ifcopenshell.util.element.get_aggregate(container) + if container.is_a("IfcContext"): + container = None + elements = set() + for container in containers: + for rel in container.ContainsElements: + elements.update(rel.RelatedElements) + self.append_decomposed_elements(elements) + return elements + + def append_decomposed_elements(self, elements): + decomposed_elements = set() + for element in elements: + if element.IsDecomposedBy: + for subelement in element.IsDecomposedBy[0].RelatedObjects: + decomposed_elements.add(subelement) + if decomposed_elements: + self.append_decomposed_elements(decomposed_elements) + elements.update(decomposed_elements) + + def get_ifc_class_elements(self): + elements = set() + for filter_category in self.props.filter_categories: + if not filter_category.is_selected: + continue + elements.update(self.file.by_type(filter_category.name, include_subtypes=False)) + return elements + + def get_whitelist_elements(self): + selector = ifcopenshell.util.selector.Selector() + return set(selector.parse(self.file, self.props.filter_query)) + + def get_blacklist_elements(self): + selector = ifcopenshell.util.selector.Selector() + return set(self.file.by_type("IfcElement")) - set(selector.parse(self.file, self.props.filter_query)) + + +class LinkIfc(bpy.types.Operator): + bl_idname = "bim.link_ifc" + bl_label = "Link IFC" + bl_options = {"REGISTER", "UNDO"} + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.blend;*.blend1", options={"HIDDEN"}) + + def execute(self, context): + new = context.scene.BIMProjectProperties.links.add() + new.name = self.filepath + bpy.ops.bim.load_link(filepath=self.filepath) + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class UnlinkIfc(bpy.types.Operator): + bl_idname = "bim.unlink_ifc" + bl_label = "UnLink IFC" + bl_options = {"REGISTER", "UNDO"} + filepath: bpy.props.StringProperty() + + def execute(self, context): + bpy.ops.bim.unload_link(filepath=self.filepath) + index = context.scene.BIMProjectProperties.links.find(self.filepath) + if index != -1: + context.scene.BIMProjectProperties.links.remove(index) + return {"FINISHED"} + + +class UnloadLink(bpy.types.Operator): + bl_idname = "bim.unload_link" + bl_label = "Unload Link" + bl_options = {"REGISTER", "UNDO"} + filepath: bpy.props.StringProperty() + + def execute(self, context): + for collection in context.scene.collection.children: + if collection.library and collection.library.filepath == self.filepath: + context.scene.collection.children.unlink(collection) + for scene in bpy.data.scenes: + if scene.library and scene.library.filepath == self.filepath: + bpy.data.scenes.remove(scene) + link = context.scene.BIMProjectProperties.links.get(self.filepath) + link.is_loaded = False + return {"FINISHED"} + + +class LoadLink(bpy.types.Operator): + bl_idname = "bim.load_link" + bl_label = "Load Link" + bl_options = {"REGISTER", "UNDO"} + filepath: bpy.props.StringProperty() + + def execute(self, context): + with bpy.data.libraries.load(self.filepath, link=True) as (data_from, data_to): + data_to.scenes = data_from.scenes + for scene in bpy.data.scenes: + if not scene.library or scene.library.filepath != self.filepath: + continue + for child in scene.collection.children: + if "IfcProject" not in child.name: + continue + bpy.data.scenes[0].collection.children.link(child) + link = context.scene.BIMProjectProperties.links.get(self.filepath) + link.is_loaded = True + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index a4052984a9..aaff522274 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -1,27 +1,79 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy +from blenderbim.bim.ifc import IfcStore from blenderbim.bim.prop import StrProperty from bpy.types import PropertyGroup from bpy.props import ( - PointerProperty, StringProperty, - EnumProperty, BoolProperty, - IntProperty, FloatProperty, - FloatVectorProperty, + IntProperty, CollectionProperty, ) +def update_filter_mode(self, context): + self.filter_categories.clear() + if self.filter_mode == "NONE": + return + file = IfcStore.get_file() + if self.filter_mode == "DECOMPOSITION": + if file.schema == "IFC2X3": + elements = file.by_type("IfcSpatialStructureElement") + else: + elements = file.by_type("IfcSpatialElement") + for element in elements: + new = self.filter_categories.add() + new.name = "{}/{}".format(element.is_a(), element.Name or "Unnamed") + new.ifc_definition_id = element.id() + new.total_elements = sum([len(r.RelatedElements) for r in element.ContainsElements]) + elif self.filter_mode == "IFC_CLASS": + for ifc_class in sorted(list(set([e.is_a() for e in file.by_type("IfcElement")]))): + new = self.filter_categories.add() + new.name = ifc_class + new.total_elements = len(file.by_type(ifc_class, include_subtypes=False)) + + class LibraryElement(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") is_declared: BoolProperty(name="Is Declared", default=False) + is_appended: BoolProperty(name="Is Appended", default=False) + + +class FilterCategory(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + is_selected: BoolProperty(name="Is Selected", default=False) + total_elements: IntProperty(name="Total Elements") + + +class Link(PropertyGroup): + name: StringProperty(name="Name") + is_loaded: BoolProperty(name="Is Loaded", default=False) class BIMProjectProperties(PropertyGroup): is_authoring: BoolProperty(name="Enable Authoring Mode", default=True) is_editing: BoolProperty(name="Is Editing", default=False) + is_loading: BoolProperty(name="Is Loading", default=False) mvd: StringProperty(name="MVD") author_name: StringProperty(name="Author") author_email: StringProperty(name="Author Email") @@ -32,3 +84,40 @@ class BIMProjectProperties(PropertyGroup): library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty) library_elements: CollectionProperty(name="Library Elements", type=LibraryElement) active_library_element_index: IntProperty(name="Active Library Element Index") + collection_mode: bpy.props.EnumProperty( + items=[ + ("DECOMPOSITION", "Decomposition", "Collections represent aggregates and spatial containers"), + ("SPATIAL_DECOMPOSITION", "Spatial Decomposition", "Collections represent spatial containers"), + ("IFC_CLASS", "IFC Class", "Collections represent IFC class"), + ("NONE", "None", "No collections are created"), + ], + name="Collection Mode", + ) + filter_mode: bpy.props.EnumProperty( + items=[ + ("NONE", "None", "No filtering is performed"), + ("DECOMPOSITION", "Decomposition", "Filter objects by decomposition"), + ("IFC_CLASS", "IFC Class", "Filter objects by class"), + ("WHITELIST", "Whitelist", "Filter objects using a custom whitelist query"), + ("BLACKLIST", "Blacklist", "Filter objects using a custom blacklist query"), + ], + name="Filter Mode", + update=update_filter_mode, + ) + filter_categories: CollectionProperty(name="Filter Categories", type=FilterCategory) + active_filter_category_index: IntProperty(name="Active Filter Category Index") + filter_query: StringProperty(name="Filter Query") + should_use_cpu_multiprocessing: BoolProperty(name="Import with CPU Multiprocessing", default=True) + should_merge_by_class: BoolProperty(name="Import and Merge by Class", default=False) + should_merge_by_material: BoolProperty(name="Import and Merge by Material", default=False) + should_merge_materials_by_colour: BoolProperty(name="Import and Merge Materials by Colour", default=False) + should_clean_mesh: BoolProperty(name="Import and Clean Mesh", default=True) + deflection_tolerance: FloatProperty(name="Import Deflection Tolerance", default=0.001) + angular_tolerance: FloatProperty(name="Import Angular Tolerance", default=0.5) + should_offset_model: BoolProperty(name="Import and Offset Model", default=False) + model_offset_coordinates: StringProperty(name="Model Offset Coordinates", default="0,0,0") + links: CollectionProperty(name="Links", type=Link) + active_link_index: IntProperty(name="Active Link Index") + + def get_library_element_index(self, lib_element): + return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element)) diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 435ff66d9a..a594ad5306 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore @@ -14,12 +32,56 @@ class BIM_PT_project(Panel): self.layout.use_property_decorate = False self.layout.use_property_split = True props = context.scene.BIMProperties + pprops = context.scene.BIMProjectProperties self.file = IfcStore.get_file() - if self.file or props.ifc_file: + if pprops.is_loading: + self.draw_load_ui(context) + elif self.file or props.ifc_file: self.draw_project_ui(context) else: self.draw_create_project_ui(context) + def draw_load_ui(self, context): + pprops = context.scene.BIMProjectProperties + row = self.layout.row() + row.prop(pprops, "collection_mode") + row = self.layout.row() + row.prop(pprops, "filter_mode") + if pprops.filter_mode in ["DECOMPOSITION", "IFC_CLASS"]: + self.layout.template_list( + "BIM_UL_filter_categories", + "", + pprops, + "filter_categories", + pprops, + "active_filter_category_index", + ) + elif pprops.filter_mode in ["WHITELIST", "BLACKLIST"]: + row = self.layout.row() + row.prop(pprops, "filter_query") + row = self.layout.row() + row.prop(pprops, "should_use_cpu_multiprocessing") + row = self.layout.row() + row.prop(pprops, "should_merge_by_class") + row = self.layout.row() + row.prop(pprops, "should_merge_by_material") + row = self.layout.row() + row.prop(pprops, "should_merge_materials_by_colour") + row = self.layout.row() + row.prop(pprops, "should_clean_mesh") + row = self.layout.row() + row.prop(pprops, "deflection_tolerance") + row = self.layout.row() + row.prop(pprops, "angular_tolerance") + row = self.layout.row() + row.prop(pprops, "should_offset_model") + row = self.layout.row() + row.prop(pprops, "model_offset_coordinates") + + row = self.layout.row(align=True) + row.operator("bim.load_project_elements") + row.operator("bim.unload_project", text="", icon="CANCEL") + def draw_project_ui(self, context): props = context.scene.BIMProperties pprops = context.scene.BIMProjectProperties @@ -91,11 +153,9 @@ class BIM_PT_project(Panel): row.prop(props, "area_unit", text="Area Unit") row = self.layout.row() row.prop(props, "volume_unit", text="Volume Unit") - row = self.layout.row() + row = self.layout.row(align=True) row.operator("bim.create_project") - if props.export_schema != "IFC2X3": - row = self.layout.row() - row.operator("bim.create_project_library") + row.operator("bim.load_project") class BIM_PT_project_library(Panel): @@ -139,6 +199,29 @@ class BIM_PT_project_library(Panel): ) +class BIM_PT_links(Panel): + bl_label = "IFC Links" + bl_idname = "BIM_PT_links" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + def draw(self, context): + self.props = context.scene.BIMProjectProperties + row = self.layout.row(align=True) + row.operator("bim.link_ifc") + if self.props.links: + self.layout.template_list( + "BIM_UL_links", + "", + self.props, + "links", + self.props, + "active_link_index", + ) + + class BIM_UL_library(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: @@ -159,5 +242,38 @@ class BIM_UL_library(UIList): op = row.operator("bim.assign_library_declaration", text="", icon="KEYFRAME", emboss=False) op.definition = item.ifc_definition_id if item.ifc_definition_id: - op = row.operator("bim.append_library_element", text="", icon="APPEND_BLEND") - op.definition = item.ifc_definition_id + if item.is_appended: + row.label(text="", icon="CHECKMARK") + else: + op = row.operator("bim.append_library_element", text="", icon="APPEND_BLEND") + op.definition = item.ifc_definition_id + op.prop_index = data.get_library_element_index(item) + + +class BIM_UL_filter_categories(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=f"{item.name} ({item.total_elements})") + row.prop( + item, + "is_selected", + icon="CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT", + text="", + emboss=False, + ) + + +class BIM_UL_links(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + if item.is_loaded: + op = row.operator("bim.unload_link", text="", icon="UNLINKED") + op.filepath = item.name + else: + op = row.operator("bim.load_link", text="", icon="LINKED") + op.filepath = item.name + op = row.operator("bim.unlink_ifc", text="", icon="X") + op.filepath = item.name diff --git a/src/blenderbim/blenderbim/bim/module/pset/__init__.py b/src/blenderbim/blenderbim/bim/module/pset/__init__.py index 7c5964f3ca..8c360d3aa8 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/pset/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -11,17 +29,35 @@ classes = ( operator.AddQto, operator.GuessQuantity, prop.PsetProperties, + prop.MaterialPsetProperties, + prop.TaskPsetProperties, + prop.ResourcePsetProperties, + prop.ProfilePsetProperties, + prop.WorkSchedulePsetProperties, ui.BIM_PT_object_psets, ui.BIM_PT_object_qtos, ui.BIM_PT_material_psets, + ui.BIM_PT_task_qtos, + ui.BIM_PT_resource_qtos, + ui.BIM_PT_resource_psets, + ui.BIM_PT_profile_psets, + ui.BIM_PT_work_schedule_psets, ) def register(): bpy.types.Object.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties) - bpy.types.Material.PsetProperties = bpy.props.PointerProperty(type=prop.PsetProperties) + bpy.types.Material.PsetProperties = bpy.props.PointerProperty(type=prop.MaterialPsetProperties) + bpy.types.Scene.TaskPsetProperties = bpy.props.PointerProperty(type=prop.TaskPsetProperties) + bpy.types.Scene.ResourcePsetProperties = bpy.props.PointerProperty(type=prop.ResourcePsetProperties) + bpy.types.Scene.ProfilePsetProperties = bpy.props.PointerProperty(type=prop.ProfilePsetProperties) + bpy.types.Scene.WorkSchedulePsetProperties = bpy.props.PointerProperty(type=prop.WorkSchedulePsetProperties) def unregister(): del bpy.types.Object.PsetProperties del bpy.types.Material.PsetProperties + del bpy.types.Scene.TaskPsetProperties + del bpy.types.Scene.ResourcePsetProperties + del bpy.types.Scene.ProfilePsetProperties + del bpy.types.Scene.WorkSchedulePsetProperties diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index a6a8b7b3f2..1747d047d4 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell.api @@ -7,16 +25,57 @@ import ifcopenshell.util.attribute import blenderbim.bim.schema from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.pset.data import Data +from ifcopenshell.api.cost.data import Data as CostData from blenderbim.bim.module.pset.qto_calculator import QtoCalculator +def get_pset_props(context, obj, obj_type): + if obj_type == "Object": + obj = bpy.data.objects.get(obj) + return obj.PsetProperties + elif obj_type == "Material": + obj = bpy.data.materials.get(obj) + return obj.PsetProperties + elif obj_type == "Task": + return context.scene.TaskPsetProperties + elif obj_type == "Resource": + return context.scene.ResourcePsetProperties + elif obj_type == "Profile": + return context.scene.ProfilePsetProperties + elif obj_type == "WorkSchedule": + return context.scene.WorkSchedulePsetProperties + + +def get_pset_obj_ifc_definition_id(context, obj, obj_type): + if obj_type == "Object": + obj = bpy.data.objects.get(obj) + return obj.BIMObjectProperties.ifc_definition_id + elif obj_type == "Material": + obj = bpy.data.materials.get(obj) + return obj.BIMObjectProperties.ifc_definition_id + elif obj_type == "Task": + return context.scene.BIMTaskTreeProperties.tasks[ + context.scene.BIMWorkScheduleProperties.active_task_index + ].ifc_definition_id + elif obj_type == "Resource": + return context.scene.BIMResourceTreeProperties.resources[ + context.scene.BIMResourceProperties.active_resource_index + ].ifc_definition_id + elif obj_type == "Profile": + return context.scene.BIMProfileProperties.profiles[ + context.scene.BIMProfileProperties.active_profile_index + ].ifc_definition_id + elif obj_type == "WorkSchedule": + return context.scene.BIMWorkScheduleProperties.active_work_schedule_id + + class TogglePsetExpansion(bpy.types.Operator): bl_idname = "bim.toggle_pset_expansion" bl_label = "Toggle Pset Expansion" pset_id: bpy.props.IntProperty() def execute(self, context): - obj = bpy.context.active_object + obj = context.active_object data = Data.psets if self.pset_id in Data.psets else Data.qtos data[self.pset_id]["is_expanded"] = not data[self.pset_id]["is_expanded"] return {"FINISHED"} @@ -25,19 +84,15 @@ class TogglePsetExpansion(bpy.types.Operator): class EnablePsetEditing(bpy.types.Operator): bl_idname = "bim.enable_pset_editing" bl_label = "Enable Pset Editing" + bl_options = {"REGISTER", "UNDO"} pset_id: bpy.props.IntProperty() obj: bpy.props.StringProperty() obj_type: bpy.props.StringProperty() def execute(self, context): - if self.obj_type == "Object": - obj = bpy.data.objects.get(self.obj) - elif self.obj_type == "Material": - obj = bpy.data.materials.get(self.obj) - self.props = obj.PsetProperties + self.props = get_pset_props(context, self.obj, self.obj_type) - while len(self.props.properties) > 0: - self.props.properties.remove(0) + self.props.properties.clear() data = Data.psets if self.pset_id in Data.psets else Data.qtos pset_data = data[self.pset_id] @@ -65,8 +120,9 @@ class EnablePsetEditing(bpy.types.Operator): IfcStore.get_schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel") ) except: - # TODO: Occurs if the data type is something that exists in IFC4 and not in IFC2X3. To fully fix - # this we need to generate the IFC2X3 pset template definitions. + # TODO: Occurs if the data type is something that exists in + # IFC4 and not in IFC2X3. To fully fix this we need to + # generate the IFC2X3 pset template definitions. continue elif prop_template.TemplateType == "P_ENUMERATEDVALUE": data_type = "enum" @@ -81,6 +137,7 @@ class EnablePsetEditing(bpy.types.Operator): new = self.props.properties.add() new.name = prop_template.Name new.is_null = data.get(prop_template.Name, None) is None + new.is_optional = True new.data_type = data_type if data_type == "string": @@ -101,45 +158,23 @@ class EnablePsetEditing(bpy.types.Operator): prop = Data.properties[prop_id] value = prop["NominalValue"] - if isinstance(value, str): - data_type = "string" - elif isinstance(value, float): - data_type = "float" - elif isinstance(value, bool): - data_type = "boolean" - elif isinstance(value, int): - data_type = "integer" - else: - data_type = "string" - value = str(value) - new = self.props.properties.add() + new.set_value(value) new.name = prop["Name"] - new.is_null = prop["NominalValue"] is None - new.data_type = data_type - - if data_type == "string": - new.string_value = "" if new.is_null else value - elif data_type == "integer": - new.int_value = 0 if new.is_null else value - elif data_type == "float": - new.float_value = 0.0 if new.is_null else value - elif data_type == "boolean": - new.bool_value = False if new.is_null else value + new.is_null = value is None + new.is_optional = True + new.set_value(new.get_value_default() if new.is_null else value) class DisablePsetEditing(bpy.types.Operator): bl_idname = "bim.disable_pset_editing" bl_label = "Disable Pset Editing" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() obj_type: bpy.props.StringProperty() def execute(self, context): - if self.obj_type == "Object": - obj = bpy.data.objects.get(self.obj) - elif self.obj_type == "Material": - obj = bpy.data.materials.get(self.obj) - props = obj.PsetProperties + props = get_pset_props(context, self.obj, self.obj_type) props.active_pset_id = 0 return {"FINISHED"} @@ -147,19 +182,19 @@ class DisablePsetEditing(bpy.types.Operator): class EditPset(bpy.types.Operator): bl_idname = "bim.edit_pset" bl_label = "Edit Pset" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() obj_type: bpy.props.StringProperty() pset_id: bpy.props.IntProperty() properties: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() - if self.obj_type == "Object": - obj = bpy.data.objects.get(self.obj) - elif self.obj_type == "Material": - obj = bpy.data.materials.get(self.obj) - oprops = obj.BIMObjectProperties - props = obj.PsetProperties + props = get_pset_props(context, self.obj, self.obj_type) + ifc_definition_id = get_pset_obj_ifc_definition_id(context, self.obj, self.obj_type) properties = {} pset_id = self.pset_id or props.active_pset_id @@ -168,18 +203,7 @@ class EditPset(bpy.types.Operator): else: data = Data.psets if pset_id in Data.psets else Data.qtos for prop in props.properties: - if prop.is_null: - properties[prop.name] = None - elif prop.data_type == "string": - properties[prop.name] = prop.string_value - elif prop.data_type == "boolean": - properties[prop.name] = prop.bool_value - elif prop.data_type == "integer": - properties[prop.name] = prop.int_value - elif prop.data_type == "float": - properties[prop.name] = prop.float_value - elif prop.data_type == "enum": - properties[prop.name] = prop.enum_value + properties[prop.name] = prop.get_value() if pset_id in Data.psets: ifcopenshell.api.run( @@ -189,6 +213,7 @@ class EditPset(bpy.types.Operator): "pset": self.file.by_id(pset_id), "name": props.active_pset_name, "properties": properties, + "pset_template": blenderbim.bim.schema.ifc.psetqto.get_by_name(props.active_pset_name), }, ) else: @@ -204,7 +229,8 @@ class EditPset(bpy.types.Operator): "properties": properties, }, ) - Data.load(IfcStore.get_file(), oprops.ifc_definition_id) + CostData.purge() + Data.load(IfcStore.get_file(), ifc_definition_id) bpy.ops.bim.disable_pset_editing(obj=self.obj, obj_type=self.obj_type) return {"FINISHED"} @@ -212,109 +238,108 @@ class EditPset(bpy.types.Operator): class RemovePset(bpy.types.Operator): bl_idname = "bim.remove_pset" bl_label = "Remove Pset" + bl_options = {"REGISTER", "UNDO"} pset_id: bpy.props.IntProperty() obj: bpy.props.StringProperty() obj_type: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() - if self.obj_type == "Object": - obj = bpy.data.objects.get(self.obj) - elif self.obj_type == "Material": - obj = bpy.data.materials.get(self.obj) - props = obj.BIMObjectProperties + props = get_pset_props(context, self.obj, self.obj_type) + ifc_definition_id = get_pset_obj_ifc_definition_id(context, self.obj, self.obj_type) ifcopenshell.api.run( "pset.remove_pset", self.file, **{ - "product": self.file.by_id(props.ifc_definition_id), + "product": self.file.by_id(ifc_definition_id), "pset": self.file.by_id(self.pset_id), }, ) - Data.load(IfcStore.get_file(), props.ifc_definition_id) + Data.load(IfcStore.get_file(), ifc_definition_id) return {"FINISHED"} class AddPset(bpy.types.Operator): bl_idname = "bim.add_pset" bl_label = "Add Pset" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() obj_type: bpy.props.StringProperty() - pset_name: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() - if self.obj_type == "Object": - obj = bpy.data.objects.get(self.obj) - elif self.obj_type == "Material": - obj = bpy.data.materials.get(self.obj) - oprops = obj.BIMObjectProperties - props = obj.PsetProperties + return IfcStore.execute_ifc_operator(self, context) - if self.pset_name: - pset_name = self.pset_name - elif self.obj_type == "Object": - pset_name = props.pset_name - elif self.obj_type == "Material": - pset_name = props.material_pset_name + def _execute(self, context): + self.file = IfcStore.get_file() + props = get_pset_props(context, self.obj, self.obj_type) + ifc_definition_id = get_pset_obj_ifc_definition_id(context, self.obj, self.obj_type) ifcopenshell.api.run( "pset.add_pset", self.file, **{ - "product": self.file.by_id(oprops.ifc_definition_id), - "name": pset_name, + "product": self.file.by_id(ifc_definition_id), + "name": props.pset_name, }, ) - Data.load(IfcStore.get_file(), oprops.ifc_definition_id) + Data.load(IfcStore.get_file(), ifc_definition_id) return {"FINISHED"} class AddQto(bpy.types.Operator): bl_idname = "bim.add_qto" bl_label = "Add Qto" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + obj_type: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() - obj = bpy.context.active_object - oprops = obj.BIMObjectProperties - props = obj.PsetProperties + props = get_pset_props(context, self.obj, self.obj_type) + ifc_definition_id = get_pset_obj_ifc_definition_id(context, self.obj, self.obj_type) ifcopenshell.api.run( "pset.add_qto", self.file, **{ - "product": self.file.by_id(oprops.ifc_definition_id), + "product": self.file.by_id(ifc_definition_id), "name": props.qto_name, }, ) - Data.load(IfcStore.get_file(), oprops.ifc_definition_id) + Data.load(IfcStore.get_file(), ifc_definition_id) return {"FINISHED"} class GuessQuantity(bpy.types.Operator): bl_idname = "bim.guess_quantity" bl_label = "Guess Quantity" + bl_options = {"REGISTER", "UNDO"} prop: bpy.props.StringProperty() def execute(self, context): self.qto_calculator = QtoCalculator() - obj = bpy.context.active_object + obj = context.active_object prop = obj.PsetProperties.properties.get(self.prop) - prop.float_value = self.guess_quantity(obj) + prop.float_value = self.guess_quantity(obj, context) return {"FINISHED"} - def guess_quantity(self, obj): + def guess_quantity(self, obj, context): quantity = self.qto_calculator.guess_quantity(self.prop, [p.name for p in obj.PsetProperties.properties], obj) if "area" in self.prop.lower(): - if bpy.context.scene.BIMProperties.area_unit: - prefix, name = self.get_prefix_name(bpy.context.scene.BIMProperties.area_unit) + if context.scene.BIMProperties.area_unit: + prefix, name = self.get_prefix_name(context.scene.BIMProperties.area_unit) quantity = ifcopenshell.util.unit.convert(quantity, None, "SQUARE_METRE", prefix, name) elif "volume" in self.prop.lower(): - if bpy.context.scene.BIMProperties.volume_unit: - prefix, name = self.get_prefix_name(bpy.context.scene.BIMProperties.volume_unit) + if context.scene.BIMProperties.volume_unit: + prefix, name = self.get_prefix_name(context.scene.BIMProperties.volume_unit) quantity = ifcopenshell.util.unit.convert(quantity, None, "CUBIC_METRE", prefix, name) else: - prefix, name = self.get_blender_prefix_name() + prefix, name = self.get_blender_prefix_name(context) quantity = ifcopenshell.util.unit.convert(quantity, None, "METRE", prefix, name) return round(quantity, 3) @@ -323,13 +348,14 @@ class GuessQuantity(bpy.types.Operator): return value.split("/") return None, value - def get_blender_prefix_name(self): - if bpy.context.scene.unit_settings.system == "IMPERIAL": - if bpy.context.scene.unit_settings.length_unit == "INCHES": + def get_blender_prefix_name(self, context): + unit_settings = context.scene.unit_settings + if unit_settings.system == "IMPERIAL": + if unit_settings.length_unit == "INCHES": return None, "inch" - elif bpy.context.scene.unit_settings.length_unit == "FEET": + elif unit_settings.length_unit == "FEET": return None, "foot" - elif bpy.context.scene.unit_settings.system == "METRIC": - if bpy.context.scene.unit_settings.length_unit == "METERS": + elif unit_settings.system == "METRIC": + if unit_settings.length_unit == "METERS": return None, "METRE" - return bpy.context.scene.unit_settings.length_unit[0 : -len("METERS")], "METRE" + return unit_settings.length_unit[0 : -len("METERS")], "METRE" diff --git a/src/blenderbim/blenderbim/bim/module/pset/prop.py b/src/blenderbim/blenderbim/bim/module/pset/prop.py index d4dcbb75c9..f47cadd87e 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/prop.py +++ b/src/blenderbim/blenderbim/bim/module/pset/prop.py @@ -1,7 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy +import blenderbim.bim.schema from blenderbim.bim.prop import Attribute from ifcopenshell.api.pset.data import Data -import blenderbim.bim.schema +from blenderbim.bim.ifc import IfcStore from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -29,16 +48,15 @@ def purge(): def getPsetNames(self, context): global psetnames obj = context.active_object - if "/" in obj.name: - ifc_class = obj.name.split("/")[0] - if ifc_class not in psetnames: - psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True) - psetnames[ifc_class] = [(p.Name, p.Name, "") for p in psets] - assigned_names = [ - Data.psets[p]["Name"] for p in Data.products[obj.BIMObjectProperties.ifc_definition_id]["psets"] - ] - return [p for p in psetnames[ifc_class] if p[0] not in assigned_names] - return [] + if not obj.BIMObjectProperties.ifc_definition_id: + return [] + element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + ifc_class = element.is_a() + if ifc_class not in psetnames: + psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True) + psetnames[ifc_class] = [(p.Name, p.Name, "") for p in psets] + assigned_names = [Data.psets[p]["Name"] for p in Data.products[obj.BIMObjectProperties.ifc_definition_id]["psets"]] + return [p for p in psetnames[ifc_class] if p[0] not in assigned_names] def getMaterialPsetNames(self, context): @@ -50,6 +68,56 @@ def getMaterialPsetNames(self, context): return psetnames[ifc_class] +def getTaskQtoNames(self, context): + global qtonames + ifc_class = "IfcTask" + if ifc_class not in qtonames: + psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True) + qtonames[ifc_class] = [(p.Name, p.Name, "") for p in psets] + return qtonames[ifc_class] + + +def getResourcePsetNames(self, context): + global psetnames + rprops = context.scene.BIMResourceProperties + rtprops = context.scene.BIMResourceTreeProperties + ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() + if ifc_class not in psetnames: + psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True) + psetnames[ifc_class] = [(p.Name, p.Name, "") for p in psets] + return psetnames[ifc_class] + + +def getResourceQtoNames(self, context): + global qtonames + rprops = context.scene.BIMResourceProperties + rtprops = context.scene.BIMResourceTreeProperties + ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() + if ifc_class not in qtonames: + psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True) + qtonames[ifc_class] = [(p.Name, p.Name, "") for p in psets] + return qtonames[ifc_class] + + +def getProfilePsetNames(self, context): + global psetnames + pprops = context.scene.BIMProfileProperties + ifc_class = IfcStore.get_file().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a() + if ifc_class not in psetnames: + psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True) + psetnames[ifc_class] = [(p.Name, p.Name, "") for p in psets] + return psetnames[ifc_class] + + +def getWorkSchedulePsetNames(self, context): + global psetnames + ifc_class = "IfcWorkSchedule" + if ifc_class not in psetnames: + psets = blenderbim.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True) + psetnames[ifc_class] = [(p.Name, p.Name, "") for p in psets] + return psetnames[ifc_class] + + def getQtoNames(self, context): global qtonames if "/" in context.active_object.name: @@ -67,4 +135,39 @@ class PsetProperties(PropertyGroup): properties: CollectionProperty(name="Properties", type=Attribute) pset_name: EnumProperty(items=getPsetNames, name="Pset Name") qto_name: EnumProperty(items=getQtoNames, name="Qto Name") - material_pset_name: EnumProperty(items=getMaterialPsetNames, name="Pset Name") + + +class MaterialPsetProperties(PropertyGroup): + active_pset_id: IntProperty(name="Active Pset ID") + active_pset_name: StringProperty(name="Pset Name") + properties: CollectionProperty(name="Properties", type=Attribute) + pset_name: EnumProperty(items=getMaterialPsetNames, name="Pset Name") + + +class TaskPsetProperties(PropertyGroup): + active_pset_id: IntProperty(name="Active Pset ID") + active_pset_name: StringProperty(name="Pset Name") + properties: CollectionProperty(name="Properties", type=Attribute) + qto_name: EnumProperty(items=getTaskQtoNames, name="Qto Name") + + +class ResourcePsetProperties(PropertyGroup): + active_pset_id: IntProperty(name="Active Pset ID") + active_pset_name: StringProperty(name="Pset Name") + properties: CollectionProperty(name="Properties", type=Attribute) + pset_name: EnumProperty(items=getResourcePsetNames, name="Pset Name") + qto_name: EnumProperty(items=getResourceQtoNames, name="Qto Name") + + +class ProfilePsetProperties(PropertyGroup): + active_pset_id: IntProperty(name="Active Pset ID") + active_pset_name: StringProperty(name="Pset Name") + properties: CollectionProperty(name="Properties", type=Attribute) + pset_name: EnumProperty(items=getProfilePsetNames, name="Pset Name") + + +class WorkSchedulePsetProperties(PropertyGroup): + active_pset_id: IntProperty(name="Active Pset ID") + active_pset_name: StringProperty(name="Pset Name") + properties: CollectionProperty(name="Properties", type=Attribute) + pset_name: EnumProperty(items=getWorkSchedulePsetNames, name="Pset Name") diff --git a/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py b/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py index b1a056caeb..8411267ed2 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/qto_calculator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from mathutils import Vector @@ -143,7 +161,11 @@ class QtoCalculator: else: tf_tris = ( (me.vertices[tfv[0]], me.vertices[tfv[1]], me.vertices[tfv[2]]), - (me.vertices[tfv[2]], me.vertices[tfv[3]], me.vertices[tfv[0]],), + ( + me.vertices[tfv[2]], + me.vertices[tfv[3]], + me.vertices[tfv[0]], + ), ) for tf_iter in tf_tris: diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index f3f257b8bb..90f5791adf 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -1,6 +1,33 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel from ifcopenshell.api.pset.data import Data from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attribute + + +def get_active_pset_obj_name(context, obj_type): + if obj_type == "Object": + return context.active_object.name + elif obj_type == "Material": + return context.active_object.active_material.name + return "" def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): @@ -10,29 +37,30 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): pset["is_expanded"] = True icon = "TRIA_DOWN" if pset["is_expanded"] else "TRIA_RIGHT" row.operator("bim.toggle_pset_expansion", icon=icon, text="", emboss=False).pset_id = pset_id + obj_name = get_active_pset_obj_name(context, obj_type) if not props.active_pset_id: row.label(text=pset["Name"], icon="COPY_ID") op = row.operator("bim.enable_pset_editing", icon="GREASEPENCIL", text="") op.pset_id = pset_id - op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name + op.obj = obj_name op.obj_type = obj_type op = row.operator("bim.remove_pset", icon="X", text="") op.pset_id = pset_id - op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name + op.obj = obj_name op.obj_type = obj_type elif props.active_pset_id != pset_id: row.label(text=pset["Name"], icon="COPY_ID") op = row.operator("bim.remove_pset", icon="X", text="") op.pset_id = pset_id - op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name + op.obj = obj_name op.obj_type = obj_type elif props.active_pset_id == pset_id: row.prop(props, "active_pset_name", icon="COPY_ID", text="") op = row.operator("bim.edit_pset", icon="CHECKMARK", text="") - op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name + op.obj = obj_name op.obj_type = obj_type op = row.operator("bim.disable_pset_editing", icon="CANCEL", text="") - op.obj = context.active_object.name if obj_type == "Object" else context.active_object.active_material.name + op.obj = obj_name op.obj_type = obj_type if pset["is_expanded"]: if props.active_pset_id == pset_id: @@ -59,17 +87,7 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): def draw_psetqto_editable_ui(box, props, prop): row = box.row(align=True) - if prop.data_type == "string": - row.prop(prop, "string_value", text=prop.name) - elif prop.data_type == "integer": - row.prop(prop, "int_value", text=prop.name) - elif prop.data_type == "float": - row.prop(prop, "float_value", text=prop.name) - elif prop.data_type == "boolean": - row.prop(prop, "bool_value", text=prop.name) - elif prop.data_type == "enum": - row.prop(prop, "enum_value", text=prop.name) - row.prop(prop, "is_null", icon="RADIOBUT_OFF" if prop.is_null else "RADIOBUT_ON", text="") + draw_attribute(prop, row) if ( "length" in prop.name.lower() or "width" in prop.name.lower() @@ -122,8 +140,8 @@ class BIM_PT_object_psets(Panel): op.obj = context.active_object.name op.obj_type = "Object" - for pset_id in Data.products[oprops.ifc_definition_id]["psets"]: - pset = Data.psets[pset_id] + psets = [(pset_id, Data.psets[pset_id]) for pset_id in Data.products[oprops.ifc_definition_id]["psets"]] + for pset_id, pset in sorted(psets, key=lambda v: v[1]["Name"]): draw_psetqto_ui(context, pset_id, pset, props, self.layout, "Object") # TODO reimplement. See #1222. @@ -163,10 +181,12 @@ class BIM_PT_object_qtos(Panel): Data.load(IfcStore.get_file(), oprops.ifc_definition_id) row = self.layout.row(align=True) row.prop(props, "qto_name", text="") - row.operator("bim.add_qto", icon="ADD", text="") + op = row.operator("bim.add_qto", icon="ADD", text="") + op.obj = context.active_object.name + op.obj_type = "Object" - for qto_id in Data.products[oprops.ifc_definition_id]["qtos"]: - qto = Data.qtos[qto_id] + qtos = [(qto_id, Data.qtos[qto_id]) for qto_id in Data.products[oprops.ifc_definition_id]["qtos"]] + for qto_id, qto in sorted(qtos, key=lambda v: v[1]["Name"]): draw_psetqto_ui(context, qto_id, qto, props, self.layout, "Object") @@ -186,7 +206,8 @@ class BIM_PT_material_psets(Panel): props = context.active_object.active_material.BIMObjectProperties if not props.ifc_definition_id: return False - if IfcStore.get_file().schema == "IFC2X3": + file = IfcStore.get_file() + if not file or file.schema == "IFC2X3": return False # We don't support material psets in IFC2X3 because they suck if props.ifc_definition_id not in Data.products: Data.load(IfcStore.get_file(), props.ifc_definition_id) @@ -202,11 +223,180 @@ class BIM_PT_material_psets(Panel): if oprops.ifc_definition_id not in Data.products: Data.load(IfcStore.get_file(), oprops.ifc_definition_id) row = self.layout.row(align=True) - row.prop(props, "material_pset_name", text="") + row.prop(props, "pset_name", text="") op = row.operator("bim.add_pset", icon="ADD", text="") op.obj = context.active_object.active_material.name op.obj_type = "Material" - for pset_id in Data.products[oprops.ifc_definition_id]["psets"]: - pset = Data.psets[pset_id] + psets = [(pset_id, Data.psets[pset_id]) for pset_id in Data.products[oprops.ifc_definition_id]["psets"]] + for pset_id, pset in sorted(psets, key=lambda v: v[1]["Name"]): draw_psetqto_ui(context, pset_id, pset, props, self.layout, "Material") + + +class BIM_PT_task_qtos(Panel): + bl_label = "IFC Task Quantity Sets" + bl_idname = "BIM_PT_task_qtos" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_work_schedules" + + @classmethod + def poll(cls, context): + props = context.scene.BIMWorkScheduleProperties + if not props.active_work_schedule_id: + return False + total_tasks = len(context.scene.BIMTaskTreeProperties.tasks) + if total_tasks > 0 and props.active_task_index < total_tasks: + return True + return False + + def draw(self, context): + props = context.scene.TaskPsetProperties + wprops = context.scene.BIMWorkScheduleProperties + tprops = context.scene.BIMTaskTreeProperties + ifc_definition_id = tprops.tasks[wprops.active_task_index].ifc_definition_id + if ifc_definition_id not in Data.products: + Data.load(IfcStore.get_file(), ifc_definition_id) + row = self.layout.row(align=True) + row.prop(props, "qto_name", text="") + op = row.operator("bim.add_qto", icon="ADD", text="") + op.obj_type = "Task" + + qtos = [(qto_id, Data.qtos[qto_id]) for qto_id in Data.products[ifc_definition_id]["qtos"]] + for qto_id, qto in sorted(qtos, key=lambda v: v[1]["Name"]): + draw_psetqto_ui(context, qto_id, qto, props, self.layout, "Task") + + +class BIM_PT_resource_qtos(Panel): + bl_label = "IFC Resource Quantity Sets" + bl_idname = "BIM_PT_resource_qtos" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_resources" + + @classmethod + def poll(cls, context): + props = context.scene.BIMResourceProperties + total_resources = len(context.scene.BIMResourceTreeProperties.resources) + if total_resources > 0 and props.active_resource_index < total_resources: + return True + return False + + def draw(self, context): + props = context.scene.ResourcePsetProperties + rprops = context.scene.BIMResourceProperties + rtprops = context.scene.BIMResourceTreeProperties + ifc_definition_id = rtprops.resources[rprops.active_resource_index].ifc_definition_id + if ifc_definition_id not in Data.products: + Data.load(IfcStore.get_file(), ifc_definition_id) + row = self.layout.row(align=True) + row.prop(props, "qto_name", text="") + op = row.operator("bim.add_qto", icon="ADD", text="") + op.obj_type = "Resource" + + qtos = [(qto_id, Data.qtos[qto_id]) for qto_id in Data.products[ifc_definition_id]["qtos"]] + for qto_id, qto in sorted(qtos, key=lambda v: v[1]["Name"]): + draw_psetqto_ui(context, qto_id, qto, props, self.layout, "Resource") + + +class BIM_PT_resource_psets(Panel): + bl_label = "IFC Resource Property Sets" + bl_idname = "BIM_PT_resource_psets" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_resources" + + @classmethod + def poll(cls, context): + props = context.scene.BIMResourceProperties + total_resources = len(context.scene.BIMResourceTreeProperties.resources) + if total_resources > 0 and props.active_resource_index < total_resources: + return True + return False + + def draw(self, context): + props = context.scene.ResourcePsetProperties + rprops = context.scene.BIMResourceProperties + rtprops = context.scene.BIMResourceTreeProperties + ifc_definition_id = rtprops.resources[rprops.active_resource_index].ifc_definition_id + if ifc_definition_id not in Data.products: + Data.load(IfcStore.get_file(), ifc_definition_id) + row = self.layout.row(align=True) + row.prop(props, "pset_name", text="") + op = row.operator("bim.add_pset", icon="ADD", text="") + op.obj_type = "Resource" + + psets = [(pset_id, Data.psets[pset_id]) for pset_id in Data.products[ifc_definition_id]["psets"]] + for pset_id, pset in sorted(psets, key=lambda v: v[1]["Name"]): + draw_psetqto_ui(context, pset_id, pset, props, self.layout, "Resource") + + +class BIM_PT_profile_psets(Panel): + bl_label = "IFC Profile Property Sets" + bl_idname = "BIM_PT_profile_psets" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_profiles" + + @classmethod + def poll(cls, context): + props = context.scene.BIMProfileProperties + if not props.is_editing: + return False + total_profiles = len(context.scene.BIMProfileProperties.profiles) + if total_profiles > 0 and props.active_profile_index < total_profiles: + return True + return False + + def draw(self, context): + props = context.scene.ProfilePsetProperties + pprops = context.scene.BIMProfileProperties + ifc_definition_id = pprops.profiles[pprops.active_profile_index].ifc_definition_id + if ifc_definition_id not in Data.products: + Data.load(IfcStore.get_file(), ifc_definition_id) + row = self.layout.row(align=True) + row.prop(props, "pset_name", text="") + op = row.operator("bim.add_pset", icon="ADD", text="") + op.obj_type = "Profile" + + psets = [(pset_id, Data.psets[pset_id]) for pset_id in Data.products[ifc_definition_id]["psets"]] + for pset_id, pset in sorted(psets, key=lambda v: v[1]["Name"]): + draw_psetqto_ui(context, pset_id, pset, props, self.layout, "Profile") + + +class BIM_PT_work_schedule_psets(Panel): + bl_label = "IFC Work Schedule Property Sets" + bl_idname = "BIM_PT_work_schedule_psets" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_work_schedules" + + @classmethod + def poll(cls, context): + if not context.scene.BIMWorkScheduleProperties.active_work_schedule_id: + return False + return True + + def draw(self, context): + props = context.scene.WorkSchedulePsetProperties + ifc_definition_id = context.scene.BIMWorkScheduleProperties.active_work_schedule_id + if ifc_definition_id not in Data.products: + Data.load(IfcStore.get_file(), ifc_definition_id) + row = self.layout.row(align=True) + row.prop(props, "pset_name", text="") + op = row.operator("bim.add_pset", icon="ADD", text="") + op.obj_type = "WorkSchedule" + + psets = [(pset_id, Data.psets[pset_id]) for pset_id in Data.products[ifc_definition_id]["psets"]] + for pset_id, pset in sorted(psets, key=lambda v: v[1]["Name"]): + draw_psetqto_ui(context, pset_id, pset, props, self.layout, "WorkSchedule") diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py b/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py index e62901c133..e4e5591da4 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/operator.py b/src/blenderbim/blenderbim/bim/module/pset_template/operator.py index d83737be2e..b9e86ba16a 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/operator.py @@ -1,6 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell import ifcopenshell.api +import blenderbim.bim.schema +import blenderbim.bim.handler from blenderbim.bim.module.pset_template.prop import updatePsetTemplateFiles, updatePsetTemplates from ifcopenshell.api.pset_template.data import Data from blenderbim.bim.ifc import IfcStore @@ -9,34 +29,69 @@ from blenderbim.bim.ifc import IfcStore class AddPsetTemplate(bpy.types.Operator): bl_idname = "bim.add_pset_template" bl_label = "Add Pset Template" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + IfcStore.begin_transaction(self) + IfcStore.pset_template_file.begin_transaction() + result = self._execute(context) + IfcStore.pset_template_file.end_transaction() + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): props = context.scene.BIMPsetTemplateProperties ifcopenshell.api.run("pset_template.add_pset_template", IfcStore.pset_template_file) Data.load(IfcStore.pset_template_file) updatePsetTemplates(self, context) return {"FINISHED"} + def rollback(self, data): + IfcStore.pset_template_file.undo() + + def commit(self, data): + IfcStore.pset_template_file.redo() + class RemovePsetTemplate(bpy.types.Operator): bl_idname = "bim.remove_pset_template" bl_label = "Remove Pset Template" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + IfcStore.begin_transaction(self) + IfcStore.pset_template_file.begin_transaction() + result = self._execute(context) + IfcStore.pset_template_file.end_transaction() + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): props = context.scene.BIMPsetTemplateProperties if props.active_pset_template_id == int(props.pset_templates): bpy.ops.bim.disable_editing_pset_template() - ifcopenshell.api.run("pset_template.remove_pset_template", IfcStore.pset_template_file, **{ - "pset_template": IfcStore.pset_template_file.by_id(int(props.pset_templates)) - }) + ifcopenshell.api.run( + "pset_template.remove_pset_template", + IfcStore.pset_template_file, + **{"pset_template": IfcStore.pset_template_file.by_id(int(props.pset_templates))} + ) Data.load(IfcStore.pset_template_file) updatePsetTemplates(self, context) return {"FINISHED"} + def rollback(self, data): + IfcStore.pset_template_file.undo() + + def commit(self, data): + IfcStore.pset_template_file.redo() + class EnableEditingPsetTemplate(bpy.types.Operator): bl_idname = "bim.enable_editing_pset_template" bl_label = "Enable Editing Pset Template" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): props = context.scene.BIMPsetTemplateProperties @@ -53,6 +108,7 @@ class EnableEditingPsetTemplate(bpy.types.Operator): class DisableEditingPsetTemplate(bpy.types.Operator): bl_idname = "bim.disable_editing_pset_template" bl_label = "Disable Editing Pset Template" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): props = context.scene.BIMPsetTemplateProperties @@ -63,6 +119,7 @@ class DisableEditingPsetTemplate(bpy.types.Operator): class EnableEditingPropTemplate(bpy.types.Operator): bl_idname = "bim.enable_editing_prop_template" bl_label = "Enable Editing Prop Template" + bl_options = {"REGISTER", "UNDO"} prop_template: bpy.props.IntProperty() def execute(self, context): @@ -78,6 +135,7 @@ class EnableEditingPropTemplate(bpy.types.Operator): class DisableEditingPropTemplate(bpy.types.Operator): bl_idname = "bim.disable_editing_prop_template" bl_label = "Disable Editing Prop Template" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): props = context.scene.BIMPsetTemplateProperties @@ -88,23 +146,43 @@ class DisableEditingPropTemplate(bpy.types.Operator): class EditPsetTemplate(bpy.types.Operator): bl_idname = "bim.edit_pset_template" bl_label = "Edit Pset Template" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + IfcStore.begin_transaction(self) + IfcStore.pset_template_file.begin_transaction() + result = self._execute(context) + IfcStore.pset_template_file.end_transaction() + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): props = context.scene.BIMPsetTemplateProperties - ifcopenshell.api.run("pset_template.edit_pset_template", IfcStore.pset_template_file, **{ - "pset_template": IfcStore.pset_template_file.by_id(props.active_pset_template_id), - "attributes": { - "Name": props.active_pset_template.name, - "Description": props.active_pset_template.description, - "TemplateType": props.active_pset_template.template_type, - "ApplicableEntity": props.active_pset_template.applicable_entity, + ifcopenshell.api.run( + "pset_template.edit_pset_template", + IfcStore.pset_template_file, + **{ + "pset_template": IfcStore.pset_template_file.by_id(props.active_pset_template_id), + "attributes": { + "Name": props.active_pset_template.name, + "Description": props.active_pset_template.description, + "TemplateType": props.active_pset_template.template_type, + "ApplicableEntity": props.active_pset_template.applicable_entity, + }, } - }) + ) Data.load(IfcStore.pset_template_file) updatePsetTemplates(self, context) bpy.ops.bim.disable_editing_pset_template() return {"FINISHED"} + def rollback(self, data): + IfcStore.pset_template_file.undo() + + def commit(self, data): + IfcStore.pset_template_file.redo() + class SavePsetTemplateFile(bpy.types.Operator): bl_idname = "bim.save_pset_template_file" @@ -112,51 +190,109 @@ class SavePsetTemplateFile(bpy.types.Operator): def execute(self, context): IfcStore.pset_template_file.write(IfcStore.pset_template_path) + blenderbim.bim.handler.purge_module_data() + blenderbim.bim.schema.reload() return {"FINISHED"} class AddPropTemplate(bpy.types.Operator): bl_idname = "bim.add_prop_template" bl_label = "Add Prop Template" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + IfcStore.begin_transaction(self) + IfcStore.pset_template_file.begin_transaction() + result = self._execute(context) + IfcStore.pset_template_file.end_transaction() + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): props = context.scene.BIMPsetTemplateProperties pset_template_id = props.active_pset_template_id or int(props.pset_templates) - ifcopenshell.api.run("pset_template.add_prop_template", IfcStore.pset_template_file, **{ - "pset_template": IfcStore.pset_template_file.by_id(pset_template_id) - }) + ifcopenshell.api.run( + "pset_template.add_prop_template", + IfcStore.pset_template_file, + **{"pset_template": IfcStore.pset_template_file.by_id(pset_template_id)} + ) Data.load(IfcStore.pset_template_file) return {"FINISHED"} + def rollback(self, data): + IfcStore.pset_template_file.undo() + + def commit(self, data): + IfcStore.pset_template_file.redo() + class RemovePropTemplate(bpy.types.Operator): bl_idname = "bim.remove_prop_template" bl_label = "Remove Prop Template" + bl_options = {"REGISTER", "UNDO"} prop_template: bpy.props.IntProperty() def execute(self, context): + IfcStore.begin_transaction(self) + IfcStore.pset_template_file.begin_transaction() + result = self._execute(context) + IfcStore.pset_template_file.end_transaction() + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): props = context.scene.BIMPsetTemplateProperties - ifcopenshell.api.run("pset_template.remove_prop_template", IfcStore.pset_template_file, **{ - "prop_template": IfcStore.pset_template_file.by_id(self.prop_template) - }) + ifcopenshell.api.run( + "pset_template.remove_prop_template", + IfcStore.pset_template_file, + **{"prop_template": IfcStore.pset_template_file.by_id(self.prop_template)} + ) Data.load(IfcStore.pset_template_file) return {"FINISHED"} + def rollback(self, data): + IfcStore.pset_template_file.undo() + + def commit(self, data): + IfcStore.pset_template_file.redo() + class EditPropTemplate(bpy.types.Operator): bl_idname = "bim.edit_prop_template" bl_label = "Edit Prop Template" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + IfcStore.begin_transaction(self) + IfcStore.pset_template_file.begin_transaction() + result = self._execute(context) + IfcStore.pset_template_file.end_transaction() + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): props = context.scene.BIMPsetTemplateProperties - ifcopenshell.api.run("pset_template.edit_prop_template", IfcStore.pset_template_file, **{ - "prop_template": IfcStore.pset_template_file.by_id(props.active_prop_template_id), - "attributes": { - "Name": props.active_prop_template.name, - "Description": props.active_prop_template.description, - "PrimaryMeasureType": props.active_prop_template.primary_measure_type, + ifcopenshell.api.run( + "pset_template.edit_prop_template", + IfcStore.pset_template_file, + **{ + "prop_template": IfcStore.pset_template_file.by_id(props.active_prop_template_id), + "attributes": { + "Name": props.active_prop_template.name, + "Description": props.active_prop_template.description, + "PrimaryMeasureType": props.active_prop_template.primary_measure_type, + }, } - }) + ) Data.load(IfcStore.pset_template_file) bpy.ops.bim.disable_editing_prop_template() return {"FINISHED"} + + def rollback(self, data): + IfcStore.pset_template_file.undo() + + def commit(self, data): + IfcStore.pset_template_file.redo() diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/prop.py b/src/blenderbim/blenderbim/bim/module/pset_template/prop.py index 83b134c399..6f41a98a23 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/prop.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import ifcopenshell diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/ui.py b/src/blenderbim/blenderbim/bim/module/pset_template/ui.py index b252e4e496..0049612fd5 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Panel from blenderbim.bim.ifc import IfcStore @@ -29,7 +47,7 @@ class BIM_PT_pset_template(Panel): if props.active_pset_template_id: row.operator("bim.edit_pset_template", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_pset_template", text="", icon="X") + row.operator("bim.disable_editing_pset_template", text="", icon="CANCEL") else: row.operator("bim.add_pset_template", text="", icon="ADD") row.operator("bim.enable_editing_pset_template", text="", icon="GREASEPENCIL") @@ -78,7 +96,7 @@ class BIM_PT_pset_template(Panel): if props.active_prop_template_id and props.active_prop_template_id == prop_template_id: op = row.operator("bim.edit_prop_template", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_prop_template", icon="X", text="") + row.operator("bim.disable_editing_prop_template", icon="CANCEL", text="") elif props.active_prop_template_id and props.active_prop_template_id != prop_template_id: row.operator("bim.remove_prop_template", icon="X", text="").prop_template = prop_template_id else: diff --git a/src/blenderbim/blenderbim/bim/module/qto/__init__.py b/src/blenderbim/blenderbim/bim/module/qto/__init__.py index 2cd6381ffb..6583deae6a 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/qto/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/qto/helper.py b/src/blenderbim/blenderbim/bim/module/qto/helper.py index b55992bf00..80dd27b565 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/helper.py +++ b/src/blenderbim/blenderbim/bim/module/qto/helper.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import bmesh @@ -6,15 +24,41 @@ def calculate_height(obj): return obj.dimensions[2] -def calculate_volume(obj): - bm = bmesh.new() - bm.from_mesh(obj.data) - result = bm.calc_volume() - bm.free() +def calculate_edges_lengths(objs, context): + return calculate_mesh_quantity(objs, context, lambda bm: sum((e.calc_length() for e in bm.edges if e.select))) + + +def calculate_faces_areas(objs, context): + return calculate_mesh_quantity(objs, context, lambda bm: sum((f.calc_area() for f in bm.faces if f.select))) + + +def calculate_volumes(objs, context): + return calculate_mesh_quantity(objs, context, lambda bm: bm.calc_volume()) + + +def calculate_mesh_quantity(objs: bpy.types.Object, context, operation): + """Get the sum of the target quantity on all passed mesh objects + + :param objs: iterable of mesh object + :param context: current execution context + :param operation: function which takes a single bmesh as an argument, returns a float value + :returns float: + """ + result = 0 + edit_mode = context.active_object.mode == "EDIT" + for obj in objs: + if edit_mode: + bm = bmesh.from_edit_mesh(obj.data) + result += operation(bm) + else: + bm = bmesh.new() + bm.from_mesh(obj.data) + result += operation(bm) + bm.free() return result -def calculate_formwork_area(objs): +def calculate_formwork_area(objs, context): """ Formwork is defined as the surface area required to cover all exposed surfaces of one or more objects, excluding top surfaces (i.e. that have a @@ -27,13 +71,14 @@ def calculate_formwork_area(objs): new_obj = obj.copy() new_obj.data = obj.data.copy() new_obj.animation_data_clear() - bpy.context.collection.objects.link(new_obj) + context.collection.objects.link(new_obj) copied_objs.append(new_obj) - context_override = {} - context_override["object"] = context_override["active_object"] = copied_objs[0] - context_override["selected_objects"] = context_override["selected_editable_objects"] = copied_objs - bpy.ops.object.join(context_override) + if len(objs) > 1: + context_override = {} + context_override["object"] = context_override["active_object"] = copied_objs[0] + context_override["selected_objects"] = context_override["selected_editable_objects"] = copied_objs + bpy.ops.object.join(context_override) copied_objs[0].name = "Formwork" copied_objs[0].BIMObjectProperties.ifc_definition_id = 0 @@ -53,7 +98,7 @@ def calculate_formwork_area(objs): else: modifier.octree_depth = 5 - mesh = copied_objs[0].evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() + mesh = copied_objs[0].evaluated_get(context.evaluated_depsgraph_get()).to_mesh() for polygon in mesh.polygons: if polygon.normal.z > 0.5: continue diff --git a/src/blenderbim/blenderbim/bim/module/qto/operator.py b/src/blenderbim/blenderbim/bim/module/qto/operator.py index b52a89cae4..0a40caddcc 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/operator.py +++ b/src/blenderbim/blenderbim/bim/module/qto/operator.py @@ -1,5 +1,22 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy -import bmesh import ifcopenshell import ifcopenshell.api from blenderbim.bim.ifc import IfcStore @@ -10,67 +27,68 @@ from ifcopenshell.api.pset.data import Data as PsetData class CalculateEdgeLengths(bpy.types.Operator): bl_idname = "bim.calculate_edge_lengths" bl_label = "Calculate Edge Lengths" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects and context.active_object def execute(self, context): - result = 0 - for obj in bpy.context.selected_objects: - if not obj.data or not obj.data.edges: - continue - for edge in obj.data.edges: - if edge.select: - result += (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length - bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + result = helper.calculate_edges_lengths([o for o in context.selected_objects if o.type == "MESH"], context) + context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) return {"FINISHED"} class CalculateFaceAreas(bpy.types.Operator): bl_idname = "bim.calculate_face_areas" bl_label = "Calculate Face Areas" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects and context.active_object def execute(self, context): - result = 0 - for obj in bpy.context.selected_objects: - if not obj.data or not obj.data.polygons: - continue - for polygon in obj.data.polygons: - if polygon.select: - result += polygon.area - bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + result = helper.calculate_faces_areas([o for o in context.selected_objects if o.type == "MESH"], context) + context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) return {"FINISHED"} class CalculateObjectVolumes(bpy.types.Operator): bl_idname = "bim.calculate_object_volumes" bl_label = "Calculate Object Volumes" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects and context.active_object def execute(self, context): - result = 0 - for obj in bpy.context.selected_objects: - if not obj.data or not isinstance(obj.data, bpy.types.Mesh): - continue - bm = bmesh.new() - bm.from_mesh(obj.data) - result += bm.calc_volume() - bm.free() - bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + result = helper.calculate_volumes([o for o in context.selected_objects if o.type == "MESH"], context) + context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) return {"FINISHED"} class ExecuteQtoMethod(bpy.types.Operator): bl_idname = "bim.execute_qto_method" bl_label = "Execute Qto Method" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects def execute(self, context): - props = bpy.context.scene.BIMQtoProperties + selected_mesh_objects = [o for o in context.selected_objects if o.type == "MESH"] + props = context.scene.BIMQtoProperties result = 0 if props.qto_methods == "HEIGHT": - for obj in bpy.context.selected_objects: + for obj in selected_mesh_objects: result += helper.calculate_height(obj) elif props.qto_methods == "VOLUME": - for obj in bpy.context.selected_objects: - result += helper.calculate_volume(obj) + result = helper.calculate_volumes(selected_mesh_objects, context) elif props.qto_methods == "FORMWORK": - result = helper.calculate_formwork_area(bpy.context.selected_objects) + result = helper.calculate_formwork_area(selected_mesh_objects, context) props.qto_result = str(round(result, 3)) return {"FINISHED"} @@ -78,20 +96,28 @@ class ExecuteQtoMethod(bpy.types.Operator): class QuantifyObjects(bpy.types.Operator): bl_idname = "bim.quantify_objects" bl_label = "Quantify Objects" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.selected_objects def execute(self, context): - props = bpy.context.scene.BIMQtoProperties + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMQtoProperties self.file = IfcStore.get_file() - for obj in bpy.context.selected_objects: + for obj in (o for o in context.selected_objects if o.type == "MESH"): if not obj.BIMObjectProperties.ifc_definition_id: continue result = 0 if props.qto_methods == "HEIGHT": result = helper.calculate_height(obj) elif props.qto_methods == "VOLUME": - result = helper.calculate_volume(obj) + result = helper.calculate_volumes([obj], context) elif props.qto_methods == "FORMWORK": - result = helper.calculate_formwork_area([obj]) + result = helper.calculate_formwork_area([obj], context) if not result: continue result = round(result, 3) @@ -101,11 +127,6 @@ class QuantifyObjects(bpy.types.Operator): product=self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), name=props.qto_name, ) - ifcopenshell.api.run( - "pset.edit_qto", - self.file, - qto=qto, - properties={props.prop_name: result} - ) + ifcopenshell.api.run("pset.edit_qto", self.file, qto=qto, properties={props.prop_name: result}) PsetData.load(self.file, obj.BIMObjectProperties.ifc_definition_id) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/qto/prop.py b/src/blenderbim/blenderbim/bim/module/qto/prop.py index a081e0cb54..0bc3eb4af9 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/prop.py +++ b/src/blenderbim/blenderbim/bim/module/qto/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup @@ -12,6 +30,7 @@ from bpy.props import ( CollectionProperty, ) + class BIMQtoProperties(PropertyGroup): qto_result: StringProperty(default="", name="Qto Result") qto_methods: EnumProperty( diff --git a/src/blenderbim/blenderbim/bim/module/qto/ui.py b/src/blenderbim/blenderbim/bim/module/qto/ui.py index 9bdcbb8147..0b021e66ff 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/ui.py +++ b/src/blenderbim/blenderbim/bim/module/qto/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py index 38db174c0d..b2bdd02794 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -7,13 +25,29 @@ classes = ( operator.EnableEditingResource, operator.LoadResources, operator.AddResource, + operator.AddResourceQuantity, operator.EditResource, operator.RemoveResource, + operator.RemoveResourceQuantity, operator.LoadResourceProperties, operator.ExpandResource, operator.ContractResource, operator.AssignResource, operator.UnassignResource, + operator.EnableEditingResourceTime, + operator.EnableEditingResourceQuantity, + operator.EnableEditingResourceBaseQuantity, + operator.EnableEditingResourceCosts, + operator.EnableEditingResourceCostValueFormula, + operator.EnableEditingResourceCostValue, + operator.EditResourceTime, + operator.EditResourceQuantity, + operator.EditResourceCostValue, + operator.EditResourceCostValueFormula, + operator.DisableEditingResourceTime, + operator.DisableEditingResourceQuantity, + operator.DisableEditingResourceCostValue, + operator.CalculateResourceWork, prop.Resource, prop.BIMResourceProperties, prop.BIMResourceTreeProperties, @@ -26,6 +60,7 @@ def register(): bpy.types.Scene.BIMResourceProperties = bpy.props.PointerProperty(type=prop.BIMResourceProperties) bpy.types.Scene.BIMResourceTreeProperties = bpy.props.PointerProperty(type=prop.BIMResourceTreeProperties) + def unregister(): del bpy.types.Scene.BIMResourceProperties del bpy.types.Scene.BIMResourceTreeProperties diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index 979832532c..412101ef71 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -1,19 +1,43 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json +import time +import isodate import ifcopenshell.api +import blenderbim.bim.helper +import blenderbim.bim.module.sequence.helper as helper +from datetime import datetime from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.resource.data import Data +from ifcopenshell.api.unit.data import Data as UnitData class LoadResources(bpy.types.Operator): bl_idname = "bim.load_resources" bl_label = "Load Resources" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): self.props = context.scene.BIMResourceProperties self.tprops = context.scene.BIMResourceTreeProperties - while len(self.tprops.resources) > 0: - self.tprops.resources.remove(0) + self.tprops.resources.clear() self.contracted_resources = json.loads(self.props.contracted_resources) for resource_id, data in Data.resources.items(): @@ -41,13 +65,14 @@ class LoadResources(bpy.types.Operator): class EnableEditingResource(bpy.types.Operator): bl_idname = "bim.enable_editing_resource" bl_label = "Enable Editing Resource" + bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMResourceProperties self.props.active_resource_id = self.resource - while len(self.props.resource_attributes) > 0: - self.props.resource_attributes.remove(0) + self.props.resource_attributes.clear() + self.props.editing_resource_type = "ATTRIBUTES" self.enable_editing_resource() return {"FINISHED"} @@ -73,6 +98,7 @@ class EnableEditingResource(bpy.types.Operator): class LoadResourceProperties(bpy.types.Operator): bl_idname = "bim.load_resource_properties" bl_label = "Load Resource Properties" + bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() def execute(self, context): @@ -91,15 +117,18 @@ class LoadResourceProperties(bpy.types.Operator): class DisableEditingResource(bpy.types.Operator): bl_idname = "bim.disable_editing_resource" bl_label = "Disable Editing Workplan" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMResourceProperties.active_resource_id = 0 + context.scene.BIMResourceProperties.active_task_time_id = 0 return {"FINISHED"} class DisableResourceEditingUI(bpy.types.Operator): bl_idname = "bim.disable_resource_editing_ui" bl_label = "Disable Resources Editing UI" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMResourceProperties.is_editing = False @@ -109,10 +138,14 @@ class DisableResourceEditingUI(bpy.types.Operator): class AddResource(bpy.types.Operator): bl_idname = "bim.add_resource" bl_label = "Add resource" + bl_options = {"REGISTER", "UNDO"} ifc_class: bpy.props.StringProperty() resource: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run( "resource.add_resource", IfcStore.get_file(), @@ -127,18 +160,14 @@ class AddResource(bpy.types.Operator): class EditResource(bpy.types.Operator): bl_idname = "bim.edit_resource" bl_label = "Edit Resource" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMResourceProperties - attributes = {} - for attribute in props.resource_attributes: - if attribute.is_null: - attributes[attribute.name] = None - else: - if attribute.data_type == "string": - attributes[attribute.name] = attribute.string_value - elif attribute.data_type == "enum": - attributes[attribute.name] = attribute.enum_value + attributes = blenderbim.bim.helper.export_attributes(props.resource_attributes) self.file = IfcStore.get_file() ifcopenshell.api.run( "resource.edit_resource", @@ -154,9 +183,13 @@ class EditResource(bpy.types.Operator): class RemoveResource(bpy.types.Operator): bl_idname = "bim.remove_resource" bl_label = "Remove Resource" + bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run( "resource.remove_resource", IfcStore.get_file(), @@ -170,6 +203,7 @@ class RemoveResource(bpy.types.Operator): class ExpandResource(bpy.types.Operator): bl_idname = "bim.expand_resource" bl_label = "Expand Resource" + bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() def execute(self, context): @@ -186,6 +220,7 @@ class ExpandResource(bpy.types.Operator): class ContractResource(bpy.types.Operator): bl_idname = "bim.contract_resource" bl_label = "Contract Resource" + bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() def execute(self, context): @@ -202,12 +237,16 @@ class ContractResource(bpy.types.Operator): class AssignResource(bpy.types.Operator): bl_idname = "bim.assign_resource" bl_label = "Assign Resource" + bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() related_object: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects ) for related_object in related_objects: self.file = IfcStore.get_file() @@ -224,12 +263,13 @@ class AssignResource(bpy.types.Operator): class UnassignResource(bpy.types.Operator): bl_idname = "bim.unassign_resource" bl_label = "Unassign Resource" + bl_options = {"REGISTER", "UNDO"} resource: bpy.props.IntProperty() related_object: bpy.props.StringProperty() def execute(self, context): related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects ) for related_object in related_objects: self.file = IfcStore.get_file() @@ -241,3 +281,399 @@ class UnassignResource(bpy.types.Operator): ) Data.load(self.file) return {"FINISHED"} + + +class EnableEditingResourceTime(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_time" + bl_label = "Enable Editing Resource Usage" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMResourceProperties + self.file = IfcStore.get_file() + resource_time_id = Data.resources[self.resource]["Usage"] or self.add_resource_time().id() + props.resource_time_attributes.clear() + + data = Data.resource_times[resource_time_id] + + blenderbim.bim.helper.import_attributes( + "IfcResourceTime", props.resource_time_attributes, data, self.import_attributes + ) + props.active_resource_time_id = resource_time_id + props.active_resource_id = self.resource + props.editing_resource_type = "USAGE" + return {"FINISHED"} + + def import_attributes(self, name, prop, data): + if prop.data_type == "string": + if isinstance(data[name], datetime): + prop.string_value = "" if prop.is_null else data[name].isoformat() + return True + elif isinstance(data[name], isodate.Duration): + prop.string_value = ( + "" if prop.is_null else ifcopenshell.util.date.datetime2ifc(data[name], "IfcDuration") + ) + return True + + def add_resource_time(self): + resource_time = ifcopenshell.api.run( + "resource.add_resource_time", self.file, resource=self.file.by_id(self.resource) + ) + Data.load(self.file) + return resource_time + + +class DisableEditingResourceTime(bpy.types.Operator): + bl_idname = "bim.disable_editing_resource_time" + bl_label = "Disable Editing Resource Time" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMResourceProperties.active_resource_time_id = 0 + bpy.ops.bim.disable_editing_resource() + return {"FINISHED"} + + +class EditResourceTime(bpy.types.Operator): + bl_idname = "bim.edit_resource_time" + bl_label = "Edit Resource Usage" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.props = context.scene.BIMResourceProperties + attributes = blenderbim.bim.helper.export_attributes( + self.props.resource_time_attributes, self.export_attributes + ) + + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "resource.edit_resource_time", + self.file, + **{"resource_time": self.file.by_id(self.props.active_resource_time_id), "attributes": attributes}, + ) + Data.load(self.file) + bpy.ops.bim.disable_editing_resource_time() + bpy.ops.bim.load_resource_properties(resource=self.props.active_resource_id) + return {"FINISHED"} + + def export_attributes(self, attributes, prop): + if "Start" in prop.name or "Finish" in prop.name or prop.name == "StatusTime": + if prop.is_null: + attributes[prop.name] = None + return True + attributes[prop.name] = helper.parse_datetime(prop.string_value) + return True + elif prop.name == "LevelingDelay" or "Work" in prop.name: + if prop.is_null: + attributes[prop.name] = None + return True + attributes[prop.name] = helper.parse_duration(prop.string_value) + return True + + +class CalculateResourceWork(bpy.types.Operator): + bl_idname = "bim.calculate_resource_work" + bl_label = "Calculate Resource Work" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run("resource.calculate_resource_work", self.file, resource=self.file.by_id(self.resource)) + Data.load(self.file) + bpy.ops.bim.load_resources() + return {"FINISHED"} + + +class EnableEditingResourceCosts(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_costs" + bl_label = "Enable Editing Resource Costs" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMResourceProperties + props.active_resource_id = self.resource + props.editing_resource_type = "COSTS" + bpy.ops.bim.disable_editing_resource_cost_value() + return {"FINISHED"} + + +class DisableEditingResourceCostValue(bpy.types.Operator): + bl_idname = "bim.disable_editing_resource_cost_value" + bl_label = "Disable Editing Resource Cost Value" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMResourceProperties + props.active_cost_value_id = 0 + props.cost_value_editing_type = "" + return {"FINISHED"} + + +class DisableEditingResourceCostValue(bpy.types.Operator): + bl_idname = "bim.disable_editing_resource_cost_value" + bl_label = "Disable Editing Resource Cost Value" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMResourceProperties + props.active_cost_value_id = 0 + props.cost_value_editing_type = "" + return {"FINISHED"} + + +class EnableEditingResourceCostValueFormula(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_cost_value_formula" + bl_label = "Enable Editing Resource Cost Value Formula" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMResourceProperties + self.props.cost_value_attributes.clear() + self.props.active_cost_value_id = self.cost_value + self.props.cost_value_editing_type = "FORMULA" + self.props.cost_value_formula = Data.cost_values[self.cost_value]["Formula"] + return {"FINISHED"} + + +class EnableEditingResourceCostValue(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_cost_value" + bl_label = "Enable Editing Resource Cost Value" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMResourceProperties + self.props.cost_value_attributes.clear() + self.props.active_cost_value_id = self.cost_value + self.props.cost_value_editing_type = "ATTRIBUTES" + data = Data.cost_values[self.cost_value] + + blenderbim.bim.helper.import_attributes( + data["type"], + self.props.cost_value_attributes, + data, + lambda name, prop, data: self.import_attributes(name, prop, data, context), + ) + return {"FINISHED"} + + def import_attributes(self, name, prop, data, context): + if name == "AppliedValue": + # TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure) + prop = self.props.cost_value_attributes.add() + prop.data_type = "float" + prop.name = "AppliedValue" + prop.is_optional = True + prop.float_value = 0.0 if prop.is_null else data[name] + return True + elif name == "UnitBasis": + prop = self.props.cost_value_attributes.add() + prop.name = "UnitBasisValue" + prop.data_type = "float" + prop.is_null = data["UnitBasis"] is None + prop.is_optional = True + if data["UnitBasis"]: + prop.float_value = data["UnitBasis"]["ValueComponent"] or 0 + else: + prop.float_value = 0 + + prop = self.props.cost_value_attributes.add() + prop.name = "UnitBasisUnit" + prop.data_type = "enum" + prop.is_null = prop.is_optional = False + units = {} + for unit_id, unit in UnitData.units.items(): + if unit.get("UnitType", None) in [ + "AREAUNIT", + "LENGTHUNIT", + "TIMEUNIT", + "VOLUMEUNIT", + "MASSUNIT", + "USERDEFINED", + ]: + if unit["type"] == "IfcContextDependentUnit": + units[unit_id] = f"{unit['UnitType']} / {unit['Name']}" + else: + name = unit["Name"] + if unit.get("Prefix", None): + name = f"(unit['Prefix']) {name}" + units[unit_id] = f"{unit['UnitType']} / {name}" + prop.enum_items = json.dumps(units) + if data["UnitBasis"] and data["UnitBasis"]["UnitComponent"]: + prop.enum_value = str(data["UnitBasis"]["UnitComponent"]) + return True + + +class EditResourceCostValueFormula(bpy.types.Operator): + bl_idname = "bim.edit_resource_cost_value_formula" + bl_label = "Edit Resource Cost Value Formula" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMResourceProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_value_formula", + self.file, + **{"cost_value": self.file.by_id(self.cost_value), "formula": props.cost_value_formula}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_resource_cost_value() + return {"FINISHED"} + + +class EditResourceCostValue(bpy.types.Operator): + bl_idname = "bim.edit_resource_cost_value" + bl_label = "Edit Resource Cost Value" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMResourceProperties + attributes = blenderbim.bim.helper.export_attributes( + props.cost_value_attributes, lambda attributes, prop: self.export_attributes(attributes, prop, context) + ) + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_value", + self.file, + **{"cost_value": self.file.by_id(self.cost_value), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_resource_cost_value() + return {"FINISHED"} + + def export_attributes(self, attributes, prop, context): + if prop.name == "UnitBasisValue": + if prop.is_null: + attributes["UnitBasis"] = None + return True + attributes["UnitBasis"] = { + "ValueComponent": prop.float_value or 1, + "UnitComponent": IfcStore.get_file().by_id( + int(context.scene.BIMResourceProperties.cost_value_attributes.get("UnitBasisUnit").enum_value) + ), + } + return True + if prop.name == "UnitBasisUnit": + return True + + +class EnableEditingResourceBaseQuantity(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_base_quantity" + bl_label = "Enable Editing Resource Quantity" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMResourceProperties + props.active_resource_id = self.resource + props.editing_resource_type = "QUANTITY" + return {"FINISHED"} + + +class AddResourceQuantity(bpy.types.Operator): + bl_idname = "bim.add_resource_quantity" + bl_label = "Add Resource Quantity" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + ifc_class: bpy.props.StringProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "resource.add_resource_quantity", + self.file, + resource=self.file.by_id(self.resource), + ifc_class=self.ifc_class, + ) + Data.load(self.file) + return {"FINISHED"} + + +class RemoveResourceQuantity(bpy.types.Operator): + bl_idname = "bim.remove_resource_quantity" + bl_label = "Remove Resource Quantity" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "resource.remove_resource_quantity", + self.file, + resource=self.file.by_id(self.resource), + ) + Data.load(self.file) + return {"FINISHED"} + + +class EnableEditingResourceQuantity(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_quantity" + bl_label = "Enable Editing Resource Quantity" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMResourceProperties + self.props.quantity_attributes.clear() + self.props.is_editing_quantity = True + data = Data.resources[self.resource]["BaseQuantity"] + blenderbim.bim.helper.import_attributes(data["type"], self.props.quantity_attributes, data) + return {"FINISHED"} + + +class DisableEditingResourceQuantity(bpy.types.Operator): + bl_idname = "bim.disable_editing_resource_quantity" + bl_label = "Disable Editing Resource Quantity" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMResourceProperties + props.is_editing_quantity = False + return {"FINISHED"} + + +class EditResourceQuantity(bpy.types.Operator): + bl_idname = "bim.edit_resource_quantity" + bl_label = "Edit Resource Quantity" + bl_options = {"REGISTER", "UNDO"} + physical_quantity: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMResourceProperties + attributes = blenderbim.bim.helper.export_attributes(props.quantity_attributes) + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "resource.edit_resource_quantity", + self.file, + **{"physical_quantity": self.file.by_id(self.physical_quantity), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_resource_quantity() + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py index db1ca153e2..178171cd37 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/prop.py +++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api from blenderbim.bim.ifc import IfcStore @@ -16,6 +34,14 @@ from bpy.props import ( ) +quantitytypes_enum = [] + + +def purge(): + global quantitytypes_enum + quantitytypes_enum = [] + + def updateResourceName(self, context): props = context.scene.BIMResourceProperties if not props.is_resource_update_enabled or self.name == "Unnamed": @@ -32,6 +58,18 @@ def updateResourceName(self, context): attribute.string_value = self.name +def get_quantity_types(self, context): + global quantitytypes_enum + if len(quantitytypes_enum) == 0 and IfcStore.get_schema(): + quantitytypes_enum.extend( + [ + (t.name(), t.name(), "") + for t in IfcStore.get_schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes() + ] + ) + return quantitytypes_enum + + class Resource(PropertyGroup): name: StringProperty(name="Name", update=updateResourceName) ifc_definition_id: IntProperty(name="IFC Definition ID") @@ -52,3 +90,22 @@ class BIMResourceProperties(PropertyGroup): contracted_resources: StringProperty(name="Contracted Resources", default="[]") is_resource_update_enabled: BoolProperty(name="Is Resource Update Enabled", default=True) is_loaded: BoolProperty(name="Is Editing") + active_resource_time_id: IntProperty(name="Active Resource Usage Id") + resource_time_attributes: CollectionProperty(name="Resource Usage Attributes", type=Attribute) + editing_resource_type: StringProperty(name="Editing Resource Type") + cost_types: EnumProperty( + items=[ + ("FIXED", "Fixed", "The cost value is a fixed number"), + ("SUM", "Sum", "The cost value is automatically derived from the sum of all nested cost items"), + ("CATEGORY", "Category", "The cost value represents a single category"), + ], + name="Cost Types", + ) + cost_category: StringProperty(name="Cost Category") + active_cost_value_id: IntProperty(name="Active Resource Cost Value Id") + cost_value_editing_type: StringProperty(name="Cost Value Editing Type") + cost_value_attributes: CollectionProperty(name="Cost Value Attributes", type=Attribute) + cost_value_formula: StringProperty(name="Cost Value Formula") + quantity_types: EnumProperty(items=get_quantity_types, name="Quantity Types") + is_editing_quantity: BoolProperty(name="Is Editing Quantity") + quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute) diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 1544d3fd39..5d6bf8950c 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -1,3 +1,22 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.resource.data import Data @@ -13,7 +32,8 @@ class BIM_PT_resources(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): self.props = context.scene.BIMResourceProperties @@ -32,30 +52,7 @@ class BIM_PT_resources(Panel): if not self.props.is_editing: return - row = self.layout.row(align=True) - op = row.operator("bim.add_resource", text="Add SubContract", icon="TEXT") - op.ifc_class = "IfcSubContractResource" - op.resource = 0 - op = row.operator("bim.add_resource", text="Add Crew", icon="COMMUNITY") - op.ifc_class = "IfcCrewResource" - op.resource = 0 - - icon_map = { - "IfcConstructionEquipmentResource": "TOOL_SETTINGS", - "IfcLaborResource": "OUTLINER_OB_ARMATURE", - "IfcConstructionMaterialResource": "MATERIAL", - "IfcConstructionProductResource": "PACKAGE", - } - - total_resources = len(self.tprops.resources) - if total_resources and self.props.active_resource_index < total_resources: - row = self.layout.row(align=True) - for ifc_class, icon in icon_map.items(): - label = ifc_class.replace("Ifc", "").replace("Construction", "").replace("Resource", "") - op = row.operator("bim.add_resource", text=label, icon=icon) - op.resource = self.tprops.resources[self.props.active_resource_index].ifc_definition_id - op.ifc_class = ifc_class - + self.draw_resource_operators() self.layout.template_list( "BIM_UL_resources", "", @@ -64,24 +61,165 @@ class BIM_PT_resources(Panel): self.props, "active_resource_index", ) - if self.props.active_resource_id: - self.draw_editable_resource_ui() - def draw_editable_resource_ui(self): - for attribute in self.props.resource_attributes: + if self.props.active_resource_id and self.props.editing_resource_type == "ATTRIBUTES": + self.draw_editable_resource_attributes_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY": + self.draw_editable_resource_quantity_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "COSTS": + self.draw_editable_resource_costs_ui() + elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE": + self.draw_editable_resource_time_attributes_ui() + + def draw_resource_operators(self): + row = self.layout.row(align=True) + op = row.operator("bim.add_resource", text="Add SubContract", icon="TEXT") + op.ifc_class = "IfcSubContractResource" + op.resource = 0 + op = row.operator("bim.add_resource", text="Add Crew", icon="COMMUNITY") + op.ifc_class = "IfcCrewResource" + op.resource = 0 + + total_resources = len(self.tprops.resources) + if not total_resources or self.props.active_resource_index >= total_resources: + return + + ifc_definition_id = self.tprops.resources[self.props.active_resource_index].ifc_definition_id + resource = Data.resources[ifc_definition_id] + + if resource["type"] != "IfcSubContractResource": + icon_map = { + "IfcSubContractResource": "TEXT", + "IfcConstructionEquipmentResource": "TOOL_SETTINGS", + "IfcLaborResource": "OUTLINER_OB_ARMATURE", + "IfcConstructionMaterialResource": "MATERIAL", + "IfcConstructionProductResource": "PACKAGE", + } row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + for ifc_class, icon in icon_map.items(): + label = ifc_class.replace("Ifc", "").replace("Construction", "").replace("Resource", "") + op = row.operator("bim.add_resource", text=label, icon=icon) + op.resource = ifc_definition_id + op.ifc_class = ifc_class + + row = self.layout.row(align=True) + row.alignment = "RIGHT" + + if self.props.active_resource_id == ifc_definition_id and self.props.editing_resource_type == "ATTRIBUTES": + row.operator("bim.edit_resource", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_resource", text="", icon="CANCEL") + elif self.props.active_resource_id == ifc_definition_id and self.props.editing_resource_type == "USAGE": + row.operator("bim.edit_resource_time", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_resource_time", text="", icon="CANCEL") + elif self.props.active_resource_id == ifc_definition_id and self.props.editing_resource_type == "COSTS": + row.operator("bim.disable_editing_resource", text="", icon="CANCEL") + elif self.props.active_resource_id == ifc_definition_id and self.props.editing_resource_type == "QUANTITY": + row.operator("bim.disable_editing_resource", text="", icon="CANCEL") + elif self.props.active_resource_id: + row.operator("bim.add_resource", text="", icon="ADD").resource = ifc_definition_id + row.operator("bim.remove_resource", text="", icon="X").resource = ifc_definition_id + else: + if resource["type"] in ["IfcLaborResource", "IfcConstructionEquipmentResource"]: + op = row.operator("bim.calculate_resource_work", text="", icon="TEMP") + op.resource = ifc_definition_id + row.operator("bim.enable_editing_resource_time", text="", icon="TIME").resource = ifc_definition_id + op = row.operator("bim.enable_editing_resource_base_quantity", text="", icon="PROPERTIES") + op.resource = ifc_definition_id + op = row.operator("bim.enable_editing_resource_costs", text="", icon="DISC") + op.resource = ifc_definition_id + row.operator("bim.enable_editing_resource", text="", icon="GREASEPENCIL").resource = ifc_definition_id + row.operator("bim.remove_resource", text="", icon="X").resource = ifc_definition_id + + def draw_editable_resource_attributes_ui(self): + blenderbim.bim.helper.draw_attributes(self.props.resource_attributes, self.layout) + + def draw_editable_resource_time_attributes_ui(self): + blenderbim.bim.helper.draw_attributes(self.props.resource_time_attributes, self.layout) + + def draw_editable_resource_quantity_ui(self): + resource = Data.resources[self.props.active_resource_id] + + if resource["BaseQuantity"]: + quantity = resource["BaseQuantity"] + value = quantity[[k for k in quantity.keys() if "Value" in k][0]] + row = self.layout.row(align=True) + row.label(text=quantity["Name"]) + row.label(text="{0:.2f}".format(value)) + if self.props.is_editing_quantity: + op = row.operator("bim.edit_resource_quantity", text="", icon="CHECKMARK") + op.physical_quantity = quantity["id"] + row.operator("bim.disable_editing_resource_quantity", text="", icon="CANCEL") + else: + op = row.operator("bim.enable_editing_resource_quantity", text="", icon="GREASEPENCIL") + op.resource = self.props.active_resource_id + op = row.operator("bim.remove_resource_quantity", text="", icon="X") + op.resource = self.props.active_resource_id + + if self.props.is_editing_quantity: + box = self.layout.box() + blenderbim.bim.helper.draw_attributes(self.props.quantity_attributes, box) + else: + row = self.layout.row(align=True) + row.prop(self.props, "quantity_types", text="") + op = row.operator("bim.add_resource_quantity", text="", icon="ADD") + op.resource = self.props.active_resource_id + op.ifc_class = self.props.quantity_types + + def draw_editable_resource_costs_ui(self): + row = self.layout.row(align=True) + row.prop(self.props, "cost_types", text="") + if self.props.cost_types == "CATEGORY": + row.prop(self.props, "cost_category", text="") + op = row.operator("bim.add_cost_value", text="", icon="ADD") + op.parent = self.props.active_resource_id + op.cost_type = self.props.cost_types + if self.props.cost_types == "CATEGORY": + op.cost_category = self.props.cost_category + + for cost_value_id in Data.resources[self.props.active_resource_id]["BaseCosts"] or []: + row = self.layout.row(align=True) + self.draw_readonly_cost_value_ui(row, cost_value_id) + + if self.props.cost_value_editing_type == "ATTRIBUTES": + box = self.layout.box() + self.draw_editable_cost_value_ui(box, Data.cost_values[self.props.active_cost_value_id]) + + def draw_readonly_cost_value_ui(self, layout, cost_value_id): + cost_value = Data.cost_values[cost_value_id] + + if self.props.active_cost_value_id == cost_value_id and self.props.cost_value_editing_type == "FORMULA": + layout.prop(self.props, "cost_value_formula", text="") + else: + cost_value_label = "{0:.2f}".format(cost_value["AppliedValue"]) + cost_value_label += " = " + cost_value["Formula"] + layout.label(text=cost_value_label, icon="DISC") + + self.draw_cost_value_operator_ui(layout, cost_value_id, self.props.active_resource_id) + + def draw_cost_value_operator_ui(self, layout, cost_value_id, parent_id): + if self.props.active_cost_value_id and self.props.active_cost_value_id == cost_value_id: + if self.props.cost_value_editing_type == "ATTRIBUTES": + op = layout.operator("bim.edit_resource_cost_value", text="", icon="CHECKMARK") + op.cost_value = cost_value_id + elif self.props.cost_value_editing_type == "FORMULA": + op = layout.operator("bim.edit_resource_cost_value_formula", text="", icon="CHECKMARK") + op.cost_value = cost_value_id + layout.operator("bim.disable_editing_resource_cost_value", text="", icon="CANCEL") + elif self.props.active_cost_value_id: + op = layout.operator("bim.remove_cost_value", text="", icon="X") + op.parent = parent_id + op.cost_value = cost_value_id + else: + op = layout.operator("bim.enable_editing_resource_cost_value_formula", text="", icon="CON_TRANSLIKE") + op.cost_value = cost_value_id + op = layout.operator("bim.enable_editing_resource_cost_value", text="", icon="GREASEPENCIL") + op.cost_value = cost_value_id + op = layout.operator("bim.remove_cost_value", text="", icon="X") + op.parent = parent_id + op.cost_value = cost_value_id + + def draw_editable_cost_value_ui(self, layout, cost_value): + blenderbim.bim.helper.draw_attributes(self.props.cost_value_attributes, layout) class BIM_UL_resources(UIList): @@ -122,16 +260,3 @@ class BIM_UL_resources(UIList): else: op = row.operator("bim.assign_resource", text="", icon="KEYFRAME", emboss=False) op.resource = item.ifc_definition_id - - if props.active_resource_id == item.ifc_definition_id: - row.operator("bim.edit_resource", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_resource", text="", icon="CANCEL") - elif props.active_resource_id: - row.operator("bim.add_resource", text="", icon="ADD").resource = item.ifc_definition_id - row.operator("bim.remove_resource", text="", icon="X").resource = item.ifc_definition_id - else: - row.operator( - "bim.enable_editing_resource", text="", icon="GREASEPENCIL" - ).resource = item.ifc_definition_id - - row.operator("bim.remove_resource", text="", icon="X").resource = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/module/root/__init__.py b/src/blenderbim/blenderbim/bim/module/root/__init__.py index 0d41a5eba9..b43b044acf 100644 --- a/src/blenderbim/blenderbim/bim/module/root/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/root/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/root/operator.py b/src/blenderbim/blenderbim/bim/module/root/operator.py index f6bffb0e3d..3470c45cbb 100644 --- a/src/blenderbim/blenderbim/bim/module/root/operator.py +++ b/src/blenderbim/blenderbim/bim/module/root/operator.py @@ -1,10 +1,27 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy -import numpy as np import ifcopenshell import ifcopenshell.api import ifcopenshell.util.schema import ifcopenshell.util.element -from ifcopenshell.api.geometry.data import Data as GeometryData +import blenderbim.bim.handler from ifcopenshell.api.void.data import Data as VoidData from blenderbim.bim.ifc import IfcStore @@ -15,10 +32,10 @@ class EnableReassignClass(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = bpy.context.active_object + obj = context.active_object self.file = IfcStore.get_file() ifc_class = obj.name.split("/")[0] - bpy.context.active_object.BIMObjectProperties.is_reassigning_class = True + context.active_object.BIMObjectProperties.is_reassigning_class = True ifc_products = [ "IfcElement", "IfcElementType", @@ -31,11 +48,11 @@ class EnableReassignClass(bpy.types.Operator): ] for ifc_product in ifc_products: if ifcopenshell.util.schema.is_a(IfcStore.get_schema().declaration_by_name(ifc_class), ifc_product): - bpy.context.scene.BIMRootProperties.ifc_product = ifc_product + context.scene.BIMRootProperties.ifc_product = ifc_product element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - bpy.context.scene.BIMRootProperties.ifc_class = element.is_a() + context.scene.BIMRootProperties.ifc_class = element.is_a() if hasattr(element, "PredefinedType") and element.PredefinedType: - bpy.context.scene.BIMRootProperties.ifc_predefined_type = element.PredefinedType + context.scene.BIMRootProperties.ifc_predefined_type = element.PredefinedType return {"FINISHED"} @@ -45,7 +62,7 @@ class DisableReassignClass(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.context.active_object.BIMObjectProperties.is_reassigning_class = False + context.active_object.BIMObjectProperties.is_reassigning_class = False return {"FINISHED"} @@ -59,18 +76,18 @@ class ReassignClass(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - objects = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects + objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects self.file = IfcStore.get_file() - predefined_type = bpy.context.scene.BIMRootProperties.ifc_predefined_type + predefined_type = context.scene.BIMRootProperties.ifc_predefined_type if predefined_type == "USERDEFINED": - predefined_type = bpy.context.scene.BIMRootProperties.ifc_userdefined_type + predefined_type = context.scene.BIMRootProperties.ifc_userdefined_type for obj in objects: product = ifcopenshell.api.run( "root.reassign_class", self.file, **{ "product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), - "ifc_class": bpy.context.scene.BIMRootProperties.ifc_class, + "ifc_class": context.scene.BIMRootProperties.ifc_class, "predefined_type": predefined_type, }, ) @@ -96,13 +113,13 @@ class AssignClass(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - objects = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects + objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects self.file = IfcStore.get_file() + if not self.ifc_class: + self.ifc_class = context.scene.BIMRootProperties.ifc_class self.declaration = IfcStore.get_schema().declaration_by_name(self.ifc_class) if self.predefined_type == "USERDEFINED": - self.predefined_type = self.ifc_userdefined_type - elif self.predefined_type == "": - predefined_type = None + self.predefined_type = self.userdefined_type for obj in objects: self.assign_class(context, obj) return {"FINISHED"} @@ -115,7 +132,7 @@ class AssignClass(bpy.types.Operator): self.file, **{ "ifc_class": self.ifc_class, - "predefined_type": self.predefined_type, + "predefined_type": self.predefined_type or None, "name": obj.name, }, ) @@ -128,20 +145,23 @@ class AssignClass(bpy.types.Operator): ) if product.is_a("IfcElementType"): - self.place_in_types_collection(obj) + self.place_in_types_collection(obj, context) + elif product.is_a("IfcOpeningElement"): + obj.display_type = "WIRE" + self.place_in_openings_collection(obj, context) elif ( product.is_a("IfcSpatialElement") or product.is_a("IfcSpatialStructureElement") or product.is_a("IfcProject") or product.is_a("IfcContext") ): - self.place_in_spatial_collection(obj) + self.place_in_spatial_collection(obj, context) else: self.assign_potential_spatial_container(obj) context.view_layer.objects.active = obj - def place_in_types_collection(self, obj): - for project in [c for c in bpy.context.view_layer.layer_collection.children if "IfcProject" in c.name]: + def place_in_types_collection(self, obj, context): + for project in [c for c in context.view_layer.layer_collection.children if "IfcProject" in c.name]: if not [c for c in project.children if "Types" in c.name]: types = bpy.data.collections.new("Types") project.collection.children.link(types) @@ -152,7 +172,19 @@ class AssignClass(bpy.types.Operator): break break - def place_in_spatial_collection(self, obj): + def place_in_openings_collection(self, obj, context): + for project in [c for c in context.view_layer.layer_collection.children if "IfcProject" in c.name]: + if not [c for c in project.children if "IfcOpeningElements" in c.name]: + opening_elements = bpy.data.collections.new("IfcOpeningElements") + project.collection.children.link(opening_elements) + for collection in [c for c in project.children if "IfcOpeningElements" in c.name]: + for user_collection in obj.users_collection: + user_collection.objects.unlink(obj) + collection.collection.objects.link(obj) + break + break + + def place_in_spatial_collection(self, obj, context): for collection in obj.users_collection: if collection.name == obj.name: return @@ -167,7 +199,7 @@ class AssignClass(bpy.types.Operator): parent_collection.children.link(collection) bpy.ops.bim.assign_object(related_object=obj.name, relating_object=parent_collection.name) else: - bpy.context.scene.collection.children.link(collection) + context.scene.collection.children.link(collection) def assign_potential_spatial_container(self, obj): for collection in obj.users_collection: @@ -175,6 +207,11 @@ class AssignClass(bpy.types.Operator): continue spatial_obj = bpy.data.objects.get(collection.name) if spatial_obj and spatial_obj.BIMObjectProperties.ifc_definition_id: + element = self.file.by_id(spatial_obj.BIMObjectProperties.ifc_definition_id) + if self.file.schema != "IFC2X3" and not element.is_a("IfcSpatialElement"): + continue + elif self.file.schema == "IFC2X3" and not element.is_a("IfcSpatialStructureElement"): + continue bpy.ops.bim.assign_container( relating_structure=spatial_obj.BIMObjectProperties.ifc_definition_id, related_element=obj.name ) @@ -195,7 +232,7 @@ class UnassignClass(bpy.types.Operator): if self.obj: objects = [bpy.data.objects.get(self.obj)] else: - objects = bpy.context.selected_objects + objects = context.selected_objects for obj in objects: if not obj.BIMObjectProperties.ifc_definition_id: continue @@ -238,7 +275,7 @@ class UnlinkObject(bpy.types.Operator): if self.obj: objects = [bpy.data.objects.get(self.obj)] else: - objects = bpy.context.selected_objects + objects = context.selected_objects for obj in objects: if obj.BIMObjectProperties.ifc_definition_id: IfcStore.unlink_element(obj=obj) @@ -261,17 +298,50 @@ class CopyClass(bpy.types.Operator): if self.obj: objects = [bpy.data.objects.get(self.obj)] else: - objects = bpy.context.selected_objects + objects = context.selected_objects for obj in objects: if not obj.BIMObjectProperties.ifc_definition_id: continue - result = ifcopenshell.api.run( - "root.copy_class", self.file, **{"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)} - ) + old_element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + result = ifcopenshell.api.run("root.copy_class", self.file, **{"product": old_element}) IfcStore.link_element(result, obj) relating_type = ifcopenshell.util.element.get_type(result) if relating_type and relating_type.RepresentationMaps: bpy.ops.bim.assign_type(relating_type=relating_type.id(), related_object=obj.name) else: bpy.ops.bim.add_representation(obj=obj.name) + if result.is_a("IfcSpatialElement") or result.is_a("IfcSpatialStructureElement"): + self.place_in_spatial_collection(result, obj) + elif result.is_a("IfcOpeningElement"): + self.add_opening_modifiers(result, obj) + blenderbim.bim.handler.purge_module_data() return {"FINISHED"} + + def place_in_spatial_collection(self, element, obj): + aggregate = ifcopenshell.util.element.get_aggregate(element) + if not aggregate: + return + container_obj = IfcStore.get_element(aggregate.id()) + for collection in obj.users_collection: + collection.objects.unlink(obj) + if "Ifc" in collection.name: + parent_collection = collection + for collection in container_obj.users_collection: + if collection.name == container_obj.name: + new = bpy.data.collections.new(obj.name) + new.objects.link(obj) + collection.children.link(new) + + def add_opening_modifiers(self, result, obj): + for rel in result.VoidsElements: + building_obj = IfcStore.get_element(rel.RelatingBuildingElement.id()) + try: + modifier = next(m for m in obj.modifiers if m.type == "BOOLEAN" and m.object == obj) + except StopIteration: + modifier = building_obj.modifiers.new("IfcOpeningElement", "BOOLEAN") + modifier.object = obj + finally: + modifier.operation = "DIFFERENCE" + modifier.solver = "EXACT" + modifier.use_self = True + modifier.operand_type = "OBJECT" diff --git a/src/blenderbim/blenderbim/bim/module/root/prop.py b/src/blenderbim/blenderbim/bim/module/root/prop.py index 80ba2a20e4..1e73812bf3 100644 --- a/src/blenderbim/blenderbim/bim/module/root/prop.py +++ b/src/blenderbim/blenderbim/bim/module/root/prop.py @@ -1,4 +1,24 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy +import ifcopenshell +import ifcopenshell.util.schema from blenderbim.bim.ifc import IfcStore from bpy.types import PropertyGroup from bpy.props import ( @@ -51,7 +71,8 @@ def refreshPredefinedTypes(self, context): global types_enum types_enum.clear() enum = getIfcPredefinedTypes(self, context) - context.scene.BIMRootProperties.ifc_predefined_type = enum[0][0] + if enum: + context.scene.BIMRootProperties.ifc_predefined_type = enum[0][0] def getIfcProducts(self, context): @@ -82,17 +103,8 @@ def getIfcClasses(self, context): file = IfcStore.get_file() if len(classes_enum) < 1 and file: declaration = IfcStore.get_schema().declaration_by_name(context.scene.BIMRootProperties.ifc_product) - - def get_classes(declaration): - results = [] - if not declaration.is_abstract(): - results.append(declaration.name()) - for subtype in declaration.subtypes(): - results.extend(get_classes(subtype)) - return results - - classes = get_classes(declaration) - classes_enum.extend([(c, c, "") for c in sorted(classes)]) + declarations = ifcopenshell.util.schema.get_subtypes(declaration) + classes_enum.extend([(c, c, "") for c in sorted([d.name() for d in declarations])]) return classes_enum diff --git a/src/blenderbim/blenderbim/bim/module/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py index f698b9ca75..950195a5ab 100644 --- a/src/blenderbim/blenderbim/bim/module/root/ui.py +++ b/src/blenderbim/blenderbim/bim/module/root/ui.py @@ -1,4 +1,23 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy +import blenderbim.bim.module.root.prop as root_prop from bpy.types import Panel from ifcopenshell.api.root.data import Data from blenderbim.bim.ifc import IfcStore @@ -29,17 +48,23 @@ class BIM_PT_class(Panel): if props.is_reassigning_class: row = self.layout.row(align=True) row.operator("bim.reassign_class", icon="CHECKMARK") - row.operator("bim.disable_reassign_class", icon="X", text="") - self.draw_class_dropdowns() + row.operator("bim.disable_reassign_class", icon="CANCEL", text="") + self.draw_class_dropdowns( + context, root_prop.getIfcPredefinedTypes(context.scene.BIMRootProperties, context) + ) else: data = Data.products[props.ifc_definition_id] name = data["type"] if data["PredefinedType"] and data["PredefinedType"] == "USERDEFINED": - name += "[{}]".format(data["ObjectType"]) + if data["ObjectType"]: + name += "[{}]".format(data["ObjectType"]) + elif data["ElementType"]: + name += "[{}]".format(data["ElementType"]) elif data["PredefinedType"]: name += "[{}]".format(data["PredefinedType"]) row = self.layout.row(align=True) row.label(text=name) + row.operator("bim.select_ifc_class", text="", icon="RESTRICT_SELECT_OFF").ifc_class = data["type"] row.operator("bim.copy_class", icon="DUPLICATE", text="") row.operator("bim.unlink_object", icon="UNLINKED", text="") if IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcRoot"): @@ -49,24 +74,25 @@ class BIM_PT_class(Panel): else: row.operator("bim.unassign_class", icon="X", text="").obj = context.active_object.name else: - self.draw_class_dropdowns() + ifc_predefined_types = root_prop.getIfcPredefinedTypes(context.scene.BIMRootProperties, context) + self.draw_class_dropdowns(context, ifc_predefined_types) row = self.layout.row(align=True) op = row.operator("bim.assign_class") - op.ifc_class = bpy.context.scene.BIMRootProperties.ifc_class - op.predefined_type = bpy.context.scene.BIMRootProperties.ifc_predefined_type - op.userdefined_type = bpy.context.scene.BIMRootProperties.ifc_userdefined_type + op.ifc_class = context.scene.BIMRootProperties.ifc_class + op.predefined_type = context.scene.BIMRootProperties.ifc_predefined_type if ifc_predefined_types else "" + op.userdefined_type = context.scene.BIMRootProperties.ifc_userdefined_type - def draw_class_dropdowns(self): - props = bpy.context.scene.BIMRootProperties + def draw_class_dropdowns(self, context, ifc_predefined_types): + props = context.scene.BIMRootProperties row = self.layout.row() row.prop(props, "ifc_product") row = self.layout.row() row.prop(props, "ifc_class") - if props.ifc_predefined_type: + if ifc_predefined_types: row = self.layout.row() row.prop(props, "ifc_predefined_type") - if props.ifc_predefined_type == "USERDEFINED": + if ifc_predefined_types == "USERDEFINED": row = self.layout.row() row.prop(props, "ifc_userdefined_type") row = self.layout.row() - row.prop(bpy.context.scene.BIMProperties, "contexts") + row.prop(context.scene.BIMProperties, "contexts") diff --git a/src/blenderbim/blenderbim/bim/module/search/__init__.py b/src/blenderbim/blenderbim/bim/module/search/__init__.py index 827083f847..2550dd1c7b 100644 --- a/src/blenderbim/blenderbim/bim/module/search/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/search/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py index 93ed1fb61b..566bf5048e 100644 --- a/src/blenderbim/blenderbim/bim/module/search/operator.py +++ b/src/blenderbim/blenderbim/bim/module/search/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import re import bpy import ifcopenshell @@ -22,25 +40,25 @@ colour_list = [ ] -def does_keyword_exist(pattern, string): +def does_keyword_exist(pattern, string, context): string = str(string) - if ( - bpy.context.scene.BIMSearchProperties.should_use_regex - and bpy.context.scene.BIMSearchProperties.should_ignorecase - and re.search(pattern, string, flags=re.IGNORECASE) - ): + props = context.scene.BIMSearchProperties + if props.should_use_regex and props.should_ignorecase and re.search(pattern, string, flags=re.IGNORECASE): return True - elif bpy.context.scene.BIMSearchProperties.should_use_regex and re.search(pattern, string): + elif props.should_use_regex and re.search(pattern, string): return True - elif bpy.context.scene.BIMSearchProperties.should_ignorecase and string.lower() == pattern.lower(): + elif props.should_ignorecase and string.lower() == pattern.lower(): return True elif string == pattern: return True class SelectGlobalId(bpy.types.Operator): + """Click to select the objects that match with the given Global ID""" + bl_idname = "bim.select_global_id" bl_label = "Select GlobalId" + bl_options = {"REGISTER", "UNDO"} global_id: bpy.props.StringProperty() def execute(self, context): @@ -58,24 +76,30 @@ class SelectGlobalId(bpy.types.Operator): class SelectIfcClass(bpy.types.Operator): + """Click to select all objects that match with the given IFC class""" + bl_idname = "bim.select_ifc_class" bl_label = "Select IFC Class" + bl_options = {"REGISTER", "UNDO"} + ifc_class: bpy.props.StringProperty() def execute(self, context): self.file = IfcStore.get_file() - props = context.scene.BIMSearchProperties for obj in context.visible_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - if does_keyword_exist(props.ifc_class, element.is_a()): + if does_keyword_exist(self.ifc_class, element.is_a(), context): obj.select_set(True) return {"FINISHED"} class SelectAttribute(bpy.types.Operator): + """Click to select all objects that match with the given Attribute Name and Value""" + bl_idname = "bim.select_attribute" bl_label = "Select Attribute" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): self.file = IfcStore.get_file() @@ -91,14 +115,17 @@ class SelectAttribute(bpy.types.Operator): value = next((v for k, v in data.items() if k.lower() == attribute_name.lower()), None) else: value = getattr(element, attribute_name, None) - if does_keyword_exist(pattern, value): + if does_keyword_exist(pattern, value, context): obj.select_set(True) return {"FINISHED"} class SelectPset(bpy.types.Operator): + """Click to select all objects that match with the given Pset Name, Properties Name and Value""" + bl_idname = "bim.select_pset" bl_label = "Select Pset" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): self.file = IfcStore.get_file() @@ -122,16 +149,27 @@ class SelectPset(bpy.types.Operator): else: props = props or psets.get(search_pset_name, {}) value = props.get(search_prop_name, None) - if does_keyword_exist(pattern, value): + if does_keyword_exist(pattern, value, context): obj.select_set(True) return {"FINISHED"} class ColourByAttribute(bpy.types.Operator): + """Click to colour different objects according to given Attribute Name""" + bl_idname = "bim.colour_by_attribute" bl_label = "Colour by Attribute" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + IfcStore.begin_transaction(self) + self.store_state(context) + result = self._execute(context) + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): self.file = IfcStore.get_file() colours = cycle(colour_list) values = {} @@ -148,16 +186,41 @@ class ColourByAttribute(bpy.types.Operator): if value not in values: values[value] = next(colours) obj.color = values[value] - area = next(area for area in context.screen.areas if area.type == "VIEW_3D") - area.spaces[0].shading.color_type = "OBJECT" + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + areas[0].spaces[0].shading.color_type = "OBJECT" return {"FINISHED"} + def store_state(self, context): + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type} + + def rollback(self, data): + if data: + data["area"].spaces[0].shading.color_type = data["color_type"] + + def commit(self, data): + if data: + data["area"].spaces[0].shading.color_type = "OBJECT" + class ColourByPset(bpy.types.Operator): + """Click to colour different objects according to given Prop Name""" + bl_idname = "bim.colour_by_pset" bl_label = "Colour by Pset" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + IfcStore.begin_transaction(self) + self.store_state(context) + result = self._execute(context) + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): self.file = IfcStore.get_file() colours = cycle(colour_list) values = {} @@ -182,20 +245,45 @@ class ColourByPset(bpy.types.Operator): if value not in values: values[value] = next(colours) obj.color = values[value] - area = next(area for area in context.screen.areas if area.type == "VIEW_3D") - area.spaces[0].shading.color_type = "OBJECT" + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + areas[0].spaces[0].shading.color_type = "OBJECT" return {"FINISHED"} + def store_state(self, context): + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type} + + def rollback(self, data): + if data: + data["area"].spaces[0].shading.color_type = data["color_type"] + + def commit(self, data): + if data: + data["area"].spaces[0].shading.color_type = "OBJECT" + class ColourByClass(bpy.types.Operator): + """Click to colour different objects according to their IFC Classes""" + bl_idname = "bim.colour_by_class" bl_label = "Colour by Class" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + IfcStore.begin_transaction(self) + self.store_state(context) + result = self._execute(context) + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): self.file = IfcStore.get_file() colours = cycle(colour_list) ifc_classes = {} - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) @@ -203,16 +291,32 @@ class ColourByClass(bpy.types.Operator): if ifc_class not in ifc_classes: ifc_classes[ifc_class] = next(colours) obj.color = ifc_classes[ifc_class] - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") - area.spaces[0].shading.color_type = "OBJECT" + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + areas[0].spaces[0].shading.color_type = "OBJECT" return {"FINISHED"} + def store_state(self, context): + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type} + + def rollback(self, data): + if data: + data["area"].spaces[0].shading.color_type = data["color_type"] + + def commit(self, data): + if data: + data["area"].spaces[0].shading.color_type = "OBJECT" + class ResetObjectColours(bpy.types.Operator): + """Reset the colour of selected objects""" + bl_idname = "bim.reset_object_colours" bl_label = "Reset Colours" def execute(self, context): - for obj in bpy.context.selected_objects: + for obj in context.selected_objects: obj.color = (1, 1, 1, 1) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/search/prop.py b/src/blenderbim/blenderbim/bim/module/search/prop.py index 7723434add..78a0949e69 100644 --- a/src/blenderbim/blenderbim/bim/module/search/prop.py +++ b/src/blenderbim/blenderbim/bim/module/search/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import PropertyGroup from bpy.props import ( @@ -11,6 +29,7 @@ from bpy.props import ( CollectionProperty, ) + class BIMSearchProperties(PropertyGroup): should_use_regex: BoolProperty(name="Search With Regex", default=False) should_ignorecase: BoolProperty(name="Search Ignoring Case", default=True) diff --git a/src/blenderbim/blenderbim/bim/module/search/ui.py b/src/blenderbim/blenderbim/bim/module/search/ui.py index a12b4ace6a..405a3623da 100644 --- a/src/blenderbim/blenderbim/bim/module/search/ui.py +++ b/src/blenderbim/blenderbim/bim/module/search/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Panel from blenderbim.bim.ifc import IfcStore @@ -28,11 +46,11 @@ class BIM_PT_search(Panel): row = self.layout.row(align=True) row.prop(props, "global_id", text="", icon="TRACKER") - row.operator("bim.select_global_id", text="", icon="VIEWZOOM") + row.operator("bim.select_global_id", text="", icon="VIEWZOOM").global_id = props.global_id row = self.layout.row(align=True) row.prop(props, "ifc_class", text="", icon="OBJECT_DATA") - row.operator("bim.select_ifc_class", text="", icon="VIEWZOOM") + row.operator("bim.select_ifc_class", text="", icon="VIEWZOOM").ifc_class = props.ifc_class row.operator("bim.colour_by_class", text="", icon="BRUSH_DATA") row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py index 553e9f4d77..12be9e5872 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -76,19 +94,30 @@ classes = ( operator.AddTaskColumn, operator.RemoveTaskColumn, operator.SetTaskSortColumn, + operator.LoadTaskInputs, + operator.LoadTaskResources, + operator.LoadTaskOutputs, + operator.CalculateTaskDuration, prop.WorkPlan, prop.BIMWorkPlanProperties, prop.Task, + prop.TaskResource, + prop.TaskProduct, prop.BIMWorkScheduleProperties, prop.BIMTaskTreeProperties, prop.WorkCalendar, prop.RecurrenceComponent, prop.BIMWorkCalendarProperties, prop.DatePickerProperties, + prop.BIMDateTextProperties, ui.BIM_PT_work_plans, ui.BIM_PT_work_schedules, ui.BIM_PT_work_calendars, + ui.BIM_PT_task_icom, ui.BIM_UL_task_columns, + ui.BIM_UL_task_inputs, + ui.BIM_UL_task_resources, + ui.BIM_UL_task_outputs, ui.BIM_UL_tasks, ) @@ -104,6 +133,7 @@ def register(): bpy.types.Scene.BIMTaskTreeProperties = bpy.props.PointerProperty(type=prop.BIMTaskTreeProperties) bpy.types.Scene.BIMWorkCalendarProperties = bpy.props.PointerProperty(type=prop.BIMWorkCalendarProperties) bpy.types.Scene.DatePickerProperties = bpy.props.PointerProperty(type=prop.DatePickerProperties) + bpy.types.TextCurve.BIMDateTextProperties = bpy.props.PointerProperty(type=prop.BIMDateTextProperties) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) @@ -113,4 +143,5 @@ def unregister(): del bpy.types.Scene.BIMTaskTreeProperties del bpy.types.Scene.BIMWorkCalendarProperties del bpy.types.Scene.DatePickerProperties + del bpy.types.TextCurve.BIMDateTextProperties bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/blenderbim/blenderbim/bim/module/sequence/helper.py b/src/blenderbim/blenderbim/bim/module/sequence/helper.py index 3c6aec4199..2e26f69921 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/helper.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/helper.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import math import isodate @@ -74,4 +92,4 @@ def get_scene_prop(prop_path): def set_scene_prop(prop_path, value): parent = get_scene_prop(prop_path[: prop_path.rfind(".")]) prop = prop_path.split(".")[-1] - parent[prop] = value \ No newline at end of file + parent[prop] = value diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 5bce3710e1..3331d56782 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import re import os import bpy @@ -18,13 +36,18 @@ from dateutil import parser, relativedelta from blenderbim.bim.ifc import IfcStore from bpy_extras.io_utils import ImportHelper from ifcopenshell.api.sequence.data import Data +from ifcopenshell.api.resource.data import Data as ResourceData class AddWorkPlan(bpy.types.Operator): bl_idname = "bim.add_work_plan" bl_label = "Add Work Plan" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run("sequence.add_work_plan", IfcStore.get_file()) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -32,9 +55,13 @@ class AddWorkPlan(bpy.types.Operator): class EditWorkPlan(bpy.types.Operator): bl_idname = "bim.edit_work_plan" + bl_options = {"REGISTER", "UNDO"} bl_label = "Edit Work Plan" def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkPlanProperties attributes = blenderbim.bim.helper.export_attributes(props.work_plan_attributes, self.export_attributes) self.file = IfcStore.get_file() @@ -49,9 +76,15 @@ class EditWorkPlan(bpy.types.Operator): def export_attributes(self, attributes, prop): if "Date" in prop.name or "Time" in prop.name: + if prop.is_null: + attributes[prop.name] = None + return True attributes[prop.name] = helper.parse_datetime(prop.string_value) return True elif prop.name == "Duration" or prop.name == "TotalFloat": + if prop.is_null: + attributes[prop.name] = None + return True attributes[prop.name] = helper.parse_duration(prop.string_value) return True @@ -59,24 +92,28 @@ class EditWorkPlan(bpy.types.Operator): class RemoveWorkPlan(bpy.types.Operator): bl_idname = "bim.remove_work_plan" bl_label = "Remove Work Plan" + bl_options = {"REGISTER", "UNDO"} work_plan: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run("sequence.remove_work_plan", self.file, **{"work_plan": self.file.by_id(self.work_plan)}) - Data.load(IfcStore.get_file()) + Data.load(self.file) return {"FINISHED"} class EnableEditingWorkPlan(bpy.types.Operator): bl_idname = "bim.enable_editing_work_plan" bl_label = "Enable Editing Work Plan" + bl_options = {"REGISTER", "UNDO"} work_plan: bpy.props.IntProperty() def execute(self, context): props = context.scene.BIMWorkPlanProperties - while len(props.work_plan_attributes) > 0: - props.work_plan_attributes.remove(0) + props.work_plan_attributes.clear() data = Data.work_plans[self.work_plan] @@ -94,6 +131,7 @@ class EnableEditingWorkPlan(bpy.types.Operator): class DisableEditingWorkPlan(bpy.types.Operator): bl_idname = "bim.disable_editing_work_plan" + bl_options = {"REGISTER", "UNDO"} bl_label = "Disable Editing Work Plan" def execute(self, context): @@ -104,6 +142,7 @@ class DisableEditingWorkPlan(bpy.types.Operator): class EnableEditingWorkPlanSchedules(bpy.types.Operator): bl_idname = "bim.enable_editing_work_plan_schedules" bl_label = "Enable Editing Work Plan Schedules" + bl_options = {"REGISTER", "UNDO"} work_plan: bpy.props.IntProperty() def execute(self, context): @@ -116,10 +155,14 @@ class EnableEditingWorkPlanSchedules(bpy.types.Operator): class AssignWorkSchedule(bpy.types.Operator): bl_idname = "bim.assign_work_schedule" bl_label = "Assign Work Schedule" + bl_options = {"REGISTER", "UNDO"} work_plan: bpy.props.IntProperty() work_schedule: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "aggregate.assign_object", @@ -136,10 +179,14 @@ class AssignWorkSchedule(bpy.types.Operator): class UnassignWorkSchedule(bpy.types.Operator): bl_idname = "bim.unassign_work_schedule" bl_label = "Unassign Work Schedule" + bl_options = {"REGISTER", "UNDO"} work_plan: bpy.props.IntProperty() work_schedule: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "aggregate.unassign_object", @@ -156,8 +203,12 @@ class UnassignWorkSchedule(bpy.types.Operator): class AddWorkSchedule(bpy.types.Operator): bl_idname = "bim.add_work_schedule" bl_label = "Add Work Schedule" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run("sequence.add_work_schedule", IfcStore.get_file()) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -166,8 +217,12 @@ class AddWorkSchedule(bpy.types.Operator): class EditWorkSchedule(bpy.types.Operator): bl_idname = "bim.edit_work_schedule" bl_label = "Edit Work Schedule" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties attributes = blenderbim.bim.helper.export_attributes(props.work_schedule_attributes, self.export_attributes) self.file = IfcStore.get_file() @@ -182,9 +237,15 @@ class EditWorkSchedule(bpy.types.Operator): def export_attributes(self, attributes, prop): if "Date" in prop.name or "Time" in prop.name: + if prop.is_null: + attributes[prop.name] = None + return True attributes[prop.name] = helper.parse_datetime(prop.string_value) return True elif prop.name == "Duration" or prop.name == "TotalFloat": + if prop.is_null: + attributes[prop.name] = None + return True attributes[prop.name] = helper.parse_duration(prop.string_value) return True @@ -192,9 +253,13 @@ class EditWorkSchedule(bpy.types.Operator): class RemoveWorkSchedule(bpy.types.Operator): bl_idname = "bim.remove_work_schedule" bl_label = "Remove Work Schedule" + bl_options = {"REGISTER", "UNDO"} work_schedule: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.remove_work_schedule", self.file, work_schedule=self.file.by_id(self.work_schedule) @@ -206,13 +271,13 @@ class RemoveWorkSchedule(bpy.types.Operator): class EnableEditingWorkSchedule(bpy.types.Operator): bl_idname = "bim.enable_editing_work_schedule" bl_label = "Enable Editing Work Schedule" + bl_options = {"REGISTER", "UNDO"} work_schedule: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMWorkScheduleProperties self.props.active_work_schedule_id = self.work_schedule - while len(self.props.work_schedule_attributes) > 0: - self.props.work_schedule_attributes.remove(0) + self.props.work_schedule_attributes.clear() self.enable_editing_work_schedule() self.props.editing_type = "WORK_SCHEDULE" return {"FINISHED"} @@ -233,14 +298,14 @@ class EnableEditingWorkSchedule(bpy.types.Operator): class EnableEditingTasks(bpy.types.Operator): bl_idname = "bim.enable_editing_tasks" bl_label = "Enable Editing Tasks" + bl_options = {"REGISTER", "UNDO"} work_schedule: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMWorkScheduleProperties self.tprops = context.scene.BIMTaskTreeProperties self.props.active_work_schedule_id = self.work_schedule - while len(self.tprops.tasks) > 0: - self.tprops.tasks.remove(0) + self.tprops.tasks.clear() self.contracted_tasks = json.loads(self.props.contracted_tasks) self.sort_keys = { @@ -295,6 +360,7 @@ class EnableEditingTasks(bpy.types.Operator): class LoadTaskProperties(bpy.types.Operator): bl_idname = "bim.load_task_properties" bl_label = "Load Task Properties" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): @@ -358,6 +424,7 @@ class LoadTaskProperties(bpy.types.Operator): class DisableEditingWorkSchedule(bpy.types.Operator): bl_idname = "bim.disable_editing_work_schedule" bl_label = "Disable Editing Work Schedule" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMWorkScheduleProperties.active_work_schedule_id = 0 @@ -367,12 +434,16 @@ class DisableEditingWorkSchedule(bpy.types.Operator): class AddTask(bpy.types.Operator): bl_idname = "bim.add_task" bl_label = "Add Task" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() - ifcopenshell.api.run("sequence.add_task", self.file, **{"parent_task": self.file.by_id(self.task)}) + ifcopenshell.api.run("sequence.add_task", self.file, parent_task=self.file.by_id(self.task)) Data.load(self.file) bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -381,12 +452,16 @@ class AddTask(bpy.types.Operator): class AddSummaryTask(bpy.types.Operator): bl_idname = "bim.add_summary_task" bl_label = "Add Task" + bl_options = {"REGISTER", "UNDO"} work_schedule: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() - ifcopenshell.api.run("sequence.add_task", self.file, **{"work_schedule": self.file.by_id(self.work_schedule)}) + ifcopenshell.api.run("sequence.add_task", self.file, work_schedule=self.file.by_id(self.work_schedule)) Data.load(self.file) bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) return {"FINISHED"} @@ -395,6 +470,7 @@ class AddSummaryTask(bpy.types.Operator): class ExpandTask(bpy.types.Operator): bl_idname = "bim.expand_task" bl_label = "Expand Task" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): @@ -411,6 +487,7 @@ class ExpandTask(bpy.types.Operator): class ContractTask(bpy.types.Operator): bl_idname = "bim.contract_task" bl_label = "Contract Task" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): @@ -427,9 +504,13 @@ class ContractTask(bpy.types.Operator): class RemoveTask(bpy.types.Operator): bl_idname = "bim.remove_task" bl_label = "Remove Task" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() ifcopenshell.api.run( @@ -444,17 +525,20 @@ class RemoveTask(bpy.types.Operator): class EnableEditingTaskTime(bpy.types.Operator): bl_idname = "bim.enable_editing_task_time" - bl_label = "Enable Editing Task" + bl_label = "Enable Editing Task Time" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() task_time_id = Data.tasks[self.task]["TaskTime"] or self.add_task_time().id() - while len(props.task_time_attributes) > 0: - props.task_time_attributes.remove(0) + props.task_time_attributes.clear() data = Data.task_times[task_time_id] @@ -477,15 +561,19 @@ class EnableEditingTaskTime(bpy.types.Operator): def add_task_time(self): task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=self.file.by_id(self.task)) - Data.load(IfcStore.get_file()) + Data.load(self.file) return task_time class EditTaskTime(bpy.types.Operator): bl_idname = "bim.edit_task_time" bl_label = "Edit Task Time" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties attributes = blenderbim.bim.helper.export_attributes(props.task_time_attributes, self.export_attributes) @@ -502,9 +590,15 @@ class EditTaskTime(bpy.types.Operator): def export_attributes(self, attributes, prop): if "Start" in prop.name or "Finish" in prop.name or prop.name == "StatusTime": + if prop.is_null: + attributes[prop.name] = None + return True attributes[prop.name] = helper.parse_datetime(prop.string_value) return True elif prop.name == "ScheduleDuration": + if prop.is_null: + attributes[prop.name] = None + return True attributes[prop.name] = helper.parse_duration(prop.string_value) return True @@ -512,13 +606,12 @@ class EditTaskTime(bpy.types.Operator): class EnableEditingTask(bpy.types.Operator): bl_idname = "bim.enable_editing_task" bl_label = "Enable Editing Task" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): props = context.scene.BIMWorkScheduleProperties - while len(props.task_attributes) > 0: - props.task_attributes.remove(0) - + props.task_attributes.clear() data = Data.tasks[self.task] blenderbim.bim.helper.import_attributes("IfcTask", props.task_attributes, data) @@ -530,6 +623,7 @@ class EnableEditingTask(bpy.types.Operator): class DisableEditingTask(bpy.types.Operator): bl_idname = "bim.disable_editing_task" bl_label = "Disable Editing Task" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMWorkScheduleProperties.active_task_id = 0 @@ -540,8 +634,12 @@ class DisableEditingTask(bpy.types.Operator): class EditTask(bpy.types.Operator): bl_idname = "bim.edit_task" bl_label = "Edit Task" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties attributes = blenderbim.bim.helper.export_attributes(props.task_attributes) self.file = IfcStore.get_file() @@ -557,9 +655,13 @@ class EditTask(bpy.types.Operator): class CopyTaskAttribute(bpy.types.Operator): bl_idname = "bim.copy_task_attribute" bl_label = "Copy Task Attribute" + bl_options = {"REGISTER", "UNDO"} data: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): data = json.loads(self.data) self.file = IfcStore.get_file() props = context.scene.BIMTaskTreeProperties @@ -581,9 +683,13 @@ class CopyTaskAttribute(bpy.types.Operator): class AssignPredecessor(bpy.types.Operator): bl_idname = "bim.assign_predecessor" bl_label = "Assign Predecessor" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() rel = ifcopenshell.api.run( @@ -600,9 +706,13 @@ class AssignPredecessor(bpy.types.Operator): class AssignSuccessor(bpy.types.Operator): bl_idname = "bim.assign_successor" bl_label = "Assign Successor" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() rel = ifcopenshell.api.run( @@ -619,9 +729,13 @@ class AssignSuccessor(bpy.types.Operator): class UnassignPredecessor(bpy.types.Operator): bl_idname = "bim.unassign_predecessor" bl_label = "Unassign Predecessor" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() ifcopenshell.api.run( @@ -638,9 +752,13 @@ class UnassignPredecessor(bpy.types.Operator): class UnassignSuccessor(bpy.types.Operator): bl_idname = "bim.unassign_successor" bl_label = "Unassign Successor" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties self.file = IfcStore.get_file() ifcopenshell.api.run( @@ -657,15 +775,21 @@ class UnassignSuccessor(bpy.types.Operator): class AssignProduct(bpy.types.Operator): bl_idname = "bim.assign_product" bl_label = "Assign Product" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() relating_product: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() relating_products = ( - [bpy.data.objects.get(self.relating_product)] if self.relating_product else bpy.context.selected_objects + [bpy.data.objects.get(self.relating_product)] if self.relating_product else context.selected_objects ) for relating_product in relating_products: - self.file = IfcStore.get_file() + if not relating_product.BIMObjectProperties.ifc_definition_id: + continue ifcopenshell.api.run( "sequence.assign_product", self.file, @@ -673,21 +797,28 @@ class AssignProduct(bpy.types.Operator): related_object=self.file.by_id(self.task), ) Data.load(self.file) + bpy.ops.bim.load_task_outputs() return {"FINISHED"} class UnassignProduct(bpy.types.Operator): bl_idname = "bim.unassign_product" bl_label = "Unassign Product" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() relating_product: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() relating_products = ( - [bpy.data.objects.get(self.relating_product)] if self.relating_product else bpy.context.selected_objects + [bpy.data.objects.get(self.relating_product)] if self.relating_product else context.selected_objects ) for relating_product in relating_products: - self.file = IfcStore.get_file() + if not relating_product.BIMObjectProperties.ifc_definition_id: + continue ifcopenshell.api.run( "sequence.unassign_product", self.file, @@ -695,51 +826,114 @@ class UnassignProduct(bpy.types.Operator): related_object=self.file.by_id(self.task), ) Data.load(self.file) + bpy.ops.bim.load_task_outputs() return {"FINISHED"} class AssignProcess(bpy.types.Operator): bl_idname = "bim.assign_process" bl_label = "Assign Process" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() + related_object_type: bpy.props.StringProperty() related_object: bpy.props.StringProperty() + resource: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + if self.related_object_type == "RESOURCE": + self.assign_resource() + elif self.related_object_type == "PRODUCT": + self.assign_product(context) + elif self.related_object_type == "CONTROL": + pass # TODO + return {"FINISHED"} + + def assign_resource(self): + task = self.file.by_id(self.task) + resource = self.file.by_id(self.resource) + subresource = ifcopenshell.api.run( + "resource.add_resource", + self.file, + **{"parent_resource": resource, "ifc_class": resource.is_a(), "name": resource.Name}, + ) + ifcopenshell.api.run( + "sequence.assign_process", self.file, **{"related_object": subresource, "relating_process": task} + ) + ResourceData.load(self.file) + Data.load(self.file) + bpy.ops.bim.load_resources() + bpy.ops.bim.load_task_resources() + + def assign_product(self, context): + task = self.file.by_id(self.task) related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects ) for related_object in related_objects: - self.file = IfcStore.get_file() + if not related_object.BIMObjectProperties.ifc_definition_id: + continue ifcopenshell.api.run( "sequence.assign_process", self.file, related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id), - relating_process=self.file.by_id(self.task), + relating_process=task, ) Data.load(self.file) - return {"FINISHED"} + bpy.ops.bim.load_task_inputs() class UnassignProcess(bpy.types.Operator): bl_idname = "bim.unassign_process" bl_label = "Unassign Process" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() + related_object_type: bpy.props.StringProperty() related_object: bpy.props.StringProperty() + resource: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + if self.related_object_type == "RESOURCE": + self.unassign_resource() + elif self.related_object_type == "PRODUCT": + self.unassign_product(context) + elif self.related_object_type == "CONTROL": + pass # TODO + return {"FINISHED"} + + def unassign_resource(self): + task = self.file.by_id(self.task) + resource = self.file.by_id(self.resource) + ifcopenshell.api.run("sequence.unassign_process", self.file, related_object=resource, relating_process=task) + ifcopenshell.api.run("resource.remove_resource", self.file, resource=resource) + ResourceData.load(self.file) + Data.load(self.file) + bpy.ops.bim.load_resources() + bpy.ops.bim.load_task_resources() + + def unassign_product(self, context): + task = self.file.by_id(self.task) related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects ) for related_object in related_objects: - self.file = IfcStore.get_file() + if not related_object.BIMObjectProperties.ifc_definition_id: + continue ifcopenshell.api.run( "sequence.unassign_process", self.file, related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id), - relating_process=self.file.by_id(self.task), + relating_process=task, ) Data.load(self.file) - return {"FINISHED"} + bpy.ops.bim.load_task_inputs() class GenerateGanttChart(bpy.types.Operator): @@ -761,10 +955,10 @@ class GenerateGanttChart(bpy.types.Operator): } for task_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]: self.create_new_task_json(task_id) - with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f: - with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t: + with open(os.path.join(context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f: + with open(os.path.join(context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t: f.write(pystache.render(t.read(), {"json_data": json.dumps(self.json)})) - webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html")) + webbrowser.open("file://" + os.path.join(context.scene.BIMProperties.data_dir, "gantt", "index.html")) return {"FINISHED"} def create_new_task_json(self, task_id): @@ -805,8 +999,12 @@ class GenerateGanttChart(bpy.types.Operator): class AddWorkCalendar(bpy.types.Operator): bl_idname = "bim.add_work_calendar" bl_label = "Add Work Calendar" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): ifcopenshell.api.run("sequence.add_work_calendar", IfcStore.get_file()) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -815,8 +1013,12 @@ class AddWorkCalendar(bpy.types.Operator): class EditWorkCalendar(bpy.types.Operator): bl_idname = "bim.edit_work_calendar" bl_label = "Edit Work Calendar" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkCalendarProperties attributes = blenderbim.bim.helper.export_attributes(props.work_calendar_attributes) self.file = IfcStore.get_file() @@ -833,9 +1035,13 @@ class EditWorkCalendar(bpy.types.Operator): class RemoveWorkCalendar(bpy.types.Operator): bl_idname = "bim.remove_work_calendar" bl_label = "Remove Work Plan" + bl_options = {"REGISTER", "UNDO"} work_calendar: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.remove_work_calendar", self.file, **{"work_calendar": self.file.by_id(self.work_calendar)} @@ -847,12 +1053,12 @@ class RemoveWorkCalendar(bpy.types.Operator): class EnableEditingWorkCalendar(bpy.types.Operator): bl_idname = "bim.enable_editing_work_calendar" bl_label = "Enable Editing Work Calendar" + bl_options = {"REGISTER", "UNDO"} work_calendar: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMWorkCalendarProperties - while len(self.props.work_calendar_attributes) > 0: - self.props.work_calendar_attributes.remove(0) + self.props.work_calendar_attributes.clear() data = Data.work_calendars[self.work_calendar] @@ -865,6 +1071,7 @@ class EnableEditingWorkCalendar(bpy.types.Operator): class DisableEditingWorkCalendar(bpy.types.Operator): bl_idname = "bim.disable_editing_work_calendar" bl_label = "Disable Editing Work Calendar" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMWorkCalendarProperties.active_work_calendar_id = 0 @@ -874,11 +1081,12 @@ class DisableEditingWorkCalendar(bpy.types.Operator): class ImportP6(bpy.types.Operator, ImportHelper): bl_idname = "import_p6.bim" bl_label = "Import P6" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".xml" filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"}) def execute(self, context): - from ifcp6.p62ifc import P62Ifc + from ifc4d.p62ifc import P62Ifc self.file = IfcStore.get_file() start = time.time() @@ -895,11 +1103,12 @@ class ImportP6(bpy.types.Operator, ImportHelper): class ImportMSP(bpy.types.Operator, ImportHelper): bl_idname = "import_msp.bim" bl_label = "Import MSP" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".xml" filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"}) def execute(self, context): - from ifcp6.msp2ifc import MSP2Ifc + from ifc4d.msp2ifc import MSP2Ifc self.file = IfcStore.get_file() start = time.time() @@ -916,6 +1125,7 @@ class ImportMSP(bpy.types.Operator, ImportHelper): class EnableEditingWorkCalendarTimes(bpy.types.Operator): bl_idname = "bim.enable_editing_work_calendar_times" bl_label = "Enable Editing Work Calendar Times" + bl_options = {"REGISTER", "UNDO"} work_calendar: bpy.props.IntProperty() def execute(self, context): @@ -928,10 +1138,14 @@ class EnableEditingWorkCalendarTimes(bpy.types.Operator): class AddWorkTime(bpy.types.Operator): bl_idname = "bim.add_work_time" bl_label = "Add Work Time" + bl_options = {"REGISTER", "UNDO"} work_calendar: bpy.props.IntProperty() time_type: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.add_work_time", @@ -945,12 +1159,12 @@ class AddWorkTime(bpy.types.Operator): class EnableEditingWorkTime(bpy.types.Operator): bl_idname = "bim.enable_editing_work_time" bl_label = "Enable Editing Work Time" + bl_options = {"REGISTER", "UNDO"} work_time: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMWorkCalendarProperties - while len(self.props.work_time_attributes) > 0: - self.props.work_time_attributes.remove(0) + self.props.work_time_attributes.clear() data = Data.work_times[self.work_time] @@ -1011,6 +1225,7 @@ class EnableEditingWorkTime(bpy.types.Operator): class DisableEditingWorkTime(bpy.types.Operator): bl_idname = "bim.disable_editing_work_time" bl_label = "Disable Editing Work Time" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMWorkCalendarProperties.active_work_time_id = 0 @@ -1020,8 +1235,12 @@ class DisableEditingWorkTime(bpy.types.Operator): class EditWorkTime(bpy.types.Operator): bl_idname = "bim.edit_work_time" bl_label = "Edit Work Time" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.props = context.scene.BIMWorkCalendarProperties attributes = blenderbim.bim.helper.export_attributes(self.props.work_time_attributes) self.file = IfcStore.get_file() @@ -1075,9 +1294,13 @@ class EditWorkTime(bpy.types.Operator): class RemoveWorkTime(bpy.types.Operator): bl_idname = "bim.remove_work_time" bl_label = "Remove Work Plan" + bl_options = {"REGISTER", "UNDO"} work_time: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run("sequence.remove_work_time", self.file, **{"work_time": self.file.by_id(self.work_time)}) Data.load(self.file) @@ -1087,10 +1310,14 @@ class RemoveWorkTime(bpy.types.Operator): class AssignRecurrencePattern(bpy.types.Operator): bl_idname = "bim.assign_recurrence_pattern" bl_label = "Assign Recurrence Pattern" + bl_options = {"REGISTER", "UNDO"} work_time: bpy.props.IntProperty() recurrence_type: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.assign_recurrence_pattern", @@ -1104,9 +1331,13 @@ class AssignRecurrencePattern(bpy.types.Operator): class UnassignRecurrencePattern(bpy.types.Operator): bl_idname = "bim.unassign_recurrence_pattern" bl_label = "Unassign Recurrence Pattern" + bl_options = {"REGISTER", "UNDO"} recurrence_pattern: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.unassign_recurrence_pattern", @@ -1120,9 +1351,13 @@ class UnassignRecurrencePattern(bpy.types.Operator): class AddTimePeriod(bpy.types.Operator): bl_idname = "bim.add_time_period" bl_label = "Add Time Period" + bl_options = {"REGISTER", "UNDO"} recurrence_pattern: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.props = context.scene.BIMWorkCalendarProperties self.file = IfcStore.get_file() try: @@ -1148,9 +1383,13 @@ class AddTimePeriod(bpy.types.Operator): class RemoveTimePeriod(bpy.types.Operator): bl_idname = "bim.remove_time_period" bl_label = "Remove Time Period" + bl_options = {"REGISTER", "UNDO"} time_period: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.remove_time_period", @@ -1164,6 +1403,7 @@ class RemoveTimePeriod(bpy.types.Operator): class EnableEditingTaskCalendar(bpy.types.Operator): bl_idname = "bim.enable_editing_task_calendar" bl_label = "Enable Editing Task Calendar" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): @@ -1176,10 +1416,14 @@ class EnableEditingTaskCalendar(bpy.types.Operator): class EditTaskCalendar(bpy.types.Operator): bl_idname = "bim.edit_task_calendar" bl_label = "Edit Task Calendar" + bl_options = {"REGISTER", "UNDO"} work_calendar: bpy.props.IntProperty() task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() task = self.file.by_id(self.task) ifcopenshell.api.run( @@ -1199,10 +1443,14 @@ class EditTaskCalendar(bpy.types.Operator): class RemoveTaskCalendar(bpy.types.Operator): bl_idname = "bim.remove_task_calendar" bl_label = "Remove Task Calendar" + bl_options = {"REGISTER", "UNDO"} work_calendar: bpy.props.IntProperty() task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() task = self.file.by_id(self.task) ifcopenshell.api.run( @@ -1235,6 +1483,7 @@ class EnableEditingTaskSequence(bpy.types.Operator): class DisableEditingTaskTime(bpy.types.Operator): bl_idname = "bim.disable_editing_task_time" bl_label = "Disable Editing Task Time" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMWorkScheduleProperties.active_task_time_id = 0 @@ -1245,14 +1494,14 @@ class DisableEditingTaskTime(bpy.types.Operator): class EnableEditingSequenceAttributes(bpy.types.Operator): bl_idname = "bim.enable_editing_sequence_attributes" bl_label = "Enable Editing Sequence Attributes" + bl_options = {"REGISTER", "UNDO"} sequence: bpy.props.IntProperty() def execute(self, context): self.props = context.scene.BIMWorkScheduleProperties self.props.active_sequence_id = self.sequence self.props.editing_sequence_type = "ATTRIBUTES" - while len(self.props.sequence_attributes) > 0: - self.props.sequence_attributes.remove(0) + self.props.sequence_attributes.clear() self.enable_editing_sequence_attributes() return {"FINISHED"} @@ -1264,6 +1513,7 @@ class EnableEditingSequenceAttributes(bpy.types.Operator): class EnableEditingSequenceTimeLag(bpy.types.Operator): bl_idname = "bim.enable_editing_sequence_time_lag" bl_label = "Enable Editing Sequence Time Lag" + bl_options = {"REGISTER", "UNDO"} sequence: bpy.props.IntProperty() lag_time: bpy.props.IntProperty() @@ -1271,8 +1521,7 @@ class EnableEditingSequenceTimeLag(bpy.types.Operator): self.props = context.scene.BIMWorkScheduleProperties self.props.active_sequence_id = self.sequence self.props.editing_sequence_type = "TIME_LAG" - while len(self.props.time_lag_attributes) > 0: - self.props.time_lag_attributes.remove(0) + self.props.time_lag_attributes.clear() self.enable_editing_attributes() return {"FINISHED"} @@ -1299,9 +1548,13 @@ class EnableEditingSequenceTimeLag(bpy.types.Operator): class UnassignLagTime(bpy.types.Operator): bl_idname = "bim.unassign_lag_time" bl_label = "Unassign Time Lag" + bl_options = {"REGISTER", "UNDO"} sequence: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.unassign_lag_time", self.file, **{"rel_sequence": self.file.by_id(self.sequence)} @@ -1314,9 +1567,13 @@ class UnassignLagTime(bpy.types.Operator): class AssignLagTime(bpy.types.Operator): bl_idname = "bim.assign_lag_time" bl_label = "Assign Time Lag" + bl_options = {"REGISTER", "UNDO"} sequence: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.assign_lag_time", @@ -1330,8 +1587,12 @@ class AssignLagTime(bpy.types.Operator): class EditSequenceAttributes(bpy.types.Operator): bl_idname = "bim.edit_sequence_attributes" bl_label = "Edit Sequence" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties attributes = blenderbim.bim.helper.export_attributes(props.sequence_attributes) self.file = IfcStore.get_file() @@ -1349,9 +1610,13 @@ class EditSequenceAttributes(bpy.types.Operator): class EditSequenceTimeLag(bpy.types.Operator): bl_idname = "bim.edit_sequence_time_lag" bl_label = "Edit Time Lag" + bl_options = {"REGISTER", "UNDO"} lag_time: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): props = context.scene.BIMWorkScheduleProperties attributes = blenderbim.bim.helper.export_attributes(props.time_lag_attributes) self.file = IfcStore.get_file() @@ -1369,6 +1634,7 @@ class EditSequenceTimeLag(bpy.types.Operator): class DisableEditingSequence(bpy.types.Operator): bl_idname = "bim.disable_editing_sequence" bl_label = "Disable Editing Sequence Attributes" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): context.scene.BIMWorkScheduleProperties.active_sequence_id = 0 @@ -1378,14 +1644,18 @@ class DisableEditingSequence(bpy.types.Operator): class SelectTaskRelatedProducts(bpy.types.Operator): bl_idname = "bim.select_task_related_products" bl_label = "Select Similar Type" + bl_options = {"REGISTER", "UNDO"} task: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() related_products = ifcopenshell.api.run( "sequence.get_related_products", self.file, **{"related_object": self.file.by_id(self.task)} ) - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: obj.select_set(False) if obj.BIMObjectProperties.ifc_definition_id in related_products: obj.select_set(True) @@ -1395,6 +1665,7 @@ class SelectTaskRelatedProducts(bpy.types.Operator): class VisualiseWorkScheduleDate(bpy.types.Operator): bl_idname = "bim.visualise_work_schedule_date" bl_label = "Visualise Work Schedule Date" + bl_options = {"REGISTER", "UNDO"} work_schedule: bpy.props.IntProperty() def execute(self, context): @@ -1450,6 +1721,7 @@ class VisualiseWorkScheduleDate(bpy.types.Operator): class VisualiseWorkScheduleDateRange(bpy.types.Operator): bl_idname = "bim.visualise_work_schedule_date_range" bl_label = "Visualise Work Schedule Date Range" + bl_options = {"REGISTER", "UNDO"} work_schedule: bpy.props.IntProperty() def execute(self, context): @@ -1459,7 +1731,7 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): self.finish = parser.parse(self.props.visualisation_finish, dayfirst=True, fuzzy=True) self.duration = self.finish - self.start self.start_frame = 1 - self.total_frames = self.calculate_total_frames() + self.total_frames = self.calculate_total_frames(context) self.preprocess_tasks() for obj in bpy.data.objects: if not obj.BIMObjectProperties.ifc_definition_id: @@ -1470,13 +1742,48 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): self.animate_input(obj, product_frame) elif product_frame["relationship"] == "output": self.animate_output(obj, product_frame) + self.add_text_animation_handler() area = next(area for area in context.screen.areas if area.type == "VIEW_3D") area.spaces[0].shading.color_type = "OBJECT" context.scene.frame_start = self.start_frame - context.scene.frame_end = self.start_frame + self.total_frames + context.scene.frame_end = int(self.start_frame + self.total_frames) + # with open("/home/dion/animation.json", "w") as json_file: + # guid_frames = {} + # for k, v in self.product_frames.items(): + # guid_frames[self.file.by_id(k).GlobalId] = v + # json.dump(guid_frames, json_file) return {"FINISHED"} + def add_text_animation_handler(self): + data = bpy.data.curves.get("Timeline") + if not data: + data = bpy.data.curves.new(type="FONT", name="Timeline") + obj = bpy.data.objects.get("Timeline") + if not obj: + obj = bpy.data.objects.new(name="Timeline", object_data=data) + bpy.context.scene.collection.objects.link(obj) + obj.data.BIMDateTextProperties.start_frame = self.start_frame + obj.data.BIMDateTextProperties.total_frames = int(self.total_frames) + obj.data.BIMDateTextProperties.start = self.props.visualisation_start + obj.data.BIMDateTextProperties.finish = self.props.visualisation_finish + bpy.app.handlers.frame_change_post.append(self.animate_text) + + def remove_text_animation_handler(self): + bpy.app.handlers.frame_change_post.remove(self.animate_text) + + def animate_text(self, scene, context): + data = bpy.data.curves.get("Timeline") + if not data or not bpy.data.objects.get("Timeline"): + self.remove_text_animation_handler() + scene.frame_current + props = data.BIMDateTextProperties + start = parser.parse(props.start, dayfirst=True, fuzzy=True) + finish = parser.parse(props.finish, dayfirst=True, fuzzy=True) + duration = finish - start + frame_date = (((scene.frame_current - props.start_frame) / props.total_frames) * duration) + start + data.body = frame_date.date().isoformat() + def animate_input(self, obj, product_frame): if product_frame["type"] in ["LOGISTIC", "MOVE", "DISPOSAL"]: self.animate_movement_from(obj, product_frame) @@ -1497,24 +1804,34 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): def animate_creation(self, obj, product_frame): obj.hide_viewport = True + obj.hide_render = True obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame) + obj.keyframe_insert(data_path="hide_render", frame=self.start_frame) obj.hide_viewport = False + obj.hide_render = False obj.color = (0.0, 1.0, 0.0, 1) obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"]) obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) obj.color = (1.0, 1.0, 1.0, 1) obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) def animate_destruction(self, obj, product_frame): obj.color = (1.0, 1.0, 1.0, 1) + obj.hide_viewport = False + obj.hide_render = False obj.keyframe_insert(data_path="color", frame=self.start_frame) + obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame) + obj.keyframe_insert(data_path="hide_render", frame=self.start_frame) obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) obj.color = (1.0, 0.0, 0.0, 1) obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) obj.hide_viewport = True + obj.hide_render = True obj.color = (0.0, 0.0, 0.0, 1) obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) def animate_operation(self, obj, product_frame): obj.color = (1.0, 1.0, 1.0, 1) @@ -1527,10 +1844,14 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): def animate_movement_to(self, obj, product_frame): obj.hide_viewport = True + obj.hide_render = True obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame) + obj.keyframe_insert(data_path="hide_render", frame=self.start_frame) obj.hide_viewport = False + obj.hide_render = False obj.color = (1.0, 1.0, 0.0, 1) obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"]) obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) obj.color = (1.0, 1.0, 1.0, 1) obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) @@ -1538,26 +1859,38 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): def animate_movement_from(self, obj, product_frame): obj.color = (1.0, 1.0, 1.0, 1) obj.keyframe_insert(data_path="color", frame=self.start_frame) + obj.hide_viewport = False + obj.hide_render = False obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"] - 1) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"] - 1) obj.color = (1.0, 0.5, 0.0, 1) obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) obj.hide_viewport = True + obj.hide_render = True obj.color = (0.0, 0.0, 0.0, 1) obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) def animate_consumption(self, obj, product_frame): obj.color = (1.0, 1.0, 1.0, 1) obj.keyframe_insert(data_path="color", frame=self.start_frame) + obj.hide_viewport = False + obj.hide_render = False obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1) + obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"] - 1) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["STARTED"] - 1) obj.color = (0.0, 1.0, 1.0, 1) obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"]) obj.hide_viewport = True + obj.hide_render = True obj.color = (0.0, 0.0, 0.0, 1) obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"]) obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"]) + obj.keyframe_insert(data_path="hide_render", frame=product_frame["COMPLETED"]) - def calculate_total_frames(self): + def calculate_total_frames(self, context): if self.props.speed_types == "FRAME_SPEED": return self.calculate_using_frames( self.start, @@ -1569,7 +1902,7 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): return self.calculate_using_duration( self.start, self.finish, - bpy.context.scene.render.fps, + context.scene.render.fps, isodate.parse_duration(self.props.speed_animation_duration), isodate.parse_duration(self.props.speed_real_duration), ) @@ -1577,7 +1910,7 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): return self.calculate_using_multiplier( self.start, self.finish, - bpy.context.scene.render.fps, + context.scene.render.fps, self.props.speed_multiplier, ) @@ -1606,13 +1939,9 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): finish = helper.derive_date(task.id(), "ScheduleFinish", is_latest=True) if not start or not finish: return - output_ids = [r.RelatingProduct.id() for r in task.HasAssignments or [] if r.is_a("IfcRelAssignsToProduct")] - for output_id in output_ids: + for output_id in Data.tasks[task.id()]["Outputs"]: self.add_product_frame(output_id, task, start, finish, "output") - - input_ids = [] - [input_ids.extend([o.id() for o in r.RelatedObjects]) for r in task.OperatesOn or []] - for input_id in input_ids: + for input_id in Data.tasks[task.id()]["Inputs"]: self.add_product_frame(input_id, task, start, finish, "input") def add_product_frame(self, product_id, task, start, finish, relationship): @@ -1629,6 +1958,7 @@ class VisualiseWorkScheduleDateRange(bpy.types.Operator): class BlenderBIM_DatePicker(bpy.types.Operator): bl_label = "Date Picker" bl_idname = "bim.datepicker" + bl_options = {"REGISTER", "UNDO"} display_date: bpy.props.StringProperty(name="Display Date") selected_date: bpy.props.StringProperty(name="Selected Date") target_prop: bpy.props.StringProperty(name="Target date prop to set") @@ -1689,6 +2019,7 @@ class BlenderBIM_DatePicker(bpy.types.Operator): class BlenderBIM_DatePickerSetDate(bpy.types.Operator): bl_label = "set date" bl_idname = "bim.datepicker_setdate" + bl_options = {"REGISTER", "UNDO"} selected_date: bpy.props.StringProperty() def invoke(self, context, event): @@ -1699,6 +2030,7 @@ class BlenderBIM_DatePickerSetDate(bpy.types.Operator): class BlenderBIM_RedrawDatePicker(bpy.types.Operator): bl_label = "redraw datepicker window" bl_idname = "bim.redraw_datepicker" + bl_options = {"REGISTER", "UNDO"} action: bpy.props.StringProperty() def invoke(self, context, event): @@ -1717,9 +2049,13 @@ class BlenderBIM_RedrawDatePicker(bpy.types.Operator): class RecalculateSchedule(bpy.types.Operator): bl_idname = "bim.recalculate_schedule" bl_label = "Recalculate Schedule" + bl_options = {"REGISTER", "UNDO"} work_schedule: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() ifcopenshell.api.run( "sequence.recalculate_schedule", self.file, work_schedule=self.file.by_id(self.work_schedule) @@ -1731,6 +2067,7 @@ class RecalculateSchedule(bpy.types.Operator): class AddTaskColumn(bpy.types.Operator): bl_idname = "bim.add_task_column" bl_label = "Add Task Column" + bl_options = {"REGISTER", "UNDO"} column_type: bpy.props.StringProperty() name: bpy.props.StringProperty() data_type: bpy.props.StringProperty() @@ -1746,16 +2083,19 @@ class AddTaskColumn(bpy.types.Operator): class RemoveTaskColumn(bpy.types.Operator): bl_idname = "bim.remove_task_column" bl_label = "Remove Task Column" + bl_options = {"REGISTER", "UNDO"} + name: bpy.props.StringProperty() def execute(self, context): self.props = context.scene.BIMWorkScheduleProperties - self.props.columns.remove(self.props.active_column_index) + self.props.columns.remove(self.props.columns.find(self.name)) return {"FINISHED"} class SetTaskSortColumn(bpy.types.Operator): bl_idname = "bim.set_task_sort_column" bl_label = "Set Task Sort Column" + bl_options = {"REGISTER", "UNDO"} column: bpy.props.StringProperty() def execute(self, context): @@ -1763,3 +2103,75 @@ class SetTaskSortColumn(bpy.types.Operator): self.props.sort_column = self.column bpy.ops.bim.enable_editing_tasks(work_schedule=self.props.active_work_schedule_id) return {"FINISHED"} + + +class LoadTaskResources(bpy.types.Operator): + bl_idname = "bim.load_task_resources" + bl_label = "Load Task Resources" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + self.file = IfcStore.get_file() + self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties + ifc_definition_id = self.tprops.tasks[self.props.active_task_index].ifc_definition_id + self.props.task_resources.clear() + for resource_id in Data.tasks[ifc_definition_id]["Resources"]: + resource = self.file.by_id(resource_id) + new = self.props.task_resources.add() + new.ifc_definition_id = resource_id + new.name = resource.Name or "Unnamed" + return {"FINISHED"} + + +class LoadTaskInputs(bpy.types.Operator): + bl_idname = "bim.load_task_inputs" + bl_label = "Load Task Inputs" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + self.file = IfcStore.get_file() + self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties + ifc_definition_id = self.tprops.tasks[self.props.active_task_index].ifc_definition_id + self.props.task_inputs.clear() + for input_id in Data.tasks[ifc_definition_id]["Inputs"]: + product = self.file.by_id(input_id) + new = self.props.task_inputs.add() + new.ifc_definition_id = input_id + new.name = product.Name or "Unnamed" + return {"FINISHED"} + + +class LoadTaskOutputs(bpy.types.Operator): + bl_idname = "bim.load_task_outputs" + bl_label = "Load Task Outputs" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + self.file = IfcStore.get_file() + self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties + ifc_definition_id = self.tprops.tasks[self.props.active_task_index].ifc_definition_id + self.props.task_outputs.clear() + for output_id in Data.tasks[ifc_definition_id]["Outputs"]: + product = self.file.by_id(output_id) + new = self.props.task_outputs.add() + new.ifc_definition_id = output_id + new.name = product.Name or "Unnamed" + return {"FINISHED"} + + +class CalculateTaskDuration(bpy.types.Operator): + bl_idname = "bim.calculate_task_duration" + bl_label = "Calculate Task Duration" + bl_options = {"REGISTER", "UNDO"} + task: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMWorkScheduleProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("sequence.calculate_task_duration", self.file, task=self.file.by_id(self.task)) + Data.load(self.file) + bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/sequence/prop.py b/src/blenderbim/blenderbim/bim/module/sequence/prop.py index 02e6e6cbde..7f002e8197 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/prop.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/prop.py @@ -1,8 +1,27 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api import ifcopenshell.util.attribute from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.sequence.data import Data +from ifcopenshell.api.resource.data import Data as ResourceData from blenderbim.bim.prop import StrProperty, Attribute from dateutil import parser from bpy.types import PropertyGroup @@ -65,6 +84,12 @@ def getWorkCalendars(self, context): return [(str(k), v["Name"], "") for k, v in Data.work_calendars.items()] +def update_active_task_index(self, context): + bpy.ops.bim.load_task_inputs() + bpy.ops.bim.load_task_resources() + bpy.ops.bim.load_task_outputs() + + def updateTaskName(self, context): props = context.scene.BIMWorkScheduleProperties if not props.is_task_update_enabled or self.name == "Unnamed": @@ -116,7 +141,6 @@ def updateTaskTimeDateTime(self, context, startfinish): return "-" return time.strftime("%d/%m/%y") - startfinish_key = "Schedule" + startfinish.capitalize() startfinish_value = getattr(self, startfinish) if startfinish_value == "-": @@ -140,6 +164,7 @@ def updateTaskTimeDateTime(self, context, startfinish): task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=task) Data.load(IfcStore.get_file()) + startfinish_key = "Schedule" + startfinish.capitalize() if Data.task_times[task_time.id()][startfinish_key] == startfinish_datetime: canonical_startfinish_value = canonicalise_time(startfinish_datetime) if startfinish_value != canonical_startfinish_value: @@ -153,10 +178,9 @@ def updateTaskTimeDateTime(self, context, startfinish): ) Data.load(IfcStore.get_file()) bpy.ops.bim.load_task_properties() - setattr(self, startfinish, canonicalise_time(startfinish_datetime)) -def updateTaskduration(self, context): +def updateTaskDuration(self, context): props = context.scene.BIMWorkScheduleProperties if not props.is_task_update_enabled: return @@ -180,15 +204,15 @@ def updateTaskduration(self, context): bpy.ops.bim.load_task_properties() -def updateVisualisationStart(self, context): - updateVisualisationStartFinish(self, context, "visualisation_start") +def update_visualisation_start(self, context): + update_visualisation_start_finish(self, context, "visualisation_start") -def updateVisualisationFinish(self, context): - updateVisualisationStartFinish(self, context, "visualisation_finish") +def update_visualisation_finish(self, context): + update_visualisation_start_finish(self, context, "visualisation_finish") -def updateVisualisationStartFinish(self, context, startfinish): +def update_visualisation_start_finish(self, context, startfinish): def canonicalise_time(time): if not time: return "-" @@ -216,7 +240,7 @@ class Task(PropertyGroup): is_selected: BoolProperty(name="Is Selected") is_expanded: BoolProperty(name="Is Expanded") level_index: IntProperty(name="Level Index") - duration: StringProperty(name="Duration", update=updateTaskduration) + duration: StringProperty(name="Duration", update=updateTaskDuration) start: StringProperty(name="Start", update=updateTaskTimeStart) finish: StringProperty(name="Finish", update=updateTaskTimeFinish) calendar: StringProperty(name="Calendar") @@ -233,6 +257,16 @@ class WorkPlan(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") +class TaskResource(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + +class TaskProduct(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + class BIMWorkPlanProperties(PropertyGroup): work_plan_attributes: CollectionProperty(name="Work Plan Attributes", type=Attribute) editing_type: StringProperty(name="Editing Type") @@ -249,7 +283,7 @@ class BIMWorkScheduleProperties(PropertyGroup): editing_task_type: StringProperty(name="Editing Task Type") active_work_schedule_index: IntProperty(name="Active Work Schedules Index") active_work_schedule_id: IntProperty(name="Active Work Schedules Id") - active_task_index: IntProperty(name="Active Task Index") + active_task_index: IntProperty(name="Active Task Index", update=update_active_task_index) active_task_id: IntProperty(name="Active Task Id") task_attributes: CollectionProperty(name="Task Attributes", type=Attribute) should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False) @@ -274,7 +308,7 @@ class BIMWorkScheduleProperties(PropertyGroup): ], name="Special Columns", ) - active_task_time_id: IntProperty(name="Active Task Id") + active_task_time_id: IntProperty(name="Active Task Time Id") task_time_attributes: CollectionProperty(name="Task Time Attributes", type=Attribute) contracted_tasks: StringProperty(name="Contracted Task Items", default="[]") is_task_update_enabled: BoolProperty(name="Is Task Update Enabled", default=True) @@ -282,8 +316,8 @@ class BIMWorkScheduleProperties(PropertyGroup): active_sequence_id: IntProperty(name="Active Sequence Id") sequence_attributes: CollectionProperty(name="Sequence Attributes", type=Attribute) time_lag_attributes: CollectionProperty(name="Time Lag Attributes", type=Attribute) - visualisation_start: StringProperty(name="Visualisation Start", update=updateVisualisationStart) - visualisation_finish: StringProperty(name="Visualisation Finish", update=updateVisualisationFinish) + visualisation_start: StringProperty(name="Visualisation Start", update=update_visualisation_start) + visualisation_finish: StringProperty(name="Visualisation Finish", update=update_visualisation_finish) speed_multiplier: FloatProperty(name="Speed Multiplier", default=10000) speed_animation_duration: StringProperty(name="Speed Animation Duration", default="PT1S") speed_animation_frames: IntProperty(name="Speed Animation Frames", default=24) @@ -297,6 +331,12 @@ class BIMWorkScheduleProperties(PropertyGroup): name="Speed Type", default="FRAME_SPEED", ) + task_resources: CollectionProperty(name="Task Resources", type=TaskResource) + active_task_resource_index: IntProperty(name="Active Task Resource Index") + task_inputs: CollectionProperty(name="Task Inputs", type=TaskProduct) + active_task_input_index: IntProperty(name="Active Task Input Index") + task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct) + active_task_output_index: IntProperty(name="Active Task Output Index") class BIMTaskTreeProperties(PropertyGroup): @@ -348,3 +388,10 @@ class BIMWorkCalendarProperties(PropertyGroup): class DatePickerProperties(PropertyGroup): display_date: StringProperty() selected_date: StringProperty() + + +class BIMDateTextProperties(PropertyGroup): + start_frame: IntProperty(name="Start Frame") + total_frames: IntProperty(name="Total Frames") + start: StringProperty(name="Start") + finish: StringProperty(name="Finish") diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 529d48bd9e..37c4bd138e 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -1,8 +1,28 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import isodate import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.sequence.data import Data +from ifcopenshell.api.resource.data import Data as ResourceData import blenderbim.bim.module.sequence.helper as helper from datetime import datetime @@ -17,7 +37,8 @@ class BIM_PT_work_plans(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and file.schema != "IFC2X3" def draw(self, context): if not Data.is_loaded: @@ -53,14 +74,7 @@ class BIM_PT_work_plans(Panel): self.draw_work_schedule_ui() def draw_editable_ui(self): - for attribute in self.props.work_plan_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.work_plan_attributes, self.layout) def draw_work_schedule_ui(self): row = self.layout.row(align=True) @@ -88,7 +102,8 @@ class BIM_PT_work_schedules(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): self.props = context.scene.BIMWorkScheduleProperties @@ -136,6 +151,31 @@ class BIM_PT_work_schedules(Panel): elif self.props.editing_type == "TASKS": self.draw_editable_task_ui(work_schedule_id) + def draw_task_operators(self): + row = self.layout.row(align=True) + row.alignment = "RIGHT" + ifc_definition_id = None + if self.tprops.tasks and self.props.active_task_index < len(self.tprops.tasks): + task = self.tprops.tasks[self.props.active_task_index] + ifc_definition_id = task.ifc_definition_id + if ifc_definition_id: + if self.props.active_task_id == ifc_definition_id: + if self.props.editing_task_type == "TASKTIME": + row.operator("bim.edit_task_time", text="", icon="CHECKMARK") + elif self.props.editing_task_type == "ATTRIBUTES": + row.operator("bim.edit_task", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_task", text="", icon="CANCEL") + elif self.props.active_task_id: + row.operator("bim.add_task", text="", icon="ADD").task = ifc_definition_id + row.operator("bim.remove_task", text="", icon="X").task = ifc_definition_id + else: + row.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING").task = ifc_definition_id + 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 + row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = ifc_definition_id + row.operator("bim.add_task", text="", icon="ADD").task = ifc_definition_id + row.operator("bim.remove_task", text="", icon="X").task = ifc_definition_id + def draw_column_ui(self): row = self.layout.row(align=True) row.prop(self.props, "column_types", text="") @@ -186,16 +226,10 @@ class BIM_PT_work_schedules(Panel): row.prop(self.props, "speed_multiplier", text="") def draw_editable_work_schedule_ui(self): - for attribute in self.props.work_schedule_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.work_schedule_attributes, self.layout) def draw_editable_task_ui(self, work_schedule_id): + self.draw_task_operators() self.layout.template_list( "BIM_UL_tasks", "", @@ -240,12 +274,12 @@ class BIM_PT_work_schedules(Panel): if self.props.active_sequence_id == sequence["id"]: if self.props.editing_sequence_type == "ATTRIBUTES": row.operator("bim.edit_sequence_attributes", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_sequence", text="", icon="X") + row.operator("bim.disable_editing_sequence", text="", icon="CANCEL") self.draw_editable_sequence_attributes_ui() elif self.props.editing_sequence_type == "TIME_LAG": op = row.operator("bim.edit_sequence_time_lag", text="", icon="CHECKMARK") op.lag_time = sequence["TimeLag"] - row.operator("bim.disable_editing_sequence", text="", icon="X") + row.operator("bim.disable_editing_sequence", text="", icon="CANCEL") self.draw_editable_sequence_time_lag_ui() else: if sequence["TimeLag"]: @@ -286,6 +320,104 @@ class BIM_PT_work_schedules(Panel): blenderbim.bim.helper.draw_attributes(self.props.task_time_attributes, self.layout) +class BIM_PT_task_icom(Panel): + bl_label = "IFC Task ICOM" + bl_idname = "BIM_PT_task_icom" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_parent_id = "BIM_PT_work_schedules" + + @classmethod + def poll(cls, context): + props = context.scene.BIMWorkScheduleProperties + if not props.active_work_schedule_id: + return False + total_tasks = len(context.scene.BIMTaskTreeProperties.tasks) + if total_tasks > 0 and props.active_task_index < total_tasks: + return True + return False + + def draw(self, context): + self.props = context.scene.BIMWorkScheduleProperties + self.tprops = context.scene.BIMTaskTreeProperties + task = self.tprops.tasks[self.props.active_task_index] + + grid = self.layout.grid_flow(columns=3, even_columns=True) + + # Column1 + col = grid.column() + + row2 = col.row(align=True) + row2.label(text="Inputs") + + if context.selected_objects: + op = row2.operator("bim.assign_process", icon="ADD", text="") + op.task = task.ifc_definition_id + op.related_object_type = "PRODUCT" + op.related_object = "" + op = row2.operator("bim.unassign_process", icon="REMOVE", text="") + op.task = task.ifc_definition_id + op.related_object_type = "PRODUCT" + op.related_object = "" + + row2 = col.row() + row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index") + + # Column2 + col = grid.column() + + row2 = col.row(align=True) + row2.label(text="Resources") + + op = row2.operator("bim.calculate_task_duration", text="", icon="TEMP") + op.task = task.ifc_definition_id + + total_resources = len(context.scene.BIMResourceTreeProperties.resources) + if total_resources and context.scene.BIMResourceProperties.active_resource_index < total_resources: + op = row2.operator("bim.assign_process", icon="ADD", text="") + op.task = task.ifc_definition_id + op.related_object_type = "RESOURCE" + op.resource = context.scene.BIMResourceTreeProperties.resources[ + context.scene.BIMResourceProperties.active_resource_index + ].ifc_definition_id + + total_task_resources = len(self.props.task_resources) + if total_task_resources and self.props.active_task_resource_index < total_task_resources: + op = row2.operator("bim.unassign_process", icon="REMOVE", text="") + op.task = task.ifc_definition_id + op.related_object_type = "RESOURCE" + op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id + + row2 = col.row() + row2.template_list( + "BIM_UL_task_resources", "", self.props, "task_resources", self.props, "active_task_resource_index" + ) + + # Column3 + col = grid.column() + + row2 = col.row(align=True) + row2.label(text="Outputs") + + if context.selected_objects: + op = row2.operator("bim.assign_product", icon="ADD", text="") + op.task = task.ifc_definition_id + op.relating_product = "" + op = row2.operator("bim.unassign_product", icon="REMOVE", text="") + op.task = task.ifc_definition_id + op.relating_product = "" + + op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="") + op.task = task.ifc_definition_id + + row2 = col.row() + row2.template_list( + "BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index" + ) + + class BIM_UL_task_columns(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): props = context.scene.BIMWorkScheduleProperties @@ -294,81 +426,51 @@ class BIM_UL_task_columns(UIList): row.prop(item, "name", emboss=False, text="") if props.sort_column == item.name: row.label(text="", icon="SORTALPHA") - row.operator("bim.remove_task_column", text="", icon="X") + row.operator("bim.remove_task_column", text="", icon="X").name = item.name + + +class BIM_UL_task_inputs(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMWorkScheduleProperties + if item: + row = layout.row(align=True) + row.prop(item, "name", emboss=False, text="") + # row.operator("bim.remove_task_column", text="", icon="X").name = item.name + + +class BIM_UL_task_resources(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMWorkScheduleProperties + if item: + row = layout.row(align=True) + row.prop(item, "name", emboss=False, text="") + + +class BIM_UL_task_outputs(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMWorkScheduleProperties + if item: + row = layout.row(align=True) + row.prop(item, "name", emboss=False, text="") class BIM_UL_tasks(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: - props = context.scene.BIMWorkScheduleProperties + self.props = context.scene.BIMWorkScheduleProperties task = IfcStore.get_file().by_id(item.ifc_definition_id) row = layout.row(align=True) - for i in range(0, item.level_index): - row.label(text="", icon="BLANK1") - if item.has_children: - if item.is_expanded: - row.operator( - "bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" - ).task = item.ifc_definition_id - else: - row.operator( - "bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" - ).task = item.ifc_definition_id - else: - row.label(text="", icon="DOT") - row.prop(item, "identification", emboss=False, text="") - row.prop(item, "name", emboss=False, text="") - for column in props.columns: - if column.name == "IfcTaskTime.ScheduleStart": - if item.derived_start: - row.label(text=item.derived_start + "*") - else: - row.prop(item, "start", emboss=False, text="") - elif column.name == "IfcTaskTime.ScheduleFinish": - if item.derived_finish: - row.label(text=item.derived_finish + "*") - else: - row.prop(item, "finish", emboss=False, text="") - elif column.name == "IfcTaskTime.ScheduleDuration": - if item.derived_duration: - row.label(text=item.derived_duration + "*") - else: - row.prop(item, "duration", emboss=False, text="") - elif column.name == "Controls.Calendar": - if item.derived_calendar: - row.label(text=item.derived_calendar + "*") - else: - row.label(text=item.calendar or "-") - else: - ifc_class, name = column.name.split(".") - if ifc_class == "IfcTask": - value = getattr(task, name) - elif ifc_class == "IfcTaskTime": - value = getattr(task.TaskTime, name) if task.TaskTime else None - if value is None: - value = "-" - row.label(text=str(value)) + self.draw_hierarchy(row, item) - if context.active_object: - oprops = context.active_object.BIMObjectProperties - row = layout.row(align=True) + split1 = row.split(factor=0.1) + split1.prop(item, "identification", emboss=False, text="") + split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns))) + split2.prop(item, "name", emboss=False, text="") - if oprops.ifc_definition_id in Data.tasks[item.ifc_definition_id]["OperatesOn"]: - op = row.operator("bim.unassign_process", text="", icon="MARKER_HLT", emboss=False) - op.task = item.ifc_definition_id - else: - op = row.operator("bim.assign_process", text="", icon="MARKER", emboss=False) - op.task = item.ifc_definition_id + self.draw_custom_columns(split2, item, task) - if oprops.ifc_definition_id in Data.tasks[item.ifc_definition_id]["RelatingProducts"]: - op = row.operator("bim.unassign_product", text="", icon="KEYFRAME_HLT", emboss=False) - op.task = item.ifc_definition_id - else: - op = row.operator("bim.assign_product", text="", icon="KEYFRAME", emboss=False) - op.task = item.ifc_definition_id - - if props.active_task_id and props.editing_task_type == "ATTRIBUTES": + if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES": row.prop( item, "is_selected", @@ -377,46 +479,66 @@ class BIM_UL_tasks(UIList): emboss=False, ) - if props.active_task_id == item.ifc_definition_id: - if props.editing_task_type == "TASKTIME": - row.operator("bim.edit_task_time", text="", icon="CHECKMARK") - elif props.editing_task_type == "ATTRIBUTES": - row.operator("bim.edit_task", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_task", text="", icon="CANCEL") - elif props.active_task_id: - if props.editing_task_type == "SEQUENCE": + if self.props.active_task_id: + if self.props.editing_task_type == "SEQUENCE" and self.props.active_task_id != item.ifc_definition_id: if item.is_predecessor: - row.operator( - "bim.unassign_predecessor", text="", icon="BACK", emboss=False - ).task = item.ifc_definition_id + op = row.operator("bim.unassign_predecessor", text="", icon="BACK", emboss=False) else: - row.operator( - "bim.assign_predecessor", text="", icon="TRACKING_BACKWARDS", emboss=False - ).task = item.ifc_definition_id + op = row.operator("bim.assign_predecessor", text="", icon="TRACKING_BACKWARDS", emboss=False) + op.task = item.ifc_definition_id if item.is_successor: - row.operator( - "bim.unassign_successor", text="", icon="FORWARD", emboss=False - ).task = item.ifc_definition_id + op = row.operator("bim.unassign_successor", text="", icon="FORWARD", emboss=False) else: - row.operator( - "bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False - ).task = item.ifc_definition_id + op = row.operator("bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False) + op.task = item.ifc_definition_id - row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id - row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id + def draw_hierarchy(self, row, item): + for i in range(0, item.level_index): + row.label(text="", icon="BLANK1") + if item.has_children: + if item.is_expanded: + row.operator( + "bim.contract_task", text="", emboss=False, icon="DISCLOSURE_TRI_DOWN" + ).task = item.ifc_definition_id else: - row.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING").task = item.ifc_definition_id row.operator( - "bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="" + "bim.expand_task", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT" ).task = item.ifc_definition_id - row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = item.ifc_definition_id - row.operator( - "bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO" - ).task = item.ifc_definition_id - row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = item.ifc_definition_id - row.operator("bim.add_task", text="", icon="ADD").task = item.ifc_definition_id - row.operator("bim.remove_task", text="", icon="X").task = item.ifc_definition_id + else: + row.label(text="", icon="DOT") + + def draw_custom_columns(self, row, item, task): + for column in self.props.columns: + if column.name == "IfcTaskTime.ScheduleStart": + if item.derived_start: + row.label(text=item.derived_start + "*") + else: + row.prop(item, "start", emboss=False, text="") + elif column.name == "IfcTaskTime.ScheduleFinish": + if item.derived_finish: + row.label(text=item.derived_finish + "*") + else: + row.prop(item, "finish", emboss=False, text="") + elif column.name == "IfcTaskTime.ScheduleDuration": + if item.derived_duration: + row.label(text=item.derived_duration + "*") + else: + row.prop(item, "duration", emboss=False, text="") + elif column.name == "Controls.Calendar": + if item.derived_calendar: + row.label(text=item.derived_calendar + "*") + else: + row.label(text=item.calendar or "-") + else: + ifc_class, name = column.name.split(".") + if ifc_class == "IfcTask": + value = getattr(task, name) + elif ifc_class == "IfcTaskTime": + value = getattr(task.TaskTime, name) if task.TaskTime else None + if value is None: + value = "-" + row.label(text=str(value)) class BIM_PT_work_calendars(Panel): @@ -429,7 +551,8 @@ class BIM_PT_work_calendars(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not Data.is_loaded: @@ -500,15 +623,7 @@ class BIM_PT_work_calendars(Panel): self.draw_editable_work_time_ui(work_time) def draw_editable_work_time_ui(self, work_time): - for attribute in self.props.work_time_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - + draw_attributes(self.props.work_time_attributes, self.layout) if work_time["RecurrencePattern"]: self.draw_editable_recurrence_pattern_ui(Data.recurrence_patterns[work_time["RecurrencePattern"]]) else: @@ -576,11 +691,4 @@ class BIM_PT_work_calendars(Panel): row.prop(self.props, "occurrences") def draw_editable_ui(self): - for attribute in self.props.work_calendar_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.work_calendar_attributes, self.layout) diff --git a/src/blenderbim/blenderbim/bim/module/spatial/__init__.py b/src/blenderbim/blenderbim/bim/module/spatial/__init__.py index 8671309965..1d1b95cc90 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/spatial/operator.py b/src/blenderbim/blenderbim/bim/module/spatial/operator.py index 053a4f9421..3b02e5651a 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/operator.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api import ifcopenshell.util.element @@ -17,9 +35,12 @@ class AssignContainer(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): + # The active object pointer is lost when the object is unlinked from all collections in the view layer + # We need to store it first, then restore it at the end + active_object = context.active_object self.file = IfcStore.get_file() related_elements = ( - [bpy.data.objects.get(self.related_element)] if self.related_element else bpy.context.selected_objects + [bpy.data.objects.get(self.related_element)] if self.related_element else context.selected_objects ) sprops = context.scene.BIMSpatialProperties relating_structure = ( @@ -27,6 +48,8 @@ class AssignContainer(bpy.types.Operator): ) for related_element in related_elements: oprops = related_element.BIMObjectProperties + if not oprops.ifc_definition_id: + continue props = related_element.BIMObjectSpatialProperties ifcopenshell.api.run( @@ -49,7 +72,7 @@ class AssignContainer(bpy.types.Operator): relating_collection = bpy.data.collections.get(relating_structure_obj.name) if aggregate_collection: - self.remove_collection(bpy.context.scene.collection, aggregate_collection) + self.remove_collection(context.scene.collection, aggregate_collection) for collection in bpy.data.collections: self.remove_collection(collection, aggregate_collection) relating_collection.children.link(aggregate_collection) @@ -57,6 +80,8 @@ class AssignContainer(bpy.types.Operator): for collection in related_element.users_collection: collection.objects.unlink(related_element) relating_collection.objects.link(related_element) + # Restore the active object : + context.view_layer.objects.active = active_object return {"FINISHED"} def remove_collection(self, parent, child): @@ -69,9 +94,10 @@ class AssignContainer(bpy.types.Operator): class EnableEditingContainer(bpy.types.Operator): bl_idname = "bim.enable_editing_container" bl_label = "Enable Editing Container" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.context.active_object.BIMObjectSpatialProperties.is_editing = True + context.active_object.BIMObjectSpatialProperties.is_editing = True getSpatialContainers(self, context) return {"FINISHED"} @@ -79,6 +105,7 @@ class EnableEditingContainer(bpy.types.Operator): class ChangeSpatialLevel(bpy.types.Operator): bl_idname = "bim.change_spatial_level" bl_label = "Change Spatial Level" + bl_options = {"REGISTER", "UNDO"} parent_id: bpy.props.IntProperty() def execute(self, context): @@ -93,7 +120,7 @@ class DisableEditingContainer(bpy.types.Operator): obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object obj.BIMObjectSpatialProperties.is_editing = False return {"FINISHED"} @@ -101,27 +128,34 @@ class DisableEditingContainer(bpy.types.Operator): class RemoveContainer(bpy.types.Operator): bl_idname = "bim.remove_container" bl_label = "Remove Container" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object - oprops = obj.BIMObjectProperties - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "spatial.remove_container", self.file, **{"product": self.file.by_id(oprops.ifc_definition_id)} - ) - Data.load(IfcStore.get_file(), oprops.ifc_definition_id) + return IfcStore.execute_ifc_operator(self, context) - aggregate_collection = bpy.data.collections.get(obj.name) - if aggregate_collection: - self.remove_collection(bpy.context.scene.collection, aggregate_collection) - for collection in bpy.data.collections: - self.remove_collection(collection, spatial_collection) - bpy.context.scene.collection.children.link(aggregate_collection) - else: - for collection in obj.users_collection: - collection.objects.unlink(obj) - bpy.context.scene.collection.objects.link(obj) + def _execute(self, context): + active_object = context.active_object + self.file = IfcStore.get_file() + objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects + for obj in objs: + obj_id = obj.BIMObjectProperties.ifc_definition_id + if not obj_id: + continue + ifcopenshell.api.run("spatial.remove_container", self.file, **{"product": self.file.by_id(obj_id)}) + Data.load(self.file, obj_id) + + aggregate_collection = bpy.data.collections.get(obj.name) + if aggregate_collection: + self.remove_collection(context.scene.collection, aggregate_collection) + for collection in bpy.data.collections: + self.remove_collection(collection, spatial_collection) + context.scene.collection.children.link(aggregate_collection) + else: + for collection in obj.users_collection: + collection.objects.unlink(obj) + context.scene.collection.objects.link(obj) + context.view_layer.objects.active = active_object return {"FINISHED"} def remove_collection(self, parent, child): @@ -132,17 +166,29 @@ class RemoveContainer(bpy.types.Operator): class CopyToContainer(bpy.types.Operator): + """ + Copies selected objects to selected containers + Check the mark next to a container in the container list to select it + Several containers can be selected at a time + """ + bl_idname = "bim.copy_to_container" bl_label = "Copy To Container" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() - objects = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects + objects = list(bpy.data.objects.get(self.obj, context.selected_objects)) sprops = context.scene.BIMSpatialProperties container_ids = [c.ifc_definition_id for c in sprops.spatial_elements if c.is_selected] for obj in objects: - container = ifcopenshell.util.element.get_container(self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)) + container = ifcopenshell.util.element.get_container( + self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + ) if container: container_obj = IfcStore.get_element(container.id()) local_position = container_obj.matrix_world.inverted() @ obj.matrix_world diff --git a/src/blenderbim/blenderbim/bim/module/spatial/prop.py b/src/blenderbim/blenderbim/bim/module/spatial/prop.py index dfea8823de..769479f3e8 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/prop.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.prop import StrProperty, Attribute from ifcopenshell.api.spatial.data import Data @@ -19,8 +37,7 @@ def getSpatialContainers(self, context, parent_id=None): Data.load(IfcStore.get_file()) self.file = IfcStore.get_file() props = context.scene.BIMSpatialProperties - while len(props.spatial_elements) > 0: - props.spatial_elements.remove(0) + props.spatial_elements.clear() if parent_id: props.active_decomposes_id = parent_id diff --git a/src/blenderbim/blenderbim/bim/module/spatial/ui.py b/src/blenderbim/blenderbim/bim/module/spatial/ui.py index f5ed85f61b..00a4e8541d 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/ui.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.spatial.data import Data diff --git a/src/blenderbim/blenderbim/bim/module/structural/__init__.py b/src/blenderbim/blenderbim/bim/module/structural/__init__.py index 7c49636828..c37f6a4751 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/structural/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator diff --git a/src/blenderbim/blenderbim/bim/module/structural/operator.py b/src/blenderbim/blenderbim/bim/module/structural/operator.py index 60323d36a8..5d60ef6e40 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/operator.py +++ b/src/blenderbim/blenderbim/bim/module/structural/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import json import ifcopenshell @@ -20,7 +38,7 @@ class AddStructuralMemberConnection(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.context.active_object + obj = context.active_object oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties file = IfcStore.get_file() @@ -46,7 +64,7 @@ class EnableEditingStructuralConnectionCondition(bpy.types.Operator): connects_structural_member: bpy.props.IntProperty() def execute(self, context): - obj = bpy.context.active_object + obj = context.active_object oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties applied_condition_id = Data.connects_structural_members[self.connects_structural_member]["AppliedCondition"] @@ -60,7 +78,7 @@ class DisableEditingStructuralConnectionCondition(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = bpy.context.active_object + obj = context.active_object props = obj.BIMStructuralProperties props.active_connects_structural_member = 0 return {"FINISHED"} @@ -134,8 +152,7 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator): def execute(self, context): obj = context.active_object props = obj.BIMStructuralProperties - while len(props.boundary_condition_attributes) > 0: - props.boundary_condition_attributes.remove(0) + props.boundary_condition_attributes.clear() data = Data.boundary_conditions[self.boundary_condition] @@ -146,7 +163,7 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator): new.name = attribute.name() new.is_null = value is None new.is_optional = attribute.optional() - if data_type == "select": + if isinstance(data_type, tuple) and data_type[0] == "select": enum_items = [s.name() for s in ifcopenshell.util.attribute.get_select_items(attribute)] new.enum_items = json.dumps(enum_items) if isinstance(value, bool): @@ -221,8 +238,7 @@ class LoadStructuralAnalysisModels(bpy.types.Operator): def execute(self, context): props = context.scene.BIMStructuralProperties - while len(props.structural_analysis_models) > 0: - props.structural_analysis_models.remove(0) + props.structural_analysis_models.clear() for ifc_definition_id, structural_analysis_model in Data.structural_analysis_models.items(): new = props.structural_analysis_models.add() new.ifc_definition_id = ifc_definition_id @@ -279,15 +295,7 @@ class EditStructuralAnalysisModel(bpy.types.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties - attributes = {} - for attribute in props.structural_analysis_model_attributes: - if attribute.is_null: - attributes[attribute.name] = None - else: - if attribute.data_type == "string": - attributes[attribute.name] = attribute.string_value - elif attribute.data_type == "enum": - attributes[attribute.name] = attribute.enum_value + attributes = blenderbim.bim.helper.export_attributes(props.structural_analysis_model_attributes) self.file = IfcStore.get_file() ifcopenshell.api.run( "structural.edit_structural_analysis_model", @@ -332,8 +340,7 @@ class EnableEditingStructuralAnalysisModel(bpy.types.Operator): def execute(self, context): props = context.scene.BIMStructuralProperties - while len(props.structural_analysis_model_attributes) > 0: - props.structural_analysis_model_attributes.remove(0) + props.structural_analysis_model_attributes.clear() data = Data.structural_analysis_models[self.structural_analysis_model] @@ -423,7 +430,7 @@ class EnableEditingStructuralItemAxis(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = bpy.context.active_object + obj = context.active_object oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties @@ -463,7 +470,7 @@ class DisableEditingStructuralItemAxis(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = bpy.context.active_object + obj = context.active_object props = obj.BIMStructuralProperties props.is_editing_axis = False if props.axis_empty: @@ -479,7 +486,7 @@ class EditStructuralItemAxis(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.context.active_object + obj = context.active_object oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted() @@ -501,7 +508,7 @@ class EnableEditingStructuralConnectionCS(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = bpy.context.active_object + obj = context.active_object oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties @@ -553,7 +560,7 @@ class DisableEditingStructuralConnectionCS(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = bpy.context.active_object + obj = context.active_object props = obj.BIMStructuralProperties props.is_editing_connection_cs = False if props.ccs_empty: @@ -570,7 +577,7 @@ class EditStructuralConnectionCS(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.context.active_object + obj = context.active_object oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties relative_matrix = props.ccs_empty.matrix_world @ obj.matrix_world.inverted() @@ -599,7 +606,10 @@ class AssignStructuralLoadCase(bpy.types.Operator): ifcopenshell.api.run( "aggregate.assign_object", self.file, - **{"relating_object": self.file.by_id(self.work_plan), "product": self.file.by_id(self.load_case),}, + **{ + "relating_object": self.file.by_id(self.work_plan), + "product": self.file.by_id(self.load_case), + }, ) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -616,7 +626,10 @@ class UnassignStructuralLoadCase(bpy.types.Operator): ifcopenshell.api.run( "aggregate.unassign_object", self.file, - **{"relating_object": self.file.by_id(self.work_plan), "product": self.file.by_id(self.load_case),}, + **{ + "relating_object": self.file.by_id(self.work_plan), + "product": self.file.by_id(self.load_case), + }, ) Data.load(IfcStore.get_file()) return {"FINISHED"} @@ -686,8 +699,7 @@ class EnableEditingStructuralLoadCase(bpy.types.Operator): self.props = context.scene.BIMStructuralProperties self.props.active_load_case_id = self.load_case self.props.load_case_editing_type = "ATTRIBUTES" - while len(self.props.load_case_attributes) > 0: - self.props.load_case_attributes.remove(0) + self.props.load_case_attributes.clear() data = Data.load_cases[self.load_case] blenderbim.bim.helper.import_attributes( "IfcStructuralLoadCase", self.props.load_case_attributes, data, self.import_attributes @@ -773,8 +785,7 @@ class EnableEditingStructuralLoadGroupActivities(bpy.types.Operator): return {"FINISHED"} def load_structural_activities(self): - while len(self.props.load_group_activities) > 0: - self.props.load_group_activities.remove(0) + self.props.load_group_activities.clear() for activity_id in Data.load_groups[self.load_group]["IsGroupedBy"]: activity = Data.structural_activities[activity_id] new = self.props.load_group_activities.add() @@ -845,8 +856,7 @@ class LoadStructuralLoads(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMStructuralProperties - while len(props.structural_loads) > 0: - props.structural_loads.remove(0) + props.structural_loads.clear() if props.filtered_structural_loads: names = [structural_load["Name"] or "Unnamed" for _, structural_load in Data.structural_loads.items()] for ifc_definition_id, structural_load in Data.structural_loads.items(): @@ -908,8 +918,7 @@ class EnableEditingStructuralLoad(bpy.types.Operator): def execute(self, context): props = context.scene.BIMStructuralProperties - while len(props.structural_load_attributes) > 0: - props.structural_load_attributes.remove(0) + props.structural_load_attributes.clear() data = Data.structural_loads[self.structural_load] blenderbim.bim.helper.import_attributes(data["type"], props.structural_load_attributes, data) @@ -964,7 +973,10 @@ class EditStructuralLoad(bpy.types.Operator): ifcopenshell.api.run( "structural.edit_structural_load", self.file, - **{"structural_load": self.file.by_id(props.active_structural_load_id), "attributes": attributes,}, + **{ + "structural_load": self.file.by_id(props.active_structural_load_id), + "attributes": attributes, + }, ) Data.load(IfcStore.get_file()) bpy.ops.bim.load_structural_loads() @@ -991,8 +1003,7 @@ class LoadBoundaryConditions(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMStructuralProperties - while len(props.boundary_conditions) > 0: - props.boundary_conditions.remove(0) + props.boundary_conditions.clear() if props.filtered_boundary_conditions: names = [ boundary_condition["Name"] or "Unnamed" for _, boundary_condition in Data.boundary_conditions.items() @@ -1071,8 +1082,7 @@ class EnableEditingBoundaryCondition(bpy.types.Operator): def execute(self, context): props = context.scene.BIMStructuralProperties - while len(props.boundary_condition_attributes) > 0: - props.boundary_condition_attributes.remove(0) + props.boundary_condition_attributes.clear() data = Data.boundary_conditions[self.boundary_condition] # blenderbim.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data) diff --git a/src/blenderbim/blenderbim/bim/module/structural/prop.py b/src/blenderbim/blenderbim/bim/module/structural/prop.py index f7c853e17c..38fb4e571a 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/prop.py +++ b/src/blenderbim/blenderbim/bim/module/structural/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from blenderbim.bim.ifc import IfcStore from math import radians @@ -31,16 +49,18 @@ def getApplicableStructuralLoadTypes(self, context): element_classes = set( [ ifc_file.by_id(o.BIMObjectProperties.ifc_definition_id).is_a() - for o in bpy.context.selected_objects + for o in context.selected_objects if o.BIMObjectProperties.ifc_definition_id ] ) types = [("IfcStructuralLoadTemperature", "IfcStructuralLoadTemperature", "")] if "IfcStructuralPointConnection" in element_classes: - types.extend([ - ("IfcStructuralLoadSingleForce", "IfcStructuralLoadSingleForce", ""), - ("IfcStructuralLoadSingleDisplacement", "IfcStructuralLoadSingleDisplacement", "") - ]) + types.extend( + [ + ("IfcStructuralLoadSingleForce", "IfcStructuralLoadSingleForce", ""), + ("IfcStructuralLoadSingleDisplacement", "IfcStructuralLoadSingleDisplacement", ""), + ] + ) if "IfcStructuralCurveMember" in element_classes: types.append(("IfcStructuralLoadLinearForce", "IfcStructuralLoadLinearForce", "")) if "IfcStructuralSurfaceMember" in element_classes: diff --git a/src/blenderbim/blenderbim/bim/module/structural/ui.py b/src/blenderbim/blenderbim/bim/module/structural/ui.py index 295cd0f1c5..498a651749 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/ui.py +++ b/src/blenderbim/blenderbim/bim/module/structural/ui.py @@ -1,7 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.structural.data import Data @@ -20,7 +39,7 @@ def draw_boundary_condition_ui(layout, boundary_condition_id, connection_id, pro if props.active_boundary_condition and props.active_boundary_condition == boundary_condition_id: row.label(text=data["type"], icon="CON_TRACKTO") row.operator("bim.edit_structural_boundary_condition", text="", icon="CHECKMARK").connection = connection_id - row.operator("bim.disable_editing_structural_boundary_condition", text="", icon="X") + row.operator("bim.disable_editing_structural_boundary_condition", text="", icon="CANCEL") elif props.active_boundary_condition and props.active_boundary_condition != boundary_condition_id: row.label(text=data["type"], icon="CON_TRACKTO") row.operator("bim.remove_structural_boundary_condition", text="", icon="X").connection = connection_id @@ -261,7 +280,8 @@ class BIM_PT_structural_analysis_models(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not Data.is_loaded: @@ -292,14 +312,7 @@ class BIM_PT_structural_analysis_models(Panel): self.draw_editable_ui(context) def draw_editable_ui(self, context): - for attribute in self.props.structural_analysis_model_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.structural_analysis_model_attributes, self.layout) class BIM_UL_structural_analysis_models(UIList): @@ -324,7 +337,7 @@ class BIM_UL_structural_analysis_models(UIList): if context.scene.BIMStructuralProperties.active_structural_analysis_model_id == item.ifc_definition_id: row.operator("bim.edit_structural_analysis_model", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_structural_analysis_model", text="", icon="X") + row.operator("bim.disable_editing_structural_analysis_model", text="", icon="CANCEL") elif context.scene.BIMStructuralProperties.active_structural_analysis_model_id: op = row.operator("bim.remove_structural_analysis_model", text="", icon="X") op.structural_analysis_model = item.ifc_definition_id @@ -345,7 +358,8 @@ class BIM_PT_structural_load_cases(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): self.props = context.scene.BIMStructuralProperties @@ -387,16 +401,7 @@ class BIM_PT_structural_load_cases(Panel): self.draw_editable_load_case_group_ui(load_case) def draw_editable_load_case_ui(self): - for attribute in self.props.load_case_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.load_case_attributes, self.layout) def draw_editable_load_case_group_ui(self, load_case): box = self.layout.box() @@ -449,7 +454,8 @@ class BIM_PT_structural_loads(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not Data.is_loaded: @@ -516,7 +522,8 @@ class BIM_PT_boundary_conditions(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + file = IfcStore.get_file() + return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): if not Data.is_loaded: diff --git a/src/blenderbim/blenderbim/bim/module/style/__init__.py b/src/blenderbim/blenderbim/bim/module/style/__init__.py index 590bb7d66e..086450e2ea 100644 --- a/src/blenderbim/blenderbim/bim/module/style/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/style/__init__.py @@ -1,17 +1,41 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy -from . import ui, operator +from . import ui, prop, operator classes = ( operator.AddStyle, + operator.EnableEditingStyle, + operator.DisableEditingStyle, operator.EditStyle, + operator.RemoveStyle, + operator.UpdateStyleColours, operator.UnlinkStyle, + prop.BIMStyleProperties, ui.BIM_PT_style, + ui.BIM_PT_style_attributes, ) def register(): - pass + bpy.types.Material.BIMStyleProperties = bpy.props.PointerProperty(type=prop.BIMStyleProperties) def unregister(): - pass + del bpy.types.Material.BIMStyleProperties diff --git a/src/blenderbim/blenderbim/bim/module/style/operator.py b/src/blenderbim/blenderbim/bim/module/style/operator.py index 06b33c8a21..c03376c187 100644 --- a/src/blenderbim/blenderbim/bim/module/style/operator.py +++ b/src/blenderbim/blenderbim/bim/module/style/operator.py @@ -1,6 +1,27 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api +import ifcopenshell.util.representation +import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.style.data import Data def get_colour_settings(material): @@ -18,9 +39,9 @@ def get_colour_settings(material): } -class EditStyle(bpy.types.Operator): - bl_idname = "bim.edit_style" - bl_label = "Edit Style" +class UpdateStyleColours(bpy.types.Operator): + bl_idname = "bim.update_style_colours" + bl_label = "Update Style Colours" bl_options = {"REGISTER", "UNDO"} material: bpy.props.StringProperty() @@ -29,10 +50,46 @@ class EditStyle(bpy.types.Operator): def _execute(self, context): self.file = IfcStore.get_file() - material = bpy.data.objects.get(self.material) if self.material else bpy.context.active_object.active_material + material = bpy.data.materials.get(self.material) if self.material else context.active_object.active_material settings = get_colour_settings(material) - settings["style"] = self.file.by_id(material.BIMMaterialProperties.ifc_style_id) - ifcopenshell.api.run("style.edit_style", self.file, **settings) + for style in self.file.by_id(material.BIMMaterialProperties.ifc_style_id).Styles: + if style.is_a("IfcSurfaceStyleRendering"): + ifcopenshell.api.run( + "style.edit_surface_style", + self.file, + style=style, + attributes={ + "SurfaceColour": settings["surface_colour"], + "Transparency": settings["transparency"], + "DiffuseColour": settings["diffuse_colour"], + }, + ) + elif style.is_a("IfcSurfaceStyleShading"): + ifcopenshell.api.run( + "style.edit_surface_style", + self.file, + style=style, + attributes={"SurfaceColour": settings["surface_colour"], "Transparency": settings["transparency"]}, + ) + return {"FINISHED"} + + +class RemoveStyle(bpy.types.Operator): + bl_idname = "bim.remove_style" + bl_label = "Remove Style" + bl_options = {"REGISTER", "UNDO"} + material: bpy.props.StringProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + material = bpy.data.materials.get(self.material) if self.material else context.active_object.active_material + ifcopenshell.api.run( + "style.remove_style", self.file, style=self.file.by_id(material.BIMMaterialProperties.ifc_style_id) + ) + material.BIMMaterialProperties.ifc_style_id = 0 return {"FINISHED"} @@ -47,12 +104,24 @@ class AddStyle(bpy.types.Operator): def _execute(self, context): self.file = IfcStore.get_file() - material = bpy.data.materials.get(self.material) if self.material else bpy.context.active_object.active_material + material = bpy.data.materials.get(self.material) if self.material else context.active_object.active_material settings = get_colour_settings(material) settings["name"] = material.name - settings["external_definition"] = None # TODO: Implement. See #1222 + settings["external_definition"] = None # TODO: Implement. See #1222 style = ifcopenshell.api.run("style.add_style", self.file, **settings) - material.BIMMaterialProperties.ifc_style_id = int(style.id()) + IfcStore.link_element(style, material) + if material.BIMObjectProperties.ifc_definition_id: + context = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW") + if context: + ifcopenshell.api.run( + "style.assign_material_style", + self.file, + **{ + "material": self.file.by_id(material.BIMObjectProperties.ifc_definition_id), + "style": style, + "context": context, + } + ) return {"FINISHED"} @@ -69,3 +138,53 @@ class UnlinkStyle(bpy.types.Operator): if "Ifc" in material.name and "/" in material.name: material.name = "/".join(material.name.split("/")[1:]) return {"FINISHED"} + + +class EnableEditingStyle(bpy.types.Operator): + bl_idname = "bim.enable_editing_style" + bl_label = "Enable Editing Style" + bl_options = {"REGISTER", "UNDO"} + material: bpy.props.StringProperty() + + def execute(self, context): + material = bpy.data.materials.get(self.material) if self.material else context.active_object.active_material + props = material.BIMStyleProperties + props.attributes.clear() + + data = Data.styles[material.BIMMaterialProperties.ifc_style_id] + blenderbim.bim.helper.import_attributes("IfcSurfaceStyle", props.attributes, data) + props.is_editing_attributes = True + return {"FINISHED"} + + +class DisableEditingStyle(bpy.types.Operator): + bl_idname = "bim.disable_editing_style" + bl_options = {"REGISTER", "UNDO"} + bl_label = "Disable Editing Style" + material: bpy.props.StringProperty() + + def execute(self, context): + material = bpy.data.materials.get(self.material) if self.material else context.active_object.active_material + props = material.BIMStyleProperties + props.is_editing_attributes = False + return {"FINISHED"} + + +class EditStyle(bpy.types.Operator): + bl_idname = "bim.edit_style" + bl_label = "Edit Style" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + material = context.active_object.active_material + props = material.BIMStyleProperties + attributes = blenderbim.bim.helper.export_attributes(props.attributes) + self.file = IfcStore.get_file() + style = self.file.by_id(material.BIMMaterialProperties.ifc_style_id) + ifcopenshell.api.run("style.edit_presentation_style", self.file, **{"style": style, "attributes": attributes}) + Data.load(IfcStore.get_file(), material.BIMMaterialProperties.ifc_style_id) + bpy.ops.bim.disable_editing_style() + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/style/prop.py b/src/blenderbim/blenderbim/bim/module/style/prop.py new file mode 100644 index 0000000000..7b56fc86ee --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/style/prop.py @@ -0,0 +1,37 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.prop import StrProperty, Attribute +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +class BIMStyleProperties(PropertyGroup): + attributes: CollectionProperty(name="Attributes", type=Attribute) + is_editing_attributes: BoolProperty(name="Is Editing Attributes") diff --git a/src/blenderbim/blenderbim/bim/module/style/ui.py b/src/blenderbim/blenderbim/bim/module/style/ui.py index b34f4595fb..9da4296975 100644 --- a/src/blenderbim/blenderbim/bim/module/style/ui.py +++ b/src/blenderbim/blenderbim/bim/module/style/ui.py @@ -1,5 +1,25 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.bim.helper from bpy.types import Panel from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.style.data import Data class BIM_PT_style(Panel): @@ -18,11 +38,55 @@ class BIM_PT_style(Panel): ) def draw(self, context): - props = context.active_object.active_material.BIMMaterialProperties + material = context.active_object.active_material + props = material.BIMMaterialProperties row = self.layout.row(align=True) if props.ifc_style_id: - row.operator("bim.edit_style", icon="GREASEPENCIL") - op = row.operator("bim.unlink_style", icon="UNLINKED", text="") - op.material = context.active_object.active_material.name + row.operator("bim.update_style_colours", icon="GREASEPENCIL").material = material.name + row.operator("bim.unlink_style", icon="UNLINKED", text="").material = material.name + row.operator("bim.remove_style", icon="X", text="").material = material.name else: row.operator("bim.add_style", icon="ADD") + + +class BIM_PT_style_attributes(Panel): + bl_label = "IFC Style Attributes" + bl_idname = "BIM_PT_style_attributes" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + + @classmethod + def poll(cls, context): + if not IfcStore.get_file(): + return False + try: + return bool(context.active_object.active_material.BIMMaterialProperties.ifc_style_id) + except: + return False + + def draw(self, context): + obj = context.active_object.active_material + mprops = obj.BIMMaterialProperties + props = obj.BIMStyleProperties + if mprops.ifc_style_id not in Data.styles: + Data.load(IfcStore.get_file(), mprops.ifc_style_id) + if props.is_editing_attributes: + row = self.layout.row(align=True) + row.operator("bim.edit_style", icon="CHECKMARK") + row.operator("bim.disable_editing_style", icon="CANCEL", text="") + blenderbim.bim.helper.draw_attributes(props.attributes, self.layout) + else: + row = self.layout.row() + row.operator("bim.enable_editing_style", icon="GREASEPENCIL", text="Edit") + + row = self.layout.row(align=True) + row.label(text="STEP ID") + row.label(text=str(mprops.ifc_style_id)) + + for name, value in Data.styles[mprops.ifc_style_id].items(): + if name in ["id", "type", "Styles"]: + continue + row = self.layout.row(align=True) + row.label(text=name) + row.label(text=str(value)) diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py new file mode 100644 index 0000000000..115d400737 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py @@ -0,0 +1,48 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +from . import ui, prop, operator + +classes = ( + operator.LoadSystems, + operator.DisableSystemEditingUI, + operator.AddSystem, + operator.EditSystem, + operator.RemoveSystem, + operator.ToggleAssigningSystem, + operator.AssignSystem, + operator.UnassignSystem, + operator.EnableEditingSystem, + operator.DisableEditingSystem, + operator.SelectSystemProducts, + prop.System, + prop.BIMSystemProperties, + ui.BIM_PT_systems, + ui.BIM_PT_object_systems, + ui.BIM_UL_systems, + ui.BIM_UL_object_systems, +) + + +def register(): + bpy.types.Scene.BIMSystemProperties = bpy.props.PointerProperty(type=prop.BIMSystemProperties) + + +def unregister(): + del bpy.types.Scene.BIMSystemProperties diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py new file mode 100644 index 0000000000..bbfa9c40eb --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -0,0 +1,224 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell.util.attribute +import ifcopenshell.api +import blenderbim.bim.helper +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.system.data import Data + + +class LoadSystems(bpy.types.Operator): + bl_idname = "bim.load_systems" + bl_label = "Load Systems" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMSystemProperties + props.systems.clear() + for ifc_definition_id, system in Data.systems.items(): + new = props.systems.add() + new.ifc_definition_id = ifc_definition_id + new.name = system["Name"] + props.is_editing = True + bpy.ops.bim.disable_editing_system() + return {"FINISHED"} + + +class DisableSystemEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_system_editing_ui" + bl_label = "Disable System Editing UI" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMSystemProperties.is_editing = False + context.scene.BIMSystemProperties.active_system_id = 0 + return {"FINISHED"} + + +class AddSystem(bpy.types.Operator): + bl_idname = "bim.add_system" + bl_label = "Add System" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + result = ifcopenshell.api.run("system.add_system", IfcStore.get_file()) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_systems() + bpy.ops.bim.enable_editing_system(system=result.id()) + return {"FINISHED"} + + +class EditSystem(bpy.types.Operator): + bl_idname = "bim.edit_system" + bl_label = "Edit System" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMSystemProperties + attributes = {} + for attribute in props.system_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + attributes[attribute.name] = attribute.string_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "system.edit_system", + self.file, + **{"system": self.file.by_id(props.active_system_id), "attributes": attributes} + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_systems() + return {"FINISHED"} + + +class RemoveSystem(bpy.types.Operator): + bl_idname = "bim.remove_system" + bl_label = "Remove System" + bl_options = {"REGISTER", "UNDO"} + system: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMSystemProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("system.remove_system", self.file, **{"system": self.file.by_id(self.system)}) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_systems() + return {"FINISHED"} + + +class EnableEditingSystem(bpy.types.Operator): + bl_idname = "bim.enable_editing_system" + bl_label = "Enable Editing System" + bl_options = {"REGISTER", "UNDO"} + system: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMSystemProperties + props.system_attributes.clear() + + blenderbim.bim.helper.import_attributes("IfcSystem", props.system_attributes, Data.systems[self.system]) + props.active_system_id = self.system + return {"FINISHED"} + + +class DisableEditingSystem(bpy.types.Operator): + bl_idname = "bim.disable_editing_system" + bl_label = "Disable Editing System" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMSystemProperties.active_system_id = 0 + return {"FINISHED"} + + +class ToggleAssigningSystem(bpy.types.Operator): + bl_idname = "bim.toggle_assigning_system" + bl_label = "Toggle Assigning System" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMSystemProperties.is_adding = not context.scene.BIMSystemProperties.is_adding + return {"FINISHED"} + + +class AssignSystem(bpy.types.Operator): + bl_idname = "bim.assign_system" + bl_label = "Assign System" + bl_options = {"REGISTER", "UNDO"} + product: bpy.props.StringProperty() + system: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + products = [bpy.data.objects.get(self.product)] if self.product else context.selected_objects + for product in products: + if not product.BIMObjectProperties.ifc_definition_id: + continue + ifcopenshell.api.run( + "system.assign_system", + self.file, + **{ + "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), + "system": self.file.by_id(self.system), + } + ) + Data.load(self.file) + return {"FINISHED"} + + +class UnassignSystem(bpy.types.Operator): + bl_idname = "bim.unassign_system" + bl_label = "Unassign System" + bl_options = {"REGISTER", "UNDO"} + product: bpy.props.StringProperty() + system: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + products = [bpy.data.objects.get(self.product)] if self.product else context.selected_objects + for product in products: + props = product.BIMObjectProperties + if not props.ifc_definition_id: + continue + if not (props.ifc_definition_id in Data.products and self.system in Data.products[props.ifc_definition_id]): + continue + ifcopenshell.api.run( + "system.unassign_system", + self.file, + **{ + "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id), + "system": self.file.by_id(self.system), + } + ) + Data.load(self.file) + return {"FINISHED"} + + +class SelectSystemProducts(bpy.types.Operator): + bl_idname = "bim.select_system_products" + bl_label = "Select System Products" + bl_options = {"REGISTER", "UNDO"} + system: bpy.props.IntProperty() + + def execute(self, context): + for obj in context.visible_objects: + obj.select_set(False) + if not obj.BIMObjectProperties.ifc_definition_id: + continue + product_systems = Data.products.get(obj.BIMObjectProperties.ifc_definition_id, []) + if self.system in product_systems: + obj.select_set(True) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/system/prop.py b/src/blenderbim/blenderbim/bim/module/system/prop.py new file mode 100644 index 0000000000..d38c867e25 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/system/prop.py @@ -0,0 +1,45 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +from blenderbim.bim.prop import StrProperty, Attribute +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +class System(PropertyGroup): + name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + +class BIMSystemProperties(PropertyGroup): + system_attributes: CollectionProperty(name="System Attributes", type=Attribute) + is_editing: BoolProperty(name="Is Editing", default=False) + is_adding: BoolProperty(name="Is Adding", default=False) + systems: CollectionProperty(name="Systems", type=System) + active_system_index: IntProperty(name="Active System Index") + active_system_id: IntProperty(name="Active System Id") diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py new file mode 100644 index 0000000000..24ece1eecf --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -0,0 +1,144 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +from bpy.types import Panel, UIList +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.system.data import Data + + +class BIM_PT_systems(Panel): + bl_label = "IFC Systems" + bl_idname = "BIM_PT_systems" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() + + def draw(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + self.props = context.scene.BIMSystemProperties + + row = self.layout.row(align=True) + row.label(text="{} Systems Found".format(len(Data.systems)), icon="OUTLINER") + if self.props.is_editing: + row.operator("bim.add_system", text="", icon="ADD") + row.operator("bim.disable_system_editing_ui", text="", icon="CANCEL") + else: + row.operator("bim.load_systems", text="", icon="GREASEPENCIL") + + if self.props.is_editing: + self.layout.template_list( + "BIM_UL_systems", + "", + self.props, + "systems", + self.props, + "active_system_index", + ) + + if self.props.active_system_id: + self.draw_editable_ui(context) + + def draw_editable_ui(self, context): + for attribute in self.props.system_attributes: + row = self.layout.row(align=True) + row.prop(attribute, "string_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + + +class BIM_PT_object_systems(Panel): + bl_label = "IFC Systems" + bl_idname = "BIM_PT_object_systems" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id + + def draw(self, context): + if not Data.is_loaded: + Data.load(IfcStore.get_file()) + self.props = context.scene.BIMSystemProperties + row = self.layout.row(align=True) + if self.props.is_adding: + row.label(text="Adding Systems", icon="OUTLINER") + row.operator("bim.toggle_assigning_system", text="", icon="CANCEL") + self.layout.template_list( + "BIM_UL_object_systems", + "", + self.props, + "systems", + self.props, + "active_system_index", + ) + else: + row.label(text=f"{len(Data.systems)} Systems in IFC Project", icon="OUTLINER") + row.operator("bim.toggle_assigning_system", text="", icon="ADD") + + systems_object = Data.products.get(context.active_object.BIMObjectProperties.ifc_definition_id, []) + for system_id in systems_object: + row = self.layout.row(align=True) + row.label(text=Data.systems[system_id].get("Name", "Unnamed")) + op = row.operator("bim.unassign_system", text="", icon="X") + op.system = system_id + + if not systems_object: + self.layout.label(text="No System associated with Active Object") + + +class BIM_UL_systems(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + system_id = item.ifc_definition_id + if context.scene.BIMSystemProperties.active_system_id == system_id: + op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") + op.system = system_id + row.operator("bim.edit_system", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_system", text="", icon="CANCEL") + elif context.scene.BIMSystemProperties.active_system_id: + op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") + op.system = system_id + op = row.operator("bim.remove_system", text="", icon="X") + op.system = system_id + else: + op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") + op.system = system_id + op = row.operator("bim.enable_editing_system", text="", icon="GREASEPENCIL") + op.system = system_id + op = row.operator("bim.remove_system", text="", icon="X") + op.system = system_id + + +class BIM_UL_object_systems(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + row = layout.row(align=True) + row.label(text=item.name) + op = row.operator("bim.assign_system", text="", icon="ADD") + op.system = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/module/type/__init__.py b/src/blenderbim/blenderbim/bim/module/type/__init__.py index dc1afcd7e2..72f1a07edc 100644 --- a/src/blenderbim/blenderbim/bim/module/type/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/type/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -7,6 +25,8 @@ classes = ( operator.EnableEditingType, operator.DisableEditingType, operator.SelectSimilarType, + operator.SelectTypeObjects, + operator.SelectType, prop.BIMTypeProperties, prop.BIMTypeObjectProperties, ui.BIM_PT_type, diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index dfb1cce965..4a95206fe9 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.util.schema import ifcopenshell.util.type @@ -22,7 +40,9 @@ class AssignType(bpy.types.Operator): self.file = IfcStore.get_file() relating_type = self.relating_type or int(context.active_object.BIMTypeProperties.relating_type) related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + [bpy.data.objects.get(self.related_object)] + if self.related_object + else context.selected_objects or [context.active_object] ) for related_object in related_objects: oprops = related_object.BIMObjectProperties @@ -39,15 +59,19 @@ class AssignType(bpy.types.Operator): MaterialData.load(IfcStore.get_file(), oprops.ifc_definition_id) representation_ids = GeometryData.products[oprops.ifc_definition_id] if not representation_ids: - pass # TODO: clear geometry? Make void? Make none type? + pass # TODO: clear geometry? Make void? Make none type? has_switched = False for representation_id in representation_ids: representation = GeometryData.representations[representation_id] if representation["ContextOfItems"]["ContextIdentifier"] == "Body": - bpy.ops.bim.switch_representation(obj=related_object.name, ifc_definition_id=representation_id) + bpy.ops.bim.switch_representation( + obj=related_object.name, ifc_definition_id=representation_id, should_switch_all_meshes=False + ) has_switched = True if not has_switched and representation_ids: - bpy.ops.bim.switch_representation(obj=related_object.name, ifc_definition_id=representation_id) + bpy.ops.bim.switch_representation( + obj=related_object.name, ifc_definition_id=representation_id, should_switch_all_meshes=False + ) bpy.ops.bim.disable_editing_type(obj=related_object.name) MaterialData.load(self.file) @@ -57,12 +81,16 @@ class AssignType(bpy.types.Operator): class UnassignType(bpy.types.Operator): bl_idname = "bim.unassign_type" bl_label = "Unassign Type" + bl_options = {"REGISTER", "UNDO"} related_object: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() related_objects = ( - [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects + [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects ) for related_object in related_objects: oprops = related_object.BIMObjectProperties @@ -80,9 +108,10 @@ class UnassignType(bpy.types.Operator): class EnableEditingType(bpy.types.Operator): bl_idname = "bim.enable_editing_type" bl_label = "Enable Editing Type" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.context.active_object.BIMTypeProperties.is_editing_type = True + context.active_object.BIMTypeProperties.is_editing_type = True return {"FINISHED"} @@ -93,19 +122,38 @@ class DisableEditingType(bpy.types.Operator): obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object obj.BIMTypeProperties.is_editing_type = False return {"FINISHED"} +class SelectType(bpy.types.Operator): + bl_idname = "bim.select_type" + bl_label = "Select Type" + bl_options = {"REGISTER", "UNDO"} + related_object: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + related_object = bpy.data.objects.get(self.related_object, context.active_object) + oprops = related_object.BIMObjectProperties + element_type = ifcopenshell.util.element.get_type(self.file.by_id(oprops.ifc_definition_id)) + if element_type is not None: + obj = IfcStore.get_element(element_type.GlobalId) + context.view_layer.objects.active = obj + obj.select_set(True) + return {"FINISHED"} + + class SelectSimilarType(bpy.types.Operator): bl_idname = "bim.select_similar_type" bl_label = "Select Similar Type" + bl_options = {"REGISTER", "UNDO"} related_object: bpy.props.StringProperty() def execute(self, context): self.file = IfcStore.get_file() - related_object = bpy.data.objects.get(self.related_object) if self.related_object else bpy.context.active_object + related_object = bpy.data.objects.get(self.related_object) if self.related_object else context.active_object oprops = related_object.BIMObjectProperties product = self.file.by_id(oprops.ifc_definition_id) declaration = IfcStore.get_schema().declaration_by_name(product.is_a()) @@ -117,7 +165,24 @@ class SelectSimilarType(bpy.types.Operator): related_objects = ifcopenshell.api.run( "type.get_related_objects", self.file, **{"related_object": self.file.by_id(oprops.ifc_definition_id)} ) - for obj in bpy.context.visible_objects: + for obj in context.visible_objects: + if obj.BIMObjectProperties.ifc_definition_id in related_objects: + obj.select_set(True) + return {"FINISHED"} + + +class SelectTypeObjects(bpy.types.Operator): + bl_idname = "bim.select_type_objects" + bl_label = "Select Type Objects" + bl_options = {"REGISTER", "UNDO"} + relating_type: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + relating_type = bpy.data.objects.get(self.relating_type) if self.relating_type else context.active_object + oprops = relating_type.BIMObjectProperties + related_objects = Data.types[oprops.ifc_definition_id] + for obj in context.visible_objects: if obj.BIMObjectProperties.ifc_definition_id in related_objects: obj.select_set(True) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/type/prop.py b/src/blenderbim/blenderbim/bim/module/type/prop.py index fc7b6acab0..7088cd20c6 100644 --- a/src/blenderbim/blenderbim/bim/module/type/prop.py +++ b/src/blenderbim/blenderbim/bim/module/type/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.util.type from blenderbim.bim.prop import StrProperty, Attribute @@ -42,7 +60,7 @@ def getIfcTypes(self, context): def getAvailableTypes(self, context): global available_types_enum - if len(available_types_enum) < 1: + if len(available_types_enum) < 1 and getIfcTypes(self, context): elements = IfcStore.get_file().by_type(self.ifc_class) available_types_enum.extend((str(e.id()), e.Name, "") for e in elements) return available_types_enum @@ -88,5 +106,3 @@ class BIMTypeObjectProperties(PropertyGroup): is_editing_type: BoolProperty(name="Is Editing Type", update=updateTypeDropdowns) relating_type_class: EnumProperty(items=getApplicableTypes, name="Relating Type Class") relating_type: EnumProperty(items=getRelatingTypes, name="Relating Type") - # This is purely a UX thing - blank_relating_type: EnumProperty(items=[("None", "No Types Found", "")], name="Relating Type") diff --git a/src/blenderbim/blenderbim/bim/module/type/ui.py b/src/blenderbim/blenderbim/bim/module/type/ui.py index 564afbc969..67d680e6e4 100644 --- a/src/blenderbim/blenderbim/bim/module/type/ui.py +++ b/src/blenderbim/blenderbim/bim/module/type/ui.py @@ -1,3 +1,22 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.bim.module.type.prop as type_prop from bpy.types import Panel from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.type.data import Data @@ -17,31 +36,43 @@ class BIM_PT_type(Panel): return False if not IfcStore.get_element(props.ifc_definition_id): return False - if props.ifc_definition_id not in Data.products: + if props.ifc_definition_id not in Data.products and props.ifc_definition_id not in Data.types: Data.load(IfcStore.get_file(), props.ifc_definition_id) - if not Data.products[props.ifc_definition_id]: + if props.ifc_definition_id not in Data.products and props.ifc_definition_id not in Data.types: + return False + if not Data.products.get(props.ifc_definition_id, None) and not Data.types.get(props.ifc_definition_id, None): return False return True - def draw(self, context): + oprops = context.active_object.BIMObjectProperties + + if oprops.ifc_definition_id in Data.products: + self.draw_product_ui(context) + else: + self.draw_type_ui(context) + + def draw_type_ui(self, context): + props = context.active_object.BIMTypeProperties + oprops = context.active_object.BIMObjectProperties + row = self.layout.row(align=True) + row.label(text=f"{len(Data.types[oprops.ifc_definition_id])} Typed Objects") + row.operator("bim.select_type_objects", icon="RESTRICT_SELECT_OFF", text="") + + def draw_product_ui(self, context): props = context.active_object.BIMTypeProperties oprops = context.active_object.BIMObjectProperties - if not oprops.ifc_definition_id: - return - if oprops.ifc_definition_id not in Data.products: - Data.load(IfcStore.get_file(), oprops.ifc_definition_id) if props.is_editing_type: row = self.layout.row(align=True) row.prop(props, "relating_type_class", text="") - if props.relating_type: + if type_prop.getRelatingTypes(None, context): row.prop(props, "relating_type", text="") row.operator("bim.assign_type", icon="CHECKMARK", text="") else: - row.prop(props, "blank_relating_type", text="") - row.operator("bim.disable_editing_type", icon="X", text="") + row.label(text="No Types Found") + row.operator("bim.disable_editing_type", icon="CANCEL", text="") else: row = self.layout.row(align=True) name = "{}/{}".format( @@ -52,6 +83,7 @@ class BIM_PT_type(Panel): else: label = name row.label(text=label) + row.operator("bim.select_type", icon="TRACKER", text="") row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="") row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="") if name != "None/None": diff --git a/src/blenderbim/blenderbim/bim/module/unit/__init__.py b/src/blenderbim/blenderbim/bim/module/unit/__init__.py index 87a28250a4..4fd0c0c258 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/unit/__init__.py @@ -1,14 +1,46 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy -from . import ui, operator +from . import ui, prop, operator classes = ( operator.AssignUnit, + operator.UnassignUnit, + operator.LoadUnits, + operator.DisableUnitEditingUI, + operator.RemoveUnit, + operator.AddMonetaryUnit, + operator.AddSIUnit, + operator.AddContextDependentUnit, + operator.EnableEditingUnit, + operator.DisableEditingUnit, + operator.EditUnit, + prop.Unit, + prop.BIMUnitProperties, + ui.BIM_PT_units, + ui.BIM_UL_units, ) def register(): - pass + bpy.types.Scene.BIMUnitProperties = bpy.props.PointerProperty(type=prop.BIMUnitProperties) def unregister(): - pass + del bpy.types.Scene.BIMUnitProperties diff --git a/src/blenderbim/blenderbim/bim/module/unit/operator.py b/src/blenderbim/blenderbim/bim/module/unit/operator.py index debf4b2e9a..aa0527f690 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/operator.py +++ b/src/blenderbim/blenderbim/bim/module/unit/operator.py @@ -1,5 +1,26 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy +import json import ifcopenshell.api +import ifcopenshell.util.unit +import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.unit.data import Data @@ -7,28 +28,40 @@ from ifcopenshell.api.unit.data import Data class AssignUnit(bpy.types.Operator): bl_idname = "bim.assign_unit" bl_label = "Assign Unit" + bl_options = {"REGISTER", "UNDO"} + unit: bpy.props.IntProperty() def execute(self, context): - ifcopenshell.api.run("unit.assign_unit", IfcStore.get_file(), **self.get_units()) - Data.load(IfcStore.get_file()) + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + if self.unit: + ifcopenshell.api.run("unit.assign_unit", self.file, units=[self.file.by_id(self.unit)]) + else: + ifcopenshell.api.run("unit.assign_unit", self.file, **self.get_units(context)) + Data.load(self.file) + if self.unit: + bpy.ops.bim.load_units() return {"FINISHED"} - def get_units(self): + def get_units(self, context): + scene = context.scene units = { "length": { "ifc": None, - "is_metric": bpy.context.scene.unit_settings.system != "IMPERIAL", - "raw": bpy.context.scene.unit_settings.length_unit, + "is_metric": scene.unit_settings.system != "IMPERIAL", + "raw": scene.unit_settings.length_unit, }, "area": { "ifc": None, - "is_metric": bpy.context.scene.unit_settings.system != "IMPERIAL", - "raw": bpy.context.scene.unit_settings.length_unit, + "is_metric": scene.unit_settings.system != "IMPERIAL", + "raw": scene.unit_settings.length_unit, }, "volume": { "ifc": None, - "is_metric": bpy.context.scene.unit_settings.system != "IMPERIAL", - "raw": bpy.context.scene.unit_settings.length_unit, + "is_metric": scene.unit_settings.system != "IMPERIAL", + "raw": scene.unit_settings.length_unit, }, } for data in units.values(): @@ -38,3 +71,224 @@ class AssignUnit(bpy.types.Operator): else: data["raw"] = "FEET" return units + + +class UnassignUnit(bpy.types.Operator): + bl_idname = "bim.unassign_unit" + bl_label = "Unassign Unit" + bl_options = {"REGISTER", "UNDO"} + unit: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run("unit.unassign_unit", IfcStore.get_file(), units=[self.file.by_id(self.unit)]) + Data.load(self.file) + bpy.ops.bim.load_units() + return {"FINISHED"} + + +class LoadUnits(bpy.types.Operator): + bl_idname = "bim.load_units" + bl_label = "Load Units" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMUnitProperties + props.units.clear() + + for ifc_definition_id, unit in Data.units.items(): + name = unit.get("Name", "") + + if unit["type"] == "IfcMonetaryUnit": + name = unit["Currency"] + + if unit["type"] == "IfcSIUnit" and unit["Prefix"]: + if "_" in name: + name_components = name.split("_") + name = f"{name_components[0]} {unit['Prefix']}{name_components[1]}" + else: + name = f"{unit['Prefix']}{name}" + + icon = "MOD_MESHDEFORM" + if unit["type"] == "IfcSIUnit": + icon = "SNAP_GRID" + elif unit["type"] == "IfcMonetaryUnit": + icon = "COPY_ID" + + unit_type = unit.get("UserDefinedType", None) + if not unit_type: + unit_type = unit.get("UnitType", None) + if unit["type"] == "IfcMonetaryUnit": + unit_type = "CURRENCY" + + new = props.units.add() + new.ifc_definition_id = ifc_definition_id + new.name = name + new.unit_type = unit_type + new.is_assigned = ifc_definition_id in Data.unit_assignment + new.icon = icon + + props.is_editing = True + bpy.ops.bim.disable_editing_unit() + return {"FINISHED"} + + +class DisableUnitEditingUI(bpy.types.Operator): + bl_idname = "bim.disable_unit_editing_ui" + bl_label = "Disable Unit Editing UI" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMUnitProperties.is_editing = False + return {"FINISHED"} + + +class RemoveUnit(bpy.types.Operator): + bl_idname = "bim.remove_unit" + bl_label = "Remove Unit" + bl_options = {"REGISTER", "UNDO"} + unit: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMUnitProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("unit.remove_unit", self.file, **{"unit": self.file.by_id(self.unit)}) + Data.load(self.file) + bpy.ops.bim.load_units() + return {"FINISHED"} + + +class AddMonetaryUnit(bpy.types.Operator): + bl_idname = "bim.add_monetary_unit" + bl_label = "Add Monetary Unit" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMUnitProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run("unit.add_monetary_unit", self.file) + Data.load(self.file) + bpy.ops.bim.load_units() + return {"FINISHED"} + + +class AddSIUnit(bpy.types.Operator): + bl_idname = "bim.add_si_unit" + bl_label = "Add SI Unit" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMUnitProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "unit.add_si_unit", + self.file, + unit_type=props.named_unit_types, + name=ifcopenshell.util.unit.si_type_names[props.named_unit_types], + ) + Data.load(self.file) + bpy.ops.bim.load_units() + return {"FINISHED"} + + +class AddContextDependentUnit(bpy.types.Operator): + bl_idname = "bim.add_context_dependent_unit" + bl_label = "Add Context Dependent Unit" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMUnitProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "unit.add_context_dependent_unit", self.file, unit_type=props.named_unit_types, name="THINGAMAJIG" + ) + Data.load(self.file) + bpy.ops.bim.load_units() + return {"FINISHED"} + + +class EnableEditingUnit(bpy.types.Operator): + bl_idname = "bim.enable_editing_unit" + bl_label = "Enable Editing Unit" + bl_options = {"REGISTER", "UNDO"} + unit: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMUnitProperties + props.unit_attributes.clear() + data = Data.units[self.unit] + blenderbim.bim.helper.import_attributes( + data["type"], + props.unit_attributes, + data, + lambda name, prop, data: self.import_attributes(name, prop, data, context), + ) + props.active_unit_id = self.unit + return {"FINISHED"} + + def import_attributes(self, name, prop, data, context): + if name == "Dimensions" and data["type"] != "IfcSIUnit": + new = context.scene.BIMUnitProperties.unit_attributes.add() + new.name = name + new.is_null = data[name] is None + new.is_optional = False + new.data_type = "string" + new.string_value = json.dumps([e for e in IfcStore.get_file().by_id(data["id"]).Dimensions]) + return True + + +class DisableEditingUnit(bpy.types.Operator): + bl_idname = "bim.disable_editing_unit" + bl_label = "Disable Editing Unit" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + context.scene.BIMUnitProperties.active_unit_id = 0 + return {"FINISHED"} + + +class EditUnit(bpy.types.Operator): + bl_idname = "bim.edit_unit" + bl_label = "Edit Unit" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMUnitProperties + attributes = blenderbim.bim.helper.export_attributes(props.unit_attributes, self.export_attributes) + self.file = IfcStore.get_file() + unit = self.file.by_id(props.active_unit_id) + if unit.is_a("IfcMonetaryUnit"): + ifcopenshell.api.run("unit.edit_monetary_unit", self.file, **{"unit": unit, "attributes": attributes}) + elif unit.is_a("IfcDerivedUnit"): + ifcopenshell.api.run("unit.edit_derived_unit", self.file, **{"unit": unit, "attributes": attributes}) + elif unit.is_a("IfcNamedUnit"): + ifcopenshell.api.run("unit.edit_named_unit", self.file, **{"unit": unit, "attributes": attributes}) + Data.load(IfcStore.get_file()) + bpy.ops.bim.load_units() + return {"FINISHED"} + + def export_attributes(self, attributes, prop): + if prop.name == "Dimensions": + try: + attributes[prop.name] = json.loads(prop.get_value()) + except: + attributes[prop.name] = (0, 0, 0, 0, 0, 0, 0) + return True diff --git a/src/blenderbim/blenderbim/bim/module/unit/prop.py b/src/blenderbim/blenderbim/bim/module/unit/prop.py new file mode 100644 index 0000000000..9dcddcbd1a --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/unit/prop.py @@ -0,0 +1,83 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell +import ifcopenshell.util.schema +import ifcopenshell.util.attribute +from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.prop import StrProperty, Attribute +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +unitclasses_enum = [] +namedunittypes_enum = [] + + +def purge(): + global unitclasses_enum + global namedunittypes_enum + unitclasses_enum = [] + namedunittypes_enum = [] + + +def getUnitClasses(self, context): + global unitclasses_enum + if not len(unitclasses_enum) and IfcStore.get_file(): + declarations = ifcopenshell.util.schema.get_subtypes(IfcStore.get_schema().declaration_by_name("IfcNamedUnit")) + unitclasses_enum.extend([(c, c, "") for c in sorted([d.name() for d in declarations])]) + unitclasses_enum.extend([("IfcDerivedUnit", "IfcDerivedUnit", ""), ("IfcMonetaryUnit", "IfcMonetaryUnit", "")]) + return unitclasses_enum + + +def getNamedUnitTypes(self, context): + global namedunittypes_enum + if not len(namedunittypes_enum) and IfcStore.get_file(): + values = ifcopenshell.util.attribute.get_enum_items( + IfcStore.get_schema().declaration_by_name("IfcNamedUnit").all_attributes()[1] + ) + namedunittypes_enum.extend([(c, c, "") for c in sorted(values)]) + return namedunittypes_enum + + +class Unit(PropertyGroup): + name: StringProperty(name="Name") + unit_type: StringProperty(name="Unit Type") + is_assigned: BoolProperty(name="Is Assigned") + icon: StringProperty(name="Icon") + ifc_definition_id: IntProperty(name="IFC Definition ID") + + +class BIMUnitProperties(PropertyGroup): + is_editing: BoolProperty(name="Is Editing") + units: CollectionProperty(name="Units", type=Unit) + active_unit_index: IntProperty(name="Active Unit Index") + active_unit_id: IntProperty(name="Active Unit Id") + unit_classes: EnumProperty(items=getUnitClasses, name="Unit Classes") + named_unit_types: EnumProperty(items=getNamedUnitTypes, name="Named Unit Types") + unit_attributes: CollectionProperty(name="Unit Attributes", type=Attribute) diff --git a/src/blenderbim/blenderbim/bim/module/unit/ui.py b/src/blenderbim/blenderbim/bim/module/unit/ui.py index e69de29bb2..790e960fe9 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/ui.py +++ b/src/blenderbim/blenderbim/bim/module/unit/ui.py @@ -0,0 +1,107 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.bim.helper +from bpy.types import Panel, UIList +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.unit.data import Data + + +class BIM_PT_units(Panel): + bl_label = "IFC Units" + bl_idname = "BIM_PT_units" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + return file + + def draw(self, context): + self.file = IfcStore.get_file() + if not Data.is_loaded: + Data.load(self.file) + self.props = context.scene.BIMUnitProperties + + row = self.layout.row(align=True) + row.label(text="{} Units Found".format(len(Data.units)), icon="SNAP_GRID") + if self.props.is_editing: + row.operator("bim.disable_unit_editing_ui", text="", icon="CANCEL") + else: + row.operator("bim.load_units", text="", icon="GREASEPENCIL") + + if not self.props.is_editing: + return + + row = self.layout.row(align=True) + row.prop(self.props, "unit_classes", text="") + + if self.props.unit_classes == "IfcMonetaryUnit": + row.operator("bim.add_monetary_unit", text="", icon="ADD") + elif self.props.unit_classes == "IfcDerivedUnit": + pass # TODO + elif self.props.unit_classes == "IfcSIUnit": + row.prop(self.props, "named_unit_types", text="") + row.operator("bim.add_si_unit", text="", icon="ADD") + elif self.props.unit_classes == "IfcContextDependentUnit": + row.prop(self.props, "named_unit_types", text="") + row.operator("bim.add_context_dependent_unit", text="", icon="ADD") + + self.layout.template_list( + "BIM_UL_units", + "", + self.props, + "units", + self.props, + "active_unit_index", + ) + + if self.props.active_unit_id: + self.draw_editable_ui(context) + + def draw_editable_ui(self, context): + blenderbim.bim.helper.draw_attributes(self.props.unit_attributes, self.layout) + + +class BIM_UL_units(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMUnitProperties + if item: + row = layout.row(align=True) + row.label(text=item.unit_type or "No Type", icon=item.icon) + row.label(text=item.name or "Unnamed") + + if item.is_assigned: + op = row.operator("bim.unassign_unit", text="", icon="KEYFRAME_HLT", emboss=False) + op.unit = item.ifc_definition_id + else: + op = row.operator("bim.assign_unit", text="", icon="KEYFRAME", emboss=False) + op.unit = item.ifc_definition_id + + if props.active_unit_id == item.ifc_definition_id: + row.operator("bim.edit_unit", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_unit", text="", icon="CANCEL") + elif props.active_unit_id: + row.operator("bim.remove_unit", text="", icon="X").unit = item.ifc_definition_id + else: + op = row.operator("bim.enable_editing_unit", text="", icon="GREASEPENCIL") + op.unit = item.ifc_definition_id + row.operator("bim.remove_unit", text="", icon="X").unit = item.ifc_definition_id diff --git a/src/blenderbim/blenderbim/bim/module/void/__init__.py b/src/blenderbim/blenderbim/bim/module/void/__init__.py index 3727f40508..c2afbcbaf6 100644 --- a/src/blenderbim/blenderbim/bim/module/void/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/void/__init__.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from . import ui, prop, operator @@ -6,6 +24,8 @@ classes = ( operator.RemoveOpening, operator.AddFilling, operator.RemoveFilling, + operator.ToggleOpeningVisibility, + operator.ToggleDecompositionParenting, prop.VoidProperties, ui.BIM_PT_voids, ) diff --git a/src/blenderbim/blenderbim/bim/module/void/operator.py b/src/blenderbim/blenderbim/bim/module/void/operator.py index a25678120b..e1276e3ea8 100644 --- a/src/blenderbim/blenderbim/bim/module/void/operator.py +++ b/src/blenderbim/blenderbim/bim/module/void/operator.py @@ -1,32 +1,49 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy import ifcopenshell.api +import ifcopenshell.util.representation from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.void.data import Data -from ifcopenshell.api.context.data import Data as ContextData class AddOpening(bpy.types.Operator): bl_idname = "bim.add_opening" bl_label = "Add Opening" + bl_options = {"REGISTER", "UNDO"} opening: bpy.props.StringProperty() obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = context.scene.objects.get(self.obj, context.active_object) opening = bpy.data.objects.get(self.opening) + if opening is None: + return {"FINISHED"} opening.display_type = "WIRE" if not opening.BIMObjectProperties.ifc_definition_id: - body_context_id = None - if not ContextData.is_loaded: - ContextData.load(IfcStore.get_file()) - for context in ContextData.contexts.values(): - for subcontext_id, subcontext in context["HasSubContexts"].items(): - if subcontext["ContextType"] == "Model" and subcontext["ContextIdentifier"] == "Body": - body_context_id = subcontext_id - break - if not body_context_id: + body_context = ifcopenshell.util.representation.get_context(IfcStore.get_file(), "Model", "Body") + if not body_context: return {"FINISHED"} - bpy.ops.bim.assign_class(obj=opening.name, ifc_class="IfcOpeningElement", context_id=body_context_id) + bpy.ops.bim.assign_class(obj=opening.name, ifc_class="IfcOpeningElement", context_id=body_context.id()) self.file = IfcStore.get_file() element_id = obj.BIMObjectProperties.ifc_definition_id @@ -38,19 +55,20 @@ class AddOpening(bpy.types.Operator): "element": self.file.by_id(element_id), }, ) - Data.load(IfcStore.get_file(), element_id) + Data.load(self.file, element_id) - has_modifier = False - - for modifier in obj.modifiers: - if modifier.type == "BOOLEAN" and modifier.object and modifier.object == opening: - has_modifier = True - break - - if not has_modifier: + try: + modifier = next(m for m in obj.modifiers if m.type == "BOOLEAN" and m.object == opening) + except StopIteration: modifier = obj.modifiers.new("IfcOpeningElement", "BOOLEAN") - modifier.operation = "DIFFERENCE" modifier.object = opening + finally: + modifier.operation = "DIFFERENCE" + modifier.solver = "EXACT" + modifier.use_self = True + modifier.operand_type = "OBJECT" + + context.view_layer.objects.active = obj return {"FINISHED"} @@ -65,19 +83,30 @@ class RemoveOpening(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() + is_modifier_removed = False for modifier in obj.modifiers: if modifier.type != "BOOLEAN": continue if modifier.object and modifier.object.BIMObjectProperties.ifc_definition_id == self.opening_id: - IfcStore.unlink_element(obj=modifier.object) - if "/" in modifier.object.name and modifier.object.name[0:3] == "Ifc": - modifier.object.name = "/".join(modifier.object.name.split("/")[1:]) + is_modifier_removed = True obj.modifiers.remove(modifier) break + opening = IfcStore.get_element(self.opening_id) + opening.name = "/".join(opening.name.split("/")[1:]) + IfcStore.unlink_element(obj=opening) + ifcopenshell.api.run("void.remove_opening", self.file, **{"opening": self.file.by_id(self.opening_id)}) + + if not is_modifier_removed: + bpy.ops.bim.switch_representation( + ifc_definition_id=obj.data.BIMMeshProperties.ifc_definition_id, + should_reload=True, + should_switch_all_meshes=True, + ) + Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) return {"FINISHED"} @@ -85,36 +114,115 @@ class RemoveOpening(bpy.types.Operator): class AddFilling(bpy.types.Operator): bl_idname = "bim.add_filling" bl_label = "Add Filling" + bl_options = {"REGISTER", "UNDO"} opening: bpy.props.StringProperty() obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object - opening = bpy.data.objects.get(self.opening) if self.opening else context.scene.VoidProperties.desired_opening + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = context.scene.objects.get(self.obj, context.active_object) + opening = context.scene.objects.get(self.opening, context.scene.VoidProperties.desired_opening) + if opening is None: + return {"FINISHED"} self.file = IfcStore.get_file() element_id = obj.BIMObjectProperties.ifc_definition_id + opening_id = opening.BIMObjectProperties.ifc_definition_id + if not element_id or not opening_id: + return {"FINISHED"} ifcopenshell.api.run( "void.add_filling", self.file, **{ - "opening": self.file.by_id(opening.BIMObjectProperties.ifc_definition_id), + "opening": self.file.by_id(opening_id), "element": self.file.by_id(element_id), }, ) - Data.load(IfcStore.get_file(), element_id) + Data.load(self.file, element_id) return {"FINISHED"} class RemoveFilling(bpy.types.Operator): bl_idname = "bim.remove_filling" bl_label = "Remove Filling" + bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() def execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() ifcopenshell.api.run( "void.remove_filling", self.file, **{"element": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)} ) Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) return {"FINISHED"} + + +class ToggleOpeningVisibility(bpy.types.Operator): + bl_idname = "bim.toggle_opening_visibility" + bl_label = "Toggle Opening Visibility" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + for project in [c for c in context.view_layer.layer_collection.children if "IfcProject" in c.name]: + for collection in [c for c in project.children if "IfcOpeningElements" in c.name]: + collection.hide_viewport = not collection.hide_viewport + return {"FINISHED"} + + +class ToggleDecompositionParenting(bpy.types.Operator): + bl_idname = "bim.toggle_decomposition_parenting" + bl_label = "Toggle Decomposition Parenting" + bl_options = {"REGISTER", "UNDO"} + obj: bpy.props.StringProperty() + + def execute(self, context): + obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object + self.file = IfcStore.get_file() + is_parenting = None + + self.decompositions = {} + self.load_decompositions(obj) + + for parent, children in self.decompositions.items(): + bpy.ops.bim.dynamically_void_product(obj=parent.name) + for child in children: + bpy.ops.bim.dynamically_void_product(obj=child.name) + if is_parenting is None: + is_parenting = not bool(child.parent) + + if is_parenting: + child.parent = parent + child.matrix_parent_inverse = parent.matrix_world.inverted() + else: + parent_matrix_world = child.matrix_world.copy() + child.parent = None + child.matrix_world = parent_matrix_world + + return {"FINISHED"} + + def load_decompositions(self, parent_obj): + element = self.file.by_id(parent_obj.BIMObjectProperties.ifc_definition_id) + for rel in self.file.get_inverse(element): + if rel.is_a("IfcRelDecomposes"): + if rel[4] != element: + continue + if isinstance(rel[5], tuple): + for related_object in rel[5]: + self.add_decomposition(parent_obj, related_object) + else: + self.add_decomposition(parent_obj, rel[5]) + elif rel.is_a("IfcRelFillsElement"): + if rel.RelatingOpeningElement != element: + continue + self.add_decomposition(parent_obj, rel.RelatedBuildingElement) + + def add_decomposition(self, parent_obj, child_element): + child_obj = IfcStore.get_element(child_element.id()) + if child_obj: + self.decompositions.setdefault(parent_obj, []).append(child_obj) + self.load_decompositions(child_obj) diff --git a/src/blenderbim/blenderbim/bim/module/void/prop.py b/src/blenderbim/blenderbim/bim/module/void/prop.py index 4dd1185fe6..faa3813f46 100644 --- a/src/blenderbim/blenderbim/bim/module/void/prop.py +++ b/src/blenderbim/blenderbim/bim/module/void/prop.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import PropertyGroup from bpy.props import PointerProperty diff --git a/src/blenderbim/blenderbim/bim/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index 48c29cdcdc..572ad8f71a 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -1,3 +1,21 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import bpy from bpy.types import Panel from ifcopenshell.api.void.data import Data diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 18eee614e7..1df58dba4f 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -1,25 +1,41 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import time import json +import tempfile import logging import webbrowser import ifcopenshell - -# Deleting the below drawing import breaks svgwrite's ElementTree appending because ... magic? -import blenderbim.bim.module.drawing +import blenderbim.bim.handler from . import export_ifc -from . import import_ifc from . import schema from blenderbim.bim.ifc import IfcStore -from bpy_extras.io_utils import ImportHelper -from mathutils import Vector, Matrix, Euler, geometry -from math import radians, degrees, atan, tan, cos, sin +from mathutils import Vector, Matrix, Euler +from math import radians class ExportIFC(bpy.types.Operator): bl_idname = "export_ifc.bim" bl_label = "Export IFC" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -30,8 +46,8 @@ class ExportIFC(bpy.types.Operator): if not IfcStore.get_file(): self.report({"ERROR"}, "No IFC project is available for export - create or import a project first.") return {"FINISHED"} - if bpy.context.scene.BIMProperties.ifc_file: - self.filepath = bpy.context.scene.BIMProperties.ifc_file + if context.scene.BIMProperties.ifc_file: + self.filepath = context.scene.BIMProperties.ifc_file return self.execute(context) if not self.filepath: self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".ifc") @@ -40,10 +56,18 @@ class ExportIFC(bpy.types.Operator): return {"RUNNING_MODAL"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): start = time.time() logger = logging.getLogger("ExportIFC") + path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log") + if not os.access(context.scene.BIMProperties.data_dir, os.W_OK): + path_log = os.path.join(tempfile.mkdtemp(), "process.log") logging.basicConfig( - filename=os.path.join(context.scene.BIMProperties.data_dir, "process.log"), filemode="a", level=logging.DEBUG + filename=path_log, + filemode="a", + level=logging.DEBUG, ) extension = self.filepath.split(".")[-1] if extension == "ifczip": @@ -62,70 +86,25 @@ class ExportIFC(bpy.types.Operator): ifc_exporter.export() settings.logger.info("Export finished in {:.2f} seconds".format(time.time() - start)) print("Export finished in {:.2f} seconds".format(time.time() - start)) - if not bpy.context.scene.DocProperties.ifc_files: - new = bpy.context.scene.DocProperties.ifc_files.add() + scene = context.scene + if not scene.DocProperties.ifc_files: + new = scene.DocProperties.ifc_files.add() new.name = output_file - if not bpy.context.scene.BIMProperties.ifc_file: - bpy.context.scene.BIMProperties.ifc_file = output_file + if not scene.BIMProperties.ifc_file: + scene.BIMProperties.ifc_file = output_file if bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath: bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath) + blenderbim.bim.handler.purge_module_data() return {"FINISHED"} -class ImportIFC(bpy.types.Operator, ImportHelper): +class ImportIFC(bpy.types.Operator): bl_idname = "import_ifc.bim" bl_label = "Import IFC" - filename_ext = ".ifc" - filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - - should_auto_set_workarounds: bpy.props.BoolProperty(name="Automatically Set Vendor Workarounds", default=True) - should_use_cpu_multiprocessing: bpy.props.BoolProperty(name="Import with CPU Multiprocessing", default=True) - should_merge_by_class: bpy.props.BoolProperty(name="Import and Merge by Class", default=False) - should_merge_by_material: bpy.props.BoolProperty(name="Import and Merge by Material", default=False) - should_merge_materials_by_colour: bpy.props.BoolProperty(name="Import and Merge Materials by Colour", default=False) - should_clean_mesh: bpy.props.BoolProperty(name="Import and Clean Mesh", default=True) - deflection_tolerance: bpy.props.FloatProperty(name="Import Deflection Tolerance", default=0.001) - angular_tolerance: bpy.props.FloatProperty(name="Import Angular Tolerance", default=0.5) - should_offset_model: bpy.props.BoolProperty(name="Import and Offset Model", default=False) - model_offset_coordinates: bpy.props.StringProperty(name="Model Offset Coordinates", default="0,0,0") - ifc_import_filter: bpy.props.EnumProperty( - items=[ - ("NONE", "None", ""), - ("WHITELIST", "Whitelist", ""), - ("BLACKLIST", "Blacklist", ""), - ], - name="Import Filter", - ) - ifc_selector: bpy.props.StringProperty(default="", name="IFC Selector") + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - start = time.time() - logger = logging.getLogger("ImportIFC") - logging.basicConfig( - filename=os.path.join(bpy.context.scene.BIMProperties.data_dir, "process.log"), filemode="a", level=logging.DEBUG - ) - - settings = import_ifc.IfcImportSettings.factory(context, self.filepath, logger) - settings.should_auto_set_workarounds = self.should_auto_set_workarounds - settings.should_use_cpu_multiprocessing = self.should_use_cpu_multiprocessing - settings.should_merge_by_class = self.should_merge_by_class - settings.should_merge_by_material = self.should_merge_by_material - settings.should_merge_materials_by_colour = self.should_merge_materials_by_colour - settings.should_clean_mesh = self.should_clean_mesh - settings.deflection_tolerance = self.deflection_tolerance - settings.angular_tolerance = self.angular_tolerance - settings.should_offset_model = self.should_offset_model - settings.model_offset_coordinates = ( - [float(o) for o in self.model_offset_coordinates.split(",")] if self.model_offset_coordinates else (0, 0, 0) - ) - settings.ifc_import_filter = self.ifc_import_filter - settings.ifc_selector = self.ifc_selector - - settings.logger.info("Starting import") - ifc_importer = import_ifc.IfcImporter(settings) - ifc_importer.execute() - settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start)) - print("Import finished in {:.2f} seconds".format(time.time() - start)) + bpy.ops.bim.load_project("INVOKE_DEFAULT") return {"FINISHED"} @@ -139,28 +118,16 @@ class OpenUri(bpy.types.Operator): return {"FINISHED"} -class SelectExternalMaterialDir(bpy.types.Operator): - bl_idname = "bim.select_external_material_dir" - bl_label = "Select Material File" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - bpy.context.active_object.active_material.BIMMaterialProperties.location = self.filepath - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - class SelectIfcFile(bpy.types.Operator): bl_idname = "bim.select_ifc_file" bl_label = "Select IFC File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) def execute(self, context): - bpy.context.scene.BIMProperties.ifc_file = self.filepath + if os.path.exists(self.filepath) and "ifc" in os.path.splitext(self.filepath)[1]: + context.scene.BIMProperties.ifc_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -171,10 +138,11 @@ class SelectIfcFile(bpy.types.Operator): class SelectDataDir(bpy.types.Operator): bl_idname = "bim.select_data_dir" bl_label = "Select Data Directory" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.BIMProperties.data_dir = self.filepath + context.scene.BIMProperties.data_dir = os.path.dirname(self.filepath) return {"FINISHED"} def invoke(self, context, event): @@ -185,10 +153,11 @@ class SelectDataDir(bpy.types.Operator): class SelectSchemaDir(bpy.types.Operator): bl_idname = "bim.select_schema_dir" bl_label = "Select Schema Directory" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - bpy.context.scene.BIMProperties.schema_dir = self.filepath + context.scene.BIMProperties.schema_dir = os.path.dirname(self.filepath) return {"FINISHED"} def invoke(self, context, event): @@ -196,52 +165,6 @@ class SelectSchemaDir(bpy.types.Operator): return {"RUNNING_MODAL"} -class FetchExternalMaterial(bpy.types.Operator): - bl_idname = "bim.fetch_external_material" - bl_label = "Fetch External Material" - - def execute(self, context): - location = bpy.context.active_object.active_material.BIMMaterialProperties.location - if location[-6:] != ".mpass": - return {"FINISHED"} - if not os.path.isabs(location): - location = os.path.join(bpy.context.scene.BIMProperties.data_dir, location) - with open(location) as f: - self.material_pass = json.load(f) - if bpy.context.scene.render.engine == "BLENDER_EEVEE" and "eevee" in self.material_pass: - self.fetch_eevee_or_cycles("eevee") - elif bpy.context.scene.render.engine == "CYCLES" and "cycles" in self.material_pass: - self.fetch_eevee_or_cycles("cycles") - return {"FINISHED"} - - def fetch_eevee_or_cycles(self, name): - identification = bpy.context.active_object.active_material.BIMMaterialProperties.identification - uri = self.material_pass[name]["uri"] - if not os.path.isabs(uri): - uri = os.path.join(bpy.context.scene.BIMProperties.data_dir, uri) - bpy.ops.wm.link(filename=identification, directory=os.path.join(uri, "Material")) - for material in bpy.data.materials: - if material.name == identification and material.library: - bpy.context.active_object.material_slots[0].material = material - return - - -class FetchObjectPassport(bpy.types.Operator): - bl_idname = "bim.fetch_object_passport" - bl_label = "Fetch Object Passport" - - def execute(self, context): - for reference in bpy.context.active_object.BIMObjectProperties.document_references: - reference = bpy.context.scene.BIMProperties.document_references[reference.name] - if reference.location[-6:] == ".blend": - self.fetch_blender(reference) - return {"FINISHED"} - - def fetch_blender(self, reference): - bpy.ops.wm.link(filename=reference.name, directory=os.path.join(reference.location, "Mesh")) - bpy.context.active_object.data = bpy.data.meshes[reference.name] - - class OpenUpstream(bpy.types.Operator): bl_idname = "bim.open_upstream" bl_label = "Open Upstream Reference" @@ -259,102 +182,42 @@ class OpenUpstream(bpy.types.Operator): return {"FINISHED"} -class CopyPropertyToSelection(bpy.types.Operator): - bl_idname = "bim.copy_property_to_selection" - bl_label = "Copy Property To Selection" - pset_name: bpy.props.StringProperty() - prop_name: bpy.props.StringProperty() - prop_value: bpy.props.StringProperty() - - def execute(self, context): - for obj in bpy.context.selected_objects: - if "/" not in obj.name: - continue - pset = obj.BIMObjectProperties.psets.get(self.pset_name) - if not pset: - applicable_psets = schema.ifc.psetqto.get_applicable(obj.name.split("/")[0], pset_only=True) - for pset_template in applicable_psets: - if pset_template.Name == self.pset_name: - break - else: - continue - pset = obj.BIMObjectProperties.psets.add() - pset.name = self.pset_name - for template_prop_name in (p.Name for p in pset_template.HasPropertyTemplates): - prop = pset.properties.add() - prop.name = template_prop_name - prop = pset.properties.get(self.prop_name) - if prop: - prop.string_value = self.prop_value - return {"FINISHED"} - - -class CopyAttributeToSelection(bpy.types.Operator): - bl_idname = "bim.copy_attribute_to_selection" - bl_label = "Copy Attribute To Selection" - attribute_name: bpy.props.StringProperty() - attribute_value: bpy.props.StringProperty() - - def execute(self, context): - # TODO: reimplement - self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(bpy.context.scene.BIMProperties.export_schema) - self.applicable_attributes_cache = {} - for obj in bpy.context.selected_objects: - if "/" not in obj.name: - continue - attribute = obj.BIMObjectProperties.attributes.get(self.attribute_name) - if not attribute: - applicable_attributes = self.get_applicable_attributes(obj.name.split("/")[0]) - if self.attribute_name not in applicable_attributes: - continue - attribute = obj.BIMObjectProperties.attributes.add() - attribute.name = self.attribute_name - attribute.string_value = self.attribute_value - return {"FINISHED"} - - def get_applicable_attributes(self, ifc_class): - if ifc_class not in self.applicable_attributes_cache: - self.applicable_attributes_cache[ifc_class] = [ - a.name() for a in self.schema.declaration_by_name(ifc_class).all_attributes() - ] - return self.applicable_attributes_cache[ifc_class] - - class AddSectionPlane(bpy.types.Operator): bl_idname = "bim.add_section_plane" bl_label = "Add Temporary Section Cutaway" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - obj = self.create_section_obj() + obj = self.create_section_obj(context) if not self.has_section_override_node(): self.create_section_compare_node() - self.create_section_override_node(obj) + self.create_section_override_node(obj, context) else: self.append_obj_to_section_override_node(obj) - self.add_default_material_if_none_exists() + self.add_default_material_if_none_exists(context) self.override_materials() return {"FINISHED"} - def create_section_obj(self): + def create_section_obj(self, context): section = bpy.data.objects.new("Section", None) section.empty_display_type = "SINGLE_ARROW" section.empty_display_size = 5 section.show_in_front = True if ( - bpy.context.active_object - and bpy.context.active_object.select_get() - and isinstance(bpy.context.active_object.data, bpy.types.Camera) + context.active_object + and context.active_object.select_get() + and isinstance(context.active_object.data, bpy.types.Camera) ): section.matrix_world = ( - bpy.context.active_object.matrix_world @ Euler((radians(180.0), 0.0, 0.0), "XYZ").to_matrix().to_4x4() + context.active_object.matrix_world @ Euler((radians(180.0), 0.0, 0.0), "XYZ").to_matrix().to_4x4() ) else: section.rotation_euler = Euler((radians(180.0), 0.0, 0.0), "XYZ") - section.location = bpy.context.scene.cursor.location + section.location = context.scene.cursor.location collection = bpy.data.collections.get("Sections") if not collection: collection = bpy.data.collections.new("Sections") - bpy.context.scene.collection.children.link(collection) + context.scene.collection.children.link(collection) collection.objects.link(section) return section @@ -386,7 +249,7 @@ class AddSectionPlane(bpy.types.Operator): group.links.new(add.outputs[0], compare.inputs[0]) group.links.new(compare.outputs[0], group_output.inputs[""]) - def create_section_override_node(self, obj): + def create_section_override_node(self, obj, context): group = bpy.data.node_groups.new("Section Override", type="ShaderNodeTree") group_input = group.nodes.new(type="NodeGroupInput") @@ -395,7 +258,7 @@ class AddSectionPlane(bpy.types.Operator): backfacing = group.nodes.new(type="ShaderNodeNewGeometry") backfacing_mix = group.nodes.new(type="ShaderNodeMixShader") emission = group.nodes.new(type="ShaderNodeEmission") - emission.inputs[0].default_value = list(bpy.context.scene.BIMProperties.section_plane_colour) + [1] + emission.inputs[0].default_value = list(context.scene.BIMProperties.section_plane_colour) + [1] group.links.new(backfacing.outputs["Backfacing"], backfacing_mix.inputs[0]) group.links.new(group_input.outputs[""], backfacing_mix.inputs[1]) @@ -440,16 +303,16 @@ class AddSectionPlane(bpy.types.Operator): section_compare.name = "Last Section Compare" - def add_default_material_if_none_exists(self): + def add_default_material_if_none_exists(self, context): material = bpy.data.materials.get("Section Override") if not material: material = bpy.data.materials.new("Section Override") material.use_nodes = True - if bpy.context.scene.BIMProperties.should_section_selected_objects: - objects = list(bpy.context.selected_objects) + if context.scene.BIMProperties.should_section_selected_objects: + objects = list(context.selected_objects) else: - objects = list(bpy.context.visible_objects) + objects = list(context.visible_objects) for obj in objects: aggregate = obj.instance_collection @@ -490,12 +353,15 @@ class AddSectionPlane(bpy.types.Operator): class RemoveSectionPlane(bpy.types.Operator): bl_idname = "bim.remove_section_plane" bl_label = "Remove Temporary Section Cutaway" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.active_object and bpy.data.node_groups.get("Section Override") def execute(self, context): - name = bpy.context.active_object.name + name = context.active_object.name section_override = bpy.data.node_groups.get("Section Override") - if not section_override: - return {"FINISHED"} for node in section_override.nodes: if node.type != "TEX_COORD" or node.object.name != name: continue @@ -508,7 +374,7 @@ class RemoveSectionPlane(bpy.types.Operator): else: # If it links to section_compare.inputs[0] if section_compare.inputs[1].links[0].from_node.name == "Mock Section": # Then it is the very last section. Purge everything. - self.purge_all_section_data() + self.purge_all_section_data(context) return {"FINISHED"} section_override.links.new( section_compare.inputs[1].links[0].from_socket, section_compare.outputs[0].links[0].to_socket @@ -521,10 +387,10 @@ class RemoveSectionPlane(bpy.types.Operator): section_mix = section_override.nodes.get("Section Mix") new_last_compare = section_mix.inputs[0].links[0].from_node new_last_compare.name = "Last Section Compare" - bpy.ops.object.delete({"selected_objects": [bpy.context.active_object]}) + bpy.ops.object.delete({"selected_objects": [context.active_object]}) return {"FINISHED"} - def purge_all_section_data(self): + def purge_all_section_data(self, context): bpy.data.materials.remove(bpy.data.materials.get("Section Override")) for material in bpy.data.materials: if not material.node_tree: @@ -538,12 +404,13 @@ class RemoveSectionPlane(bpy.types.Operator): material.node_tree.nodes.remove(override) bpy.data.node_groups.remove(bpy.data.node_groups.get("Section Override")) bpy.data.node_groups.remove(bpy.data.node_groups.get("Section Compare")) - bpy.ops.object.delete({"selected_objects": [bpy.context.active_object]}) + bpy.ops.object.delete({"selected_objects": [context.active_object]}) class ReloadIfcFile(bpy.types.Operator): bl_idname = "bim.reload_ifc_file" bl_label = "Reload IFC File" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): # TODO: reimplement. See #1222. @@ -553,9 +420,10 @@ class ReloadIfcFile(bpy.types.Operator): class AddIfcFile(bpy.types.Operator): bl_idname = "bim.add_ifc_file" bl_label = "Add IFC File" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.context.scene.DocProperties.ifc_files.add() + context.scene.DocProperties.ifc_files.add() return {"FINISHED"} @@ -563,21 +431,26 @@ class RemoveIfcFile(bpy.types.Operator): bl_idname = "bim.remove_ifc_file" bl_label = "Remove IFC File" index: bpy.props.IntProperty() + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.context.scene.DocProperties.ifc_files.remove(self.index) + context.scene.DocProperties.ifc_files.remove(self.index) return {"FINISHED"} class SetOverrideColour(bpy.types.Operator): bl_idname = "bim.set_override_colour" bl_label = "Set Override Colour" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects def execute(self, context): - result = 0 - for obj in bpy.context.selected_objects: - obj.color = bpy.context.scene.BIMProperties.override_colour - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + for obj in context.selected_objects: + obj.color = context.scene.BIMProperties.override_colour + area = next(area for area in context.screen.areas if area.type == "VIEW_3D") area.spaces[0].shading.color_type = "OBJECT" return {"FINISHED"} @@ -585,6 +458,11 @@ class SetOverrideColour(bpy.types.Operator): class SetViewportShadowFromSun(bpy.types.Operator): bl_idname = "bim.set_viewport_shadow_from_sun" bl_label = "Set Viewport Shadow from Sun" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.active_object def execute(self, context): # The vector used for the light direction is a bit funny @@ -595,48 +473,25 @@ class SetViewportShadowFromSun(bpy.types.Operator): return {"FINISHED"} -class LinkIfc(bpy.types.Operator): - bl_idname = "bim.link_ifc" - bl_label = "Link IFC" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - # bpy.context.active_object.active_material.BIMMaterialProperties.location = self.filepath - # coll_name = "MyCollection" - - with bpy.data.libraries.load(self.filepath, link=True) as (data_from, data_to): - data_to.scenes = data_from.scenes - - for scene in bpy.data.scenes: - if not scene.library or scene.library.filepath != self.filepath: - continue - for child in scene.collection.children: - if "IfcProject" not in child.name: - continue - bpy.data.scenes[0].collection.children.link(child) - - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - class SnapSpacesTogether(bpy.types.Operator): bl_idname = "bim.snap_spaces_together" bl_label = "Snap Spaces Together" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.selected_objects def execute(self, context): threshold = 0.5 processed_polygons = set() - for obj in bpy.context.selected_objects: - if obj.type != "MESH": - continue + selected_mesh_objects = [o for o in context.selected_objects if o.type == "MESH"] + for obj in selected_mesh_objects: for polygon in obj.data.polygons: center = obj.matrix_world @ polygon.center distance = None - for obj2 in bpy.context.selected_objects: - if obj2 == obj or obj.type != "MESH": + for obj2 in selected_mesh_objects: + if obj2 == obj: continue result = obj2.ray_cast(obj2.matrix_world.inverted() @ center, polygon.normal, distance=threshold) if not result[0]: @@ -662,3 +517,163 @@ class SnapSpacesTogether(bpy.types.Operator): for v in polygon.vertices: obj.data.vertices[v].co += offset return {"FINISHED"} + + +class SelectExternalMaterialDir(bpy.types.Operator): + bl_idname = "bim.select_external_material_dir" + bl_label = "Select Material File" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + context.active_object.active_material.BIMMaterialProperties.location = self.filepath + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class FetchExternalMaterial(bpy.types.Operator): + bl_idname = "bim.fetch_external_material" + bl_label = "Fetch External Material" + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + location = context.active_object.active_material.BIMMaterialProperties.location + if location[-6:] != ".mpass": + return {"FINISHED"} + if not os.path.isabs(location): + location = os.path.join(context.scene.BIMProperties.data_dir, location) + with open(location) as f: + self.material_pass = json.load(f) + if context.scene.render.engine == "BLENDER_EEVEE" and "eevee" in self.material_pass: + self.fetch_eevee_or_cycles("eevee", context) + elif context.scene.render.engine == "CYCLES" and "cycles" in self.material_pass: + self.fetch_eevee_or_cycles("cycles", context) + return {"FINISHED"} + + def fetch_eevee_or_cycles(self, name, context): + identification = context.active_object.active_material.BIMMaterialProperties.identification + uri = self.material_pass[name]["uri"] + if not os.path.isabs(uri): + uri = os.path.join(context.scene.BIMProperties.data_dir, uri) + bpy.ops.wm.link(filename=identification, directory=os.path.join(uri, "Material")) + for material in bpy.data.materials: + if material.name == identification and material.library: + context.active_object.material_slots[0].material = material + return + + +class FetchObjectPassport(bpy.types.Operator): + bl_idname = "bim.fetch_object_passport" + bl_label = "Fetch Object Passport" + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + for reference in context.active_object.BIMObjectProperties.document_references: + reference = context.scene.BIMProperties.document_references[reference.name] + if reference.location[-6:] == ".blend": + self.fetch_blender(reference, context) + return {"FINISHED"} + + def fetch_blender(self, reference, context): + bpy.ops.wm.link(filename=reference.name, directory=os.path.join(reference.location, "Mesh")) + context.active_object.data = bpy.data.meshes[reference.name] + + +class CopyPropertyToSelection(bpy.types.Operator): + bl_idname = "bim.copy_property_to_selection" + bl_label = "Copy Property To Selection" + pset_name: bpy.props.StringProperty() + prop_name: bpy.props.StringProperty() + prop_value: bpy.props.StringProperty() + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + for obj in context.selected_objects: + if "/" not in obj.name: + continue + pset = obj.BIMObjectProperties.psets.get(self.pset_name) + if not pset: + applicable_psets = schema.ifc.psetqto.get_applicable(obj.name.split("/")[0], pset_only=True) + for pset_template in applicable_psets: + if pset_template.Name == self.pset_name: + break + else: + continue + pset = obj.BIMObjectProperties.psets.add() + pset.name = self.pset_name + for template_prop_name in (p.Name for p in pset_template.HasPropertyTemplates): + prop = pset.properties.add() + prop.name = template_prop_name + prop = pset.properties.get(self.prop_name) + if prop: + prop.string_value = self.prop_value + return {"FINISHED"} + + +class CopyAttributeToSelection(bpy.types.Operator): + bl_idname = "bim.copy_attribute_to_selection" + bl_label = "Copy Attribute To Selection" + attribute_name: bpy.props.StringProperty() + attribute_value: bpy.props.StringProperty() + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(context.scene.BIMProperties.export_schema) + self.applicable_attributes_cache = {} + for obj in context.selected_objects: + if "/" not in obj.name: + continue + attribute = obj.BIMObjectProperties.attributes.get(self.attribute_name) + if not attribute: + applicable_attributes = self.get_applicable_attributes(obj.name.split("/")[0]) + if self.attribute_name not in applicable_attributes: + continue + attribute = obj.BIMObjectProperties.attributes.add() + attribute.name = self.attribute_name + attribute.string_value = self.attribute_value + return {"FINISHED"} + + def get_applicable_attributes(self, ifc_class): + if ifc_class not in self.applicable_attributes_cache: + self.applicable_attributes_cache[ifc_class] = [ + a.name() for a in self.schema.declaration_by_name(ifc_class).all_attributes() + ] + return self.applicable_attributes_cache[ifc_class] + + +class OverrideDelete(bpy.types.Operator): + bl_idname = "object.delete" + bl_label = "Delete" + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + if IfcStore.get_file(): + return IfcStore.execute_ifc_operator(self, context) + for obj in context.selected_objects: + bpy.data.objects.remove(obj) + return {"FINISHED"} + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def _execute(self, context): + for obj in context.selected_objects: + if obj.BIMObjectProperties.ifc_definition_id: + element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + if element.is_a("IfcOpeningElement"): + self.delete_opening_element(element) + elif element.HasOpenings: + for rel in element.HasOpenings: + self.delete_opening_element(rel.RelatedOpeningElement) + bpy.data.objects.remove(obj) + return {"FINISHED"} + + def delete_opening_element(self, element): + obj = IfcStore.get_element(element.VoidsElements[0].RelatingBuildingElement.id()) + bpy.ops.bim.remove_opening(opening_id=element.id(), obj=obj.name) diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index cb7e983b81..5c90d32e93 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -1,10 +1,28 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + import os import bpy import json import importlib import ifcopenshell import ifcopenshell.util.pset -from blenderbim.bim.handler import purge_module_data +import blenderbim.bim.handler from blenderbim.bim.ifc import IfcStore from bpy.types import PropertyGroup from bpy.props import ( @@ -46,11 +64,9 @@ def updateDataDir(self, context): blenderbim.bim.schema.ifc.data_dir = context.scene.BIMProperties.data_dir -def updateIfcFile(self, context): +def update_ifc_file(self, context): if context.scene.BIMProperties.ifc_file: - IfcStore.file = None - IfcStore.schema = None - purge_module_data() + blenderbim.bim.handler.loadIfcStore(context.scene) def getMaterialPsetNames(self, context): @@ -128,42 +144,74 @@ class StrProperty(PropertyGroup): pass -def updateAttributeStringValue(self, context): - updateAttributeValue(self, self.string_value) - - -def updateAttributeBoolValue(self, context): - updateAttributeValue(self, self.bool_value) - - -def updateAttributeIntValue(self, context): - updateAttributeValue(self, self.int_value) - - -def updateAttributeFloatValue(self, context): - updateAttributeValue(self, self.float_value) - - -def updateAttributeEnumValue(self, context): - updateAttributeValue(self, self.enum_value) - - -def updateAttributeValue(self, value): - if value: - self.is_null = False +def updateAttributeValue(self, context): + value_name = self.get_value_name() + if value_name: + value_names = [value_name] + else: + # We may not have a value name in