mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 14:31:39 +00:00
Merge branch 'v0.8.0' into ifcmax/initial-refresh
This commit is contained in:
@@ -82,6 +82,8 @@ src/bonsai/bonsai/translations.py
|
|||||||
|
|
||||||
# bonsai test temp files
|
# bonsai test temp files
|
||||||
src/bonsai/test/files/temp
|
src/bonsai/test/files/temp
|
||||||
|
src/bonsai/test/files/basic.ifc.cache.blend
|
||||||
|
src/bonsai/test/files/basic.ifc.cache.sqlite
|
||||||
|
|
||||||
# bonsai data
|
# bonsai data
|
||||||
src/bonsai/bonsai/bim/data/build/
|
src/bonsai/bonsai/bim/data/build/
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<!-- This file was generated with the assistance of an AI coding tool. -->
|
||||||
|
|
||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidelines for AI coding agents contributing to IfcOpenShell. This file is
|
||||||
|
intended to be read by all AI agents regardless of platform (Claude Code,
|
||||||
|
Copilot, Cursor, etc.) in addition to any tool-specific configuration files.
|
||||||
|
|
||||||
|
Human contributors using AI tools should also read this document carefully,
|
||||||
|
as they are responsible for ensuring their contributions comply with these
|
||||||
|
guidelines.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
IfcOpenShell is an open source library for working with Industry Foundation
|
||||||
|
Classes (IFC). It provides C++ and Python APIs, geometry processing, and an
|
||||||
|
ecosystem of tools including IfcConvert and the Bonsai Blender add-on.
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
All contributions must be compatible with the project's licensing:
|
||||||
|
|
||||||
|
- **Library code** (everything except Bonsai): **LGPL-3.0-or-later**
|
||||||
|
- **Bonsai** (`src/bonsai/`): **GPL-3.0-or-later**
|
||||||
|
|
||||||
|
There is no Contributor License Agreement (CLA). By submitting a pull request,
|
||||||
|
you agree that your contribution is licensed under the applicable license above.
|
||||||
|
|
||||||
|
## Indicating AI-Generated Code
|
||||||
|
|
||||||
|
Contributors must clearly indicate when code has been generated or
|
||||||
|
substantially written by an AI tool.
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
Commits that modify existing code must include a note in the **body** of the
|
||||||
|
commit message (not the subject line) indicating that the change was
|
||||||
|
AI-generated. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Fix off-by-one error in element iteration
|
||||||
|
|
||||||
|
The loop termination condition was incorrect when processing
|
||||||
|
IfcRelAggregates relationships.
|
||||||
|
|
||||||
|
Generated with the assistance of an AI coding tool.
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
|
||||||
|
New files that are AI-generated must include a comment near the top of the
|
||||||
|
file indicating this. Use the appropriate comment syntax for the language:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# This file was generated with the assistance of an AI coding tool.
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// This file was generated with the assistance of an AI coding tool.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
|
||||||
|
Pull requests containing AI-generated code must indicate in the PR description
|
||||||
|
which parts of the contribution are AI-generated. If the entire PR is
|
||||||
|
AI-generated, state that clearly. If only specific commits or files are
|
||||||
|
AI-generated, identify them.
|
||||||
|
|
||||||
|
## Pull Request Guidelines
|
||||||
|
|
||||||
|
### Scope and Size
|
||||||
|
|
||||||
|
- Each pull request should address a **single issue or feature**.
|
||||||
|
- Do not mix unrelated changes (e.g., bug fixes with refactoring or style
|
||||||
|
changes) in the same PR.
|
||||||
|
- Large pull requests should be broken down into **multiple small, standalone
|
||||||
|
commits** that are each easy to review independently. Rewrite commit history
|
||||||
|
for this purpose if necessary.
|
||||||
|
- PRs that are minimal, focused solutions to a specific problem are much more
|
||||||
|
likely to be accepted.
|
||||||
|
|
||||||
|
### What to Avoid
|
||||||
|
|
||||||
|
- **Over-engineering**: Do not add features, abstractions, or configurability
|
||||||
|
beyond what is needed to solve the immediate problem.
|
||||||
|
- **Scope creep**: Do not make changes to files or code that are not directly
|
||||||
|
related to the task at hand.
|
||||||
|
- **Unnecessary additions**: Do not add docstrings, comments, type annotations,
|
||||||
|
or error handling to code you did not otherwise need to change.
|
||||||
|
- **Cosmetic changes**: Do not reformat, rename, or reorganize code that is
|
||||||
|
unrelated to your change.
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
- The **subject line** must be **50 characters or less**.
|
||||||
|
- Use the **imperative mood** (e.g., "Fix crash in geometry kernel", not
|
||||||
|
"Fixed crash" or "Fixes crash").
|
||||||
|
- A commit message can be a single line if the purpose is obvious from the
|
||||||
|
subject alone.
|
||||||
|
- Otherwise, add a blank line after the subject followed by a short explanation
|
||||||
|
of a few lines in the body.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
- **Line length**: 120 characters
|
||||||
|
- **Formatter**: black
|
||||||
|
- **Linter**: ruff
|
||||||
|
- Configuration is in `pyproject.toml`
|
||||||
|
|
||||||
|
### C++
|
||||||
|
|
||||||
|
- **Standard**: C++17 minimum
|
||||||
|
- **Formatter**: clang-format (configuration in `.clang-format`)
|
||||||
|
- **Linter**: clang-tidy (configuration in `.clang-tidy`)
|
||||||
|
|
||||||
|
Run linters and formatters **before submitting** your pull request. Do not rely
|
||||||
|
on CI to catch formatting issues.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Pull requests with test coverage are **much more likely to be merged**.
|
||||||
|
- If tests are appropriate and feasible for your change, they should be
|
||||||
|
included.
|
||||||
|
- Tests are not required for every change (e.g., documentation-only changes),
|
||||||
|
but the expectation is that testable code changes come with tests.
|
||||||
|
- Python tests use **pytest** and are located in `test/` or `tests/` directories
|
||||||
|
within each package under `src/`.
|
||||||
|
- Run the existing test suite for the package you modified before submitting.
|
||||||
|
|
||||||
|
## Architecture Quick Reference
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
- `src/ifcparse/` — C++ IFC file parsing
|
||||||
|
- `src/ifcgeom/` — C++ geometry processing (OpenCASCADE and CGAL kernels)
|
||||||
|
- `src/serializers/` — Output format serializers (glTF, Collada, SVG, etc.)
|
||||||
|
- `src/ifcwrap/` — SWIG Python bindings
|
||||||
|
- `src/ifcconvert/` — CLI conversion tool
|
||||||
|
- `src/ifcopenshell-python/` — Python API (`ifcopenshell` package)
|
||||||
|
- `src/bonsai/` — Blender add-on (GPL-3.0-or-later)
|
||||||
|
- `src/ifctester/` — IDS model auditing
|
||||||
|
- `src/ifcpatch/` — IFC file manipulation scripts
|
||||||
|
- `src/ifcdiff/` — IFC model comparison
|
||||||
|
- `src/ifcclash/` — Clash detection
|
||||||
|
- `src/ifccsv/` — Schedule import/export
|
||||||
|
|
||||||
|
### IFC Schema Versions
|
||||||
|
|
||||||
|
The library supports IFC2x3 TC1, IFC4 Add2 TC1, IFC4x1, IFC4x2, and
|
||||||
|
IFC4x3 Add2. Schema-specific code is compiled conditionally. Be aware of
|
||||||
|
which schema versions your change affects.
|
||||||
+1
-1
@@ -3,7 +3,7 @@ name = "IfcOpenShell"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"black==26.1.0",
|
"black==26.1.0",
|
||||||
"ruff==0.15.0",
|
"ruff==0.15.1",
|
||||||
"poethepoet",
|
"poethepoet",
|
||||||
"gersemi==0.25.4",
|
"gersemi==0.25.4",
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -371,7 +371,7 @@ test-tool:
|
|||||||
ifndef MODULE
|
ifndef MODULE
|
||||||
pytest test/tool
|
pytest test/tool
|
||||||
else
|
else
|
||||||
pytest test/tool/test_$(MODULE).py
|
pytest test/tool/test_$(MODULE).py --maxfail=1
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Reregistering test is not added to the standard test suite because during unregister
|
# Reregistering test is not added to the standard test suite because during unregister
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class IfcExporter:
|
|||||||
self.set_header()
|
self.set_header()
|
||||||
IfcStore.update_cache()
|
IfcStore.update_cache()
|
||||||
self.sync_all_objects()
|
self.sync_all_objects()
|
||||||
tool.Project.save_linked_models_to_ifc()
|
|
||||||
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
|
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
|
||||||
if extension == "ifczip":
|
if extension == "ifczip":
|
||||||
with tempfile.TemporaryDirectory() as unzipped_path:
|
with tempfile.TemporaryDirectory() as unzipped_path:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import bpy
|
|||||||
from bpy.types import Panel, UIList
|
from bpy.types import Panel, UIList
|
||||||
|
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
|
import bsdd
|
||||||
from bonsai.bim.module.bsdd.data import BSDDData
|
from bonsai.bim.module.bsdd.data import BSDDData
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -83,7 +84,6 @@ class BIM_PT_bsdd(Panel):
|
|||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.operator("bim.load_bsdd_dictionaries")
|
row.operator("bim.load_bsdd_dictionaries")
|
||||||
|
|
||||||
|
|
||||||
class BIM_UL_bsdd_dictionaries(UIList):
|
class BIM_UL_bsdd_dictionaries(UIList):
|
||||||
def draw_item(
|
def draw_item(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -928,7 +928,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
for link in props.get_loaded_links_for_drawings():
|
for link in props.get_loaded_links_for_drawings():
|
||||||
files[link.name] = self.get_linked_file(link)
|
files[link.filepath] = self.get_linked_file(link)
|
||||||
|
|
||||||
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
|
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
|
||||||
self.setup_serialiser(target_view)
|
self.setup_serialiser(target_view)
|
||||||
@@ -1374,7 +1374,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_linked_file(self, link: "Link") -> ifcopenshell.file:
|
def get_linked_file(self, link: "Link") -> ifcopenshell.file:
|
||||||
link_path = link.name
|
link_path = link.filepath
|
||||||
ifc_file = IfcStore.session_files.get(link_path, None)
|
ifc_file = IfcStore.session_files.get(link_path, None)
|
||||||
if ifc_file is not None:
|
if ifc_file is not None:
|
||||||
return ifc_file
|
return ifc_file
|
||||||
@@ -2896,12 +2896,16 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Add an .ods, .xls or .xlsx file as a schedule"
|
bl_description = "Add an .ods, .xls or .xlsx file as a schedule"
|
||||||
|
|
||||||
|
files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement)
|
||||||
|
directory: bpy.props.StringProperty(subtype="DIR_PATH")
|
||||||
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
|
filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"})
|
||||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
|
for filepath in tool.Blender.get_selected_files(
|
||||||
core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath)
|
self.directory, self.files, use_relative_path=self.use_relative_path
|
||||||
|
):
|
||||||
|
core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath)
|
||||||
|
|
||||||
|
|
||||||
class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
@@ -3090,23 +3094,16 @@ class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
|||||||
bl_description = "Import a .svg file to the project as a reference"
|
bl_description = "Import a .svg file to the project as a reference"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement)
|
||||||
|
directory: bpy.props.StringProperty(subtype="DIR_PATH")
|
||||||
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
|
filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"})
|
||||||
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
|
||||||
filename_ext = ".svg"
|
filename_ext = ".svg"
|
||||||
|
|
||||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)
|
|
||||||
directory: bpy.props.StringProperty(subtype="DIR_PATH")
|
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
# Handle both single and multiple file selection
|
for filepath in tool.Blender.get_selected_files(
|
||||||
if self.files:
|
self.directory, self.files, use_relative_path=self.use_relative_path
|
||||||
for file_elem in self.files:
|
):
|
||||||
filepath = os.path.join(self.directory, file_elem.name)
|
|
||||||
uri = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path)
|
|
||||||
core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=uri)
|
|
||||||
else:
|
|
||||||
# Fallback for single file (backward compatibility)
|
|
||||||
filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)
|
|
||||||
core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath)
|
core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath)
|
||||||
|
|
||||||
|
|
||||||
@@ -3165,6 +3162,7 @@ class EditTextPopup(bpy.types.Operator):
|
|||||||
bpy.ops.bim.disable_editing_text()
|
bpy.ops.bim.disable_editing_text()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
# TODO: check for possible subtle undo bug here
|
||||||
# can't use invoke() because this operator
|
# can't use invoke() because this operator
|
||||||
# will be run indirectly by hotkey
|
# will be run indirectly by hotkey
|
||||||
# so we use execute() and track whether it's the first run of the operator
|
# so we use execute() and track whether it's the first run of the operator
|
||||||
@@ -3817,91 +3815,14 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
|||||||
description="Existing object name to add a style with reference image to. If not provided will create a new object.",
|
description="Existing object name to add a style with reference image to. If not provided will create a new object.",
|
||||||
options={"SKIP_SAVE"},
|
options={"SKIP_SAVE"},
|
||||||
)
|
)
|
||||||
|
size: bpy.props.FloatProperty(name="Size", description="Size of the reference image", default=1.0, unit="LENGTH")
|
||||||
x_length: bpy.props.FloatProperty(
|
|
||||||
name="X Length",
|
|
||||||
description="Width of the reference image in project units",
|
|
||||||
default=1.0,
|
|
||||||
min=0.001,
|
|
||||||
soft_min=0.01,
|
|
||||||
precision=3,
|
|
||||||
)
|
|
||||||
y_length: bpy.props.FloatProperty(
|
|
||||||
name="Y Length",
|
|
||||||
description="Height of the reference image in project units",
|
|
||||||
default=1.0,
|
|
||||||
min=0.001,
|
|
||||||
soft_min=0.01,
|
|
||||||
precision=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
show_dimensions_dialog: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
if Path(tool.Ifc.get_path()).is_file():
|
||||||
|
self.layout.prop(self, "use_relative_path")
|
||||||
if getattr(self, "show_dimensions_dialog", False):
|
self.layout.prop(self, "override_existing_image")
|
||||||
if tool.Ifc.get():
|
self.layout.prop(self, "use_existing_object_by_name")
|
||||||
length_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT")
|
self.layout.prop(self, "size")
|
||||||
if length_unit:
|
|
||||||
unit_name = ifcopenshell.util.unit.get_full_unit_name(length_unit).lower()
|
|
||||||
else:
|
|
||||||
unit_name = "project units"
|
|
||||||
layout.label(text=f"Set Reference Image Dimensions (in {unit_name}):")
|
|
||||||
else:
|
|
||||||
layout.label(text="Set Reference Image Dimensions (in project units):")
|
|
||||||
layout.separator()
|
|
||||||
layout.prop(self, "x_length")
|
|
||||||
layout.prop(self, "y_length")
|
|
||||||
else:
|
|
||||||
if Path(tool.Ifc.get_path()).is_file():
|
|
||||||
layout.prop(self, "use_relative_path")
|
|
||||||
else:
|
|
||||||
self.use_relative_path = False
|
|
||||||
layout.label(text="Save the .ifc file first ")
|
|
||||||
layout.label(text="to use relative paths.")
|
|
||||||
layout.prop(self, "override_existing_image")
|
|
||||||
layout.prop(self, "use_existing_object_by_name")
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
if not getattr(self, "show_dimensions_dialog", False):
|
|
||||||
context.window_manager.fileselect_add(self)
|
|
||||||
return {"RUNNING_MODAL"}
|
|
||||||
else:
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
if not getattr(self, "show_dimensions_dialog", False):
|
|
||||||
abs_path = Path(self.filepath).absolute().resolve()
|
|
||||||
if self.override_existing_image:
|
|
||||||
params = {"check_existing": True, "force_reload": True}
|
|
||||||
else:
|
|
||||||
params = {"check_existing": False}
|
|
||||||
|
|
||||||
try:
|
|
||||||
image = load_image(abs_path.name, str(abs_path.parent), **params)
|
|
||||||
|
|
||||||
image_width_px = image.size[0]
|
|
||||||
image_height_px = image.size[1]
|
|
||||||
aspect_ratio = image_width_px / image_height_px
|
|
||||||
|
|
||||||
if aspect_ratio >= 1.0:
|
|
||||||
self.x_length = 1.0
|
|
||||||
self.y_length = 1.0 / aspect_ratio
|
|
||||||
else:
|
|
||||||
self.x_length = aspect_ratio
|
|
||||||
self.y_length = 1.0
|
|
||||||
|
|
||||||
bpy.data.images.remove(image)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.report({"ERROR"}, f"Failed to load image: {str(e)}")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
self.show_dimensions_dialog = True
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
|
||||||
|
|
||||||
return self._execute(context)
|
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
space = tool.Blender.get_view3d_space()
|
space = tool.Blender.get_view3d_space()
|
||||||
@@ -3922,11 +3843,19 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
|||||||
params = {"check_existing": False}
|
params = {"check_existing": False}
|
||||||
image = load_image(abs_path.name, str(abs_path.parent), **params)
|
image = load_image(abs_path.name, str(abs_path.parent), **params)
|
||||||
|
|
||||||
|
aspect_ratio = image.size[0] / image.size[1]
|
||||||
|
if aspect_ratio >= 1.0: # Landscape
|
||||||
|
x_length = self.size
|
||||||
|
y_length = self.size / aspect_ratio
|
||||||
|
else:
|
||||||
|
x_length = self.size / aspect_ratio
|
||||||
|
y_length = self.size
|
||||||
|
|
||||||
def bm_add_image_plane(mesh):
|
def bm_add_image_plane(mesh):
|
||||||
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True)
|
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True)
|
||||||
|
|
||||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
||||||
plane_scale = Vector((self.x_length * unit_scale / 2.0, self.y_length * unit_scale / 2.0, 1.0))
|
plane_scale = Vector((x_length / 2.0, y_length / 2.0, 1.0))
|
||||||
matrix = Matrix.LocRotScale(None, None, plane_scale)
|
matrix = Matrix.LocRotScale(None, None, plane_scale)
|
||||||
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False)
|
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False)
|
||||||
|
|
||||||
@@ -4030,8 +3959,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
|
|||||||
tool.Style.reload_material_from_ifc(material)
|
tool.Style.reload_material_from_ifc(material)
|
||||||
tool.Geometry.record_object_materials(obj)
|
tool.Geometry.record_object_materials(obj)
|
||||||
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
|
|
||||||
class ConvertSVGToDXF(bpy.types.Operator):
|
class ConvertSVGToDXF(bpy.types.Operator):
|
||||||
bl_idname = "bim.convert_svg_to_dxf"
|
bl_idname = "bim.convert_svg_to_dxf"
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class BIM_PT_camera(Panel):
|
|||||||
for link in links:
|
for link in links:
|
||||||
row = panel.row(align=True)
|
row = panel.row(align=True)
|
||||||
split = row.split(factor=0.9)
|
split = row.split(factor=0.9)
|
||||||
split.label(text=link.name, icon="FILE")
|
split.label(text=link.filepath, icon="FILE")
|
||||||
split.prop(link, "include_in_drawings", text="")
|
split.prop(link, "include_in_drawings", text="")
|
||||||
else:
|
else:
|
||||||
panel.label(text="No IFC projects linked and loaded.")
|
panel.label(text="No IFC projects linked and loaded.")
|
||||||
|
|||||||
@@ -902,6 +902,7 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
|
|
||||||
if not is_valid_data_block:
|
if not is_valid_data_block:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if element:
|
if element:
|
||||||
if tool.Geometry.is_locked(element):
|
if tool.Geometry.is_locked(element):
|
||||||
@@ -912,6 +913,9 @@ class OverrideDelete(bpy.types.Operator):
|
|||||||
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
|
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
|
||||||
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
|
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
|
||||||
continue
|
continue
|
||||||
|
if element.is_a("IfcDocumentReference"):
|
||||||
|
self.report({"INFO"}, "Linked models cannot be deleted.")
|
||||||
|
continue
|
||||||
if element.is_a("IfcGridAxis"):
|
if element.is_a("IfcGridAxis"):
|
||||||
# Deleting the last W axis is OK
|
# Deleting the last W axis is OK
|
||||||
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
||||||
@@ -1208,6 +1212,11 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
|||||||
operator.report({"ERROR"}, f"Drawing '{obj.name}' not duplicated.")
|
operator.report({"ERROR"}, f"Drawing '{obj.name}' not duplicated.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if element.is_a("IfcDocumentReference"):
|
||||||
|
objects_to_remove.add(obj)
|
||||||
|
operator.report({"ERROR"}, f"Linked model '{obj.name}' not duplicated.")
|
||||||
|
continue
|
||||||
|
|
||||||
if tool.Geometry.is_locked(element):
|
if tool.Geometry.is_locked(element):
|
||||||
objects_to_remove.add(obj)
|
objects_to_remove.add(obj)
|
||||||
operator.report({"ERROR"}, lock_error_message(obj.name))
|
operator.report({"ERROR"}, lock_error_message(obj.name))
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_description = "Remove the georeferencing"
|
bl_description = "Remove the georeferencing"
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
core.remove_georeferencing(tool.Ifc)
|
core.remove_georeferencing(tool.Ifc, tool.Georeference)
|
||||||
|
|
||||||
|
|
||||||
class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
|
class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
|||||||
@@ -224,16 +224,12 @@ class BIMGeoreferenceProperties(PropertyGroup):
|
|||||||
x_axis_ordinate: StringProperty(name="X Axis Ordinate", update=update_grid_north_vector)
|
x_axis_ordinate: StringProperty(name="X Axis Ordinate", update=update_grid_north_vector)
|
||||||
x_axis_is_null: BoolProperty(name="X Axis Is Null")
|
x_axis_is_null: BoolProperty(name="X Axis Is Null")
|
||||||
|
|
||||||
# These are only for reference to capture data about a host model from a linked model
|
|
||||||
# If you relink a model from a new host origin, we can autodetect it in theory with this
|
|
||||||
host_model_origin: StringProperty(name="Host Model Origin")
|
|
||||||
host_model_origin_si: StringProperty(name="Host Model Origin SI")
|
|
||||||
host_model_project_north: StringProperty(name="Host Model Angle to Grid North")
|
|
||||||
|
|
||||||
# This is the ENH in project units and SI units of the Blender session's 0,0,0.
|
# This is the ENH in project units and SI units of the Blender session's 0,0,0.
|
||||||
# These are only for reference, using tool.Georeference.set_model_origin on
|
# These are only for reference, using tool.Georeference.set_model_origin on
|
||||||
# project load, project create, and when linking for the first time from an
|
# project load, project create, and when linking for the first time from an
|
||||||
# empty Blender session.
|
# empty Blender session.
|
||||||
|
model_is_georeferenced: BoolProperty(name="Model Is Georeferenced")
|
||||||
|
model_crs: StringProperty(name="Model CRS")
|
||||||
model_origin: StringProperty(name="Model Origin")
|
model_origin: StringProperty(name="Model Origin")
|
||||||
model_origin_si: StringProperty(name="Model Origin SI")
|
model_origin_si: StringProperty(name="Model Origin SI")
|
||||||
model_project_north: StringProperty(name="Model Angle to Grid North")
|
model_project_north: StringProperty(name="Model Angle to Grid North")
|
||||||
@@ -275,10 +271,6 @@ class BIMGeoreferenceProperties(PropertyGroup):
|
|||||||
x_axis_ordinate: str
|
x_axis_ordinate: str
|
||||||
x_axis_is_null: bool
|
x_axis_is_null: bool
|
||||||
|
|
||||||
host_model_origin: str
|
|
||||||
host_model_origin_si: str
|
|
||||||
host_model_project_north: str
|
|
||||||
|
|
||||||
model_origin: str
|
model_origin: str
|
||||||
model_origin_si: str
|
model_origin_si: str
|
||||||
model_project_north: str
|
model_project_north: str
|
||||||
|
|||||||
@@ -31,27 +31,31 @@ classes = (
|
|||||||
operator.BIM_OT_load_clipping_planes,
|
operator.BIM_OT_load_clipping_planes,
|
||||||
operator.BIM_OT_save_clipping_planes,
|
operator.BIM_OT_save_clipping_planes,
|
||||||
operator.ChangeLibraryElement,
|
operator.ChangeLibraryElement,
|
||||||
|
operator.ClearMeasurement,
|
||||||
operator.ClearRecentIFCProjects,
|
operator.ClearRecentIFCProjects,
|
||||||
operator.CreateClippingPlane,
|
operator.CreateClippingPlane,
|
||||||
operator.CreateProject,
|
operator.CreateProject,
|
||||||
operator.DisableCulling,
|
operator.DisableCulling,
|
||||||
operator.DisableEditingHeader,
|
operator.DisableEditingHeader,
|
||||||
|
operator.DisableEditingLink,
|
||||||
operator.EditHeader,
|
operator.EditHeader,
|
||||||
|
operator.EditLink,
|
||||||
operator.EditProjectLibrary,
|
operator.EditProjectLibrary,
|
||||||
operator.EnableCulling,
|
operator.EnableCulling,
|
||||||
operator.EnableEditingHeader,
|
operator.EnableEditingHeader,
|
||||||
|
operator.EnableEditingLink,
|
||||||
operator.ExportIFC,
|
operator.ExportIFC,
|
||||||
operator.FlipClippingPlane,
|
operator.FlipClippingPlane,
|
||||||
operator.IFCFileHandlerOperator,
|
operator.IFCFileHandlerOperator,
|
||||||
operator.ImageScalingTool,
|
operator.ImageScalingTool,
|
||||||
operator.LinkIfc,
|
operator.LinkIfc,
|
||||||
|
operator.LoadBlendMetadataAndIFC,
|
||||||
operator.LoadLink,
|
operator.LoadLink,
|
||||||
operator.LoadLinkedProject,
|
operator.LoadLinkedProject,
|
||||||
operator.LoadProject,
|
operator.LoadProject,
|
||||||
operator.LoadProjectElements,
|
operator.LoadProjectElements,
|
||||||
operator.MeasureTool,
|
|
||||||
operator.MeasureFaceAreaTool,
|
operator.MeasureFaceAreaTool,
|
||||||
operator.ClearMeasurement,
|
operator.MeasureTool,
|
||||||
operator.NewProject,
|
operator.NewProject,
|
||||||
operator.QueryLinkedElement,
|
operator.QueryLinkedElement,
|
||||||
operator.RefreshClippingPlanes,
|
operator.RefreshClippingPlanes,
|
||||||
@@ -69,7 +73,6 @@ classes = (
|
|||||||
operator.UnassignLibraryDeclaration,
|
operator.UnassignLibraryDeclaration,
|
||||||
operator.UnlinkIfc,
|
operator.UnlinkIfc,
|
||||||
operator.UnloadLink,
|
operator.UnloadLink,
|
||||||
operator.LoadBlendMetadataAndIFC,
|
|
||||||
workspace.ExploreHotkey,
|
workspace.ExploreHotkey,
|
||||||
prop.LibraryBreadcrumb,
|
prop.LibraryBreadcrumb,
|
||||||
prop.LibraryElement,
|
prop.LibraryElement,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -59,6 +60,15 @@ import bonsai.core.project as core
|
|||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
from bonsai.bim import export_ifc, import_ifc
|
from bonsai.bim import export_ifc, import_ifc
|
||||||
from bonsai.bim.ifc import IfcStore
|
from bonsai.bim.ifc import IfcStore
|
||||||
|
from bonsai.bim.ui import IFCFileSelector
|
||||||
|
from bonsai.bim import import_ifc
|
||||||
|
from bonsai.bim import export_ifc
|
||||||
|
from math import radians, degrees
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import defaultdict
|
||||||
|
from mathutils import Vector, Matrix
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
from ifcopenshell.geom import ShapeElementType
|
||||||
from bonsai.bim.module.model.decorator import FaceAreaDecorator, PolylineDecorator
|
from bonsai.bim.module.model.decorator import FaceAreaDecorator, PolylineDecorator
|
||||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||||
from bonsai.bim.module.project.data import LinksData, ProjectLibraryData
|
from bonsai.bim.module.project.data import LinksData, ProjectLibraryData
|
||||||
@@ -1321,7 +1331,7 @@ class ToggleFilterCategories(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class LinkIfc(bpy.types.Operator, ImportHelper):
|
class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.link_ifc"
|
bl_idname = "bim.link_ifc"
|
||||||
bl_label = "Link IFC"
|
bl_label = "Link IFC"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
@@ -1360,134 +1370,125 @@ class LinkIfc(bpy.types.Operator, ImportHelper):
|
|||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(pprops, "project_north")
|
row.prop(pprops, "project_north")
|
||||||
|
|
||||||
def execute(self, context):
|
def _execute(self, context):
|
||||||
start = time.time()
|
start = time.time()
|
||||||
files = [f.name for f in self.files] if self.files else [self.filepath]
|
files = [f.name for f in self.files] if self.files else [self.filepath]
|
||||||
|
|
||||||
|
if not files or all(not f or not f.strip() for f in files):
|
||||||
|
self.report({"ERROR"}, "No file selected")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
existing_links = tool.Project.get_linked_models_documents() if tool.Ifc.get() else {}
|
||||||
for filename in files:
|
for filename in files:
|
||||||
|
if not filename or not filename.strip():
|
||||||
|
continue
|
||||||
filepath = Path(self.directory) / filename
|
filepath = Path(self.directory) / filename
|
||||||
if bpy.data.filepath and filepath.samefile(bpy.data.filepath):
|
if bpy.data.filepath and filepath.samefile(bpy.data.filepath):
|
||||||
self.report({"INFO"}, "Can't link the current .blend file")
|
self.report({"INFO"}, "Can't link the current .blend file")
|
||||||
continue
|
continue
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
new = props.links.add()
|
|
||||||
filepath = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path)
|
filepath = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path)
|
||||||
|
|
||||||
|
new = props.links.add()
|
||||||
|
if tool.Ifc.get():
|
||||||
|
if not (document := existing_links.get(filepath)):
|
||||||
|
document = ifcopenshell.api.document.add_information(tool.Ifc.get())
|
||||||
|
document.Name = Path(filepath).name
|
||||||
|
document.Scope = "LINKED_MODEL"
|
||||||
|
reference = ifcopenshell.api.document.add_reference(tool.Ifc.get(), information=document)
|
||||||
|
reference[1] = ",".join([str(o) for o in np.eye(4).flatten().tolist()])
|
||||||
|
reference.Location = filepath.replace("\\", "/")
|
||||||
|
new.ifc_definition_id = reference.id()
|
||||||
new.name = filepath
|
new.name = filepath
|
||||||
status = bpy.ops.bim.load_link(filepath=filepath, use_cache=self.use_cache)
|
new.filepath = filepath
|
||||||
if status == {"CANCELLED"}:
|
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache)
|
||||||
error_msg = (
|
|
||||||
f'Error processing IFC file "{filepath}" '
|
|
||||||
"was critical and blend file either wasn't saved or wasn't updated. "
|
|
||||||
"See logs above in system console for details."
|
|
||||||
)
|
|
||||||
print(error_msg)
|
|
||||||
self.report({"ERROR"}, error_msg)
|
|
||||||
return {"FINISHED"}
|
|
||||||
print(f"Finished linking {len(files)} IFCs", time.time() - start)
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
|
|
||||||
class UnlinkIfc(bpy.types.Operator):
|
class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.unlink_ifc"
|
bl_idname = "bim.unlink_ifc"
|
||||||
bl_label = "Unlink IFC"
|
bl_label = "Unlink IFC"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Remove the selected file from the link list"
|
bl_description = "Remove the selected file from the link list"
|
||||||
filepath: bpy.props.StringProperty()
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
def execute(self, context):
|
def _execute(self, context):
|
||||||
filepath = Path(self.filepath).as_posix()
|
|
||||||
bpy.ops.bim.unload_link(filepath=filepath)
|
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
index = props.links.find(filepath)
|
link = props.links[self.link_index]
|
||||||
if index != -1:
|
bpy.ops.bim.unload_link(link_index=self.link_index)
|
||||||
props.links.remove(index)
|
if tool.Ifc.get():
|
||||||
return {"FINISHED"}
|
reference = tool.Ifc.get().by_id(link.ifc_definition_id)
|
||||||
|
document = tool.Document.get_reference_document(reference)
|
||||||
|
ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference)
|
||||||
|
if document and not tool.Document.get_document_references(document):
|
||||||
|
ifcopenshell.api.document.remove_information(tool.Ifc.get(), document)
|
||||||
|
props.links.remove(self.link_index)
|
||||||
|
|
||||||
|
|
||||||
class UnloadLink(bpy.types.Operator):
|
class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.unload_link"
|
bl_idname = "bim.unload_link"
|
||||||
bl_label = "Unload Link"
|
bl_label = "Unload Link"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Unload the selected linked file"
|
bl_description = "Unload the selected linked file"
|
||||||
filepath: bpy.props.StringProperty()
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
def execute(self, context):
|
def _execute(self, context):
|
||||||
filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.filepath))
|
link = tool.Project.get_project_props().links[self.link_index]
|
||||||
if filepath.suffix.lower() == ".ifc":
|
if obj := tool.Project.get_link_empty_handle(link):
|
||||||
filepath = filepath.with_suffix(".ifc.cache.blend")
|
collection = obj.instance_collection
|
||||||
|
library = collection.library
|
||||||
for library in list(bpy.data.libraries):
|
tool.Ifc.unlink(obj=obj)
|
||||||
if tool.Blender.ensure_blender_path_is_abs(Path(library.filepath)) == filepath:
|
bpy.data.objects.remove(obj)
|
||||||
|
if collection.users == 0:
|
||||||
|
bpy.data.collections.remove(collection)
|
||||||
|
if not len([c for c in bpy.data.collections if c.library == library]):
|
||||||
bpy.data.libraries.remove(library)
|
bpy.data.libraries.remove(library)
|
||||||
|
|
||||||
props = tool.Project.get_project_props()
|
|
||||||
links = props.links
|
|
||||||
link = links[self.filepath]
|
|
||||||
# Let's assume that user might delete it.
|
|
||||||
if empty_handle := link.empty_handle:
|
|
||||||
bpy.data.objects.remove(empty_handle)
|
|
||||||
|
|
||||||
# following lines removes the library also when use_relative_path=True, otherwise it doesn't
|
|
||||||
libraries = bpy.data.libraries
|
|
||||||
for library in libraries:
|
|
||||||
if library.name == self.filepath + ".cache.blend":
|
|
||||||
bpy.data.libraries.remove(library)
|
|
||||||
|
|
||||||
link.is_loaded = False
|
link.is_loaded = False
|
||||||
|
ProjectDecorator.uninstall()
|
||||||
if not any([l.is_loaded for l in links]):
|
|
||||||
ProjectDecorator.uninstall()
|
|
||||||
# we make sure we don't draw queried object from the file that was just unlinked
|
|
||||||
elif queried_obj := props.queried_obj:
|
|
||||||
queried_filepath = Path(queried_obj["ifc_filepath"])
|
|
||||||
if queried_filepath == filepath:
|
|
||||||
ProjectDecorator.uninstall()
|
|
||||||
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
|
|
||||||
class LoadLink(bpy.types.Operator):
|
class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.load_link"
|
bl_idname = "bim.load_link"
|
||||||
bl_label = "Load Link"
|
bl_label = "Load Link"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Load the selected file"
|
bl_description = "Load the selected file"
|
||||||
filepath: bpy.props.StringProperty(name="Link Filepath")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
|
||||||
|
|
||||||
filepath_: Path
|
def _execute(self, context):
|
||||||
|
self.link = tool.Project.get_project_props().links[self.link_index]
|
||||||
def execute(self, context):
|
filepath = Path(tool.Ifc.resolve_uri(self.link.filepath))
|
||||||
filepath = Path(tool.Ifc.resolve_uri(self.filepath))
|
|
||||||
if not filepath.exists():
|
if not filepath.exists():
|
||||||
self.report({"ERROR"}, f"File does not exist: '{filepath}'")
|
self.report({"ERROR"}, f"File does not exist: '{filepath}'")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
self.filepath_ = filepath
|
self.filepath_ = filepath
|
||||||
if filepath.suffix.lower().endswith(".blend"):
|
if filepath.suffix.lower().endswith(".ifc"):
|
||||||
self.link_blend(filepath)
|
return self.link_ifc()
|
||||||
elif filepath.suffix.lower().endswith(".ifc"):
|
|
||||||
status = self.link_ifc()
|
|
||||||
if status:
|
|
||||||
return status
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
def link_blend(self, filepath: Path) -> None:
|
def link_blend(self, filepath: Path) -> None:
|
||||||
with bpy.data.libraries.load(str(filepath), link=True) as (data_from, data_to):
|
with bpy.data.libraries.load(str(filepath), link=True) as (data_from, data_to):
|
||||||
data_to.scenes = data_from.scenes
|
data_to.collections = [c for c in data_from.collections if "IfcProject" in c]
|
||||||
link = tool.Project.get_project_props().links[self.filepath]
|
|
||||||
for scene in bpy.data.scenes:
|
# Find the linked collection
|
||||||
if not scene.library or Path(scene.library.filepath) != filepath:
|
for collection in bpy.data.collections:
|
||||||
|
if not collection.library or Path(collection.library.filepath) != filepath:
|
||||||
continue
|
continue
|
||||||
for child in scene.collection.children:
|
# Create unique empty instance for this link
|
||||||
if "IfcProject" not in child.name:
|
empty_name = collection.name
|
||||||
continue
|
empty = bpy.data.objects.new(empty_name, None)
|
||||||
empty = bpy.data.objects.new(child.name, None)
|
empty.instance_type = "COLLECTION"
|
||||||
empty.instance_type = "COLLECTION"
|
empty.instance_collection = collection
|
||||||
empty.instance_collection = child
|
empty.matrix_world = Matrix(tool.Project.calculate_link_matrix(self.link))
|
||||||
link.empty_handle = empty
|
|
||||||
bpy.context.scene.collection.objects.link(empty)
|
tool.Project.set_link_empty_handle(self.link, empty)
|
||||||
break
|
bpy.context.scene.collection.objects.link(empty)
|
||||||
|
self.link.is_loaded = True
|
||||||
|
if tool.Ifc.get(): # For non-IFC projects, locking has no meaning
|
||||||
|
tool.Geometry.lock_object(empty)
|
||||||
|
tool.Blender.select_and_activate_single_object(bpy.context, empty)
|
||||||
break
|
break
|
||||||
link.is_loaded = True
|
else:
|
||||||
tool.Blender.select_and_activate_single_object(bpy.context, empty)
|
print(f"WARNING: No IfcProject collection found in {filepath}")
|
||||||
|
self.link.is_loaded = False
|
||||||
|
|
||||||
def link_ifc(self) -> Union[set[str], None]:
|
def link_ifc(self) -> Union[set[str], None]:
|
||||||
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
|
blend_filepath = self.filepath_.with_suffix(".ifc.cache.blend")
|
||||||
@@ -1502,14 +1503,12 @@ class LoadLink(bpy.types.Operator):
|
|||||||
|
|
||||||
code = f"""
|
code = f"""
|
||||||
import bpy
|
import bpy
|
||||||
|
import sys
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
gprops = tool.Georeference.get_georeference_props()
|
gprops = tool.Georeference.get_georeference_props()
|
||||||
# Our model origin becomes their host model origin
|
# Our model origin becomes their host model origin
|
||||||
gprops.host_model_origin = "{gprops.model_origin}"
|
|
||||||
gprops.host_model_origin_si = "{gprops.model_origin_si}"
|
|
||||||
gprops.host_model_project_north = "{gprops.model_project_north}"
|
|
||||||
gprops.has_blender_offset = {gprops.has_blender_offset}
|
gprops.has_blender_offset = {gprops.has_blender_offset}
|
||||||
gprops.blender_offset_x = "{gprops.blender_offset_x}"
|
gprops.blender_offset_x = "{gprops.blender_offset_x}"
|
||||||
gprops.blender_offset_y = "{gprops.blender_offset_y}"
|
gprops.blender_offset_y = "{gprops.blender_offset_y}"
|
||||||
@@ -1522,7 +1521,12 @@ def run():
|
|||||||
pprops.false_origin = "{pprops.false_origin}"
|
pprops.false_origin = "{pprops.false_origin}"
|
||||||
pprops.project_north = "{pprops.project_north}"
|
pprops.project_north = "{pprops.project_north}"
|
||||||
# Use absolute path to be safe from cwd changes.
|
# Use absolute path to be safe from cwd changes.
|
||||||
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}")
|
try:
|
||||||
|
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}")
|
||||||
|
except RuntimeError as e:
|
||||||
|
# Operator failed (returned CANCELLED with error report)
|
||||||
|
print(f"Failed to load linked project: {{e}}")
|
||||||
|
sys.exit(1)
|
||||||
# Use str instead of as_posix to avoid issues with Windows shared paths.
|
# Use str instead of as_posix to avoid issues with Windows shared paths.
|
||||||
bpy.ops.wm.save_as_mainfile(filepath=r"{str(blend_filepath)}")
|
bpy.ops.wm.save_as_mainfile(filepath=r"{str(blend_filepath)}")
|
||||||
|
|
||||||
@@ -1556,14 +1560,17 @@ except Exception as e:
|
|||||||
if not blend_filepath.exists() or blend_filepath.stat().st_mtime < t:
|
if not blend_filepath.exists() or blend_filepath.stat().st_mtime < t:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
self.set_model_origin_from_link()
|
self.set_model_origin_from_link()
|
||||||
|
self.set_georeferencing_indicator()
|
||||||
self.link_blend(blend_filepath)
|
self.link_blend(blend_filepath)
|
||||||
|
|
||||||
def set_model_origin_from_link(self) -> None:
|
def set_model_origin_from_link(self) -> None:
|
||||||
if tool.Ifc.get():
|
if tool.Ifc.get():
|
||||||
return # The current model's coordinates always take priority.
|
return # The current model's coordinates always take priority.
|
||||||
|
|
||||||
|
if len(tool.Project.get_project_props().links) > 1:
|
||||||
|
return # Only the first link sets the origin
|
||||||
|
|
||||||
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
|
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
|
||||||
if not json_filepath.exists():
|
if not json_filepath.exists():
|
||||||
return
|
return
|
||||||
@@ -1576,21 +1583,36 @@ except Exception as e:
|
|||||||
if (value := data.get(prop, None)) is not None:
|
if (value := data.get(prop, None)) is not None:
|
||||||
setattr(gprops, prop, value)
|
setattr(gprops, prop, value)
|
||||||
|
|
||||||
|
def set_georeferencing_indicator(self) -> None:
|
||||||
|
if not tool.Ifc.get():
|
||||||
|
self.link.georeferenced = "NONE"
|
||||||
|
return
|
||||||
|
if not (crs_name := (ifcopenshell.util.geolocation.get_crs(tool.Ifc.get()) or {}).get("Name", "")):
|
||||||
|
self.link.georeferenced = "NONE"
|
||||||
|
return
|
||||||
|
reference = tool.Ifc.get().by_id(self.link.ifc_definition_id)
|
||||||
|
json_filepath = Path(reference.Location).with_suffix(".ifc.cache.json")
|
||||||
|
if not json_filepath.exists():
|
||||||
|
self.link.georeferenced = "NONE"
|
||||||
|
return
|
||||||
|
with open(json_filepath, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not data["model_is_georeferenced"]:
|
||||||
|
self.link.georeferenced = "NONE"
|
||||||
|
else:
|
||||||
|
self.link.georeferenced = "FULL_COMPATIBLE" if crs_name == data["model_crs"] else "NOT_COMPATIBLE"
|
||||||
|
|
||||||
|
|
||||||
class ReloadLink(bpy.types.Operator):
|
class ReloadLink(bpy.types.Operator):
|
||||||
bl_idname = "bim.reload_link"
|
bl_idname = "bim.reload_link"
|
||||||
bl_label = "Reload Link"
|
bl_label = "Reload Link"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Reload the selected file"
|
bl_description = "Reload the selected file"
|
||||||
filepath: bpy.props.StringProperty()
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
is_abs = os.path.isabs(Path(self.filepath))
|
bpy.ops.bim.unload_link(link_index=self.link_index)
|
||||||
use_relative_path = not is_abs
|
return bpy.ops.bim.load_link(link_index=self.link_index, use_cache=False) or {"FINISHED"}
|
||||||
bpy.ops.bim.unlink_ifc(filepath=self.filepath)
|
|
||||||
filepath = tool.Ifc.resolve_uri(self.filepath)
|
|
||||||
status = bpy.ops.bim.link_ifc(filepath=filepath, use_cache=False, use_relative_path=use_relative_path)
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
|
|
||||||
class ToggleLinkSelectability(bpy.types.Operator):
|
class ToggleLinkSelectability(bpy.types.Operator):
|
||||||
@@ -1598,16 +1620,18 @@ class ToggleLinkSelectability(bpy.types.Operator):
|
|||||||
bl_label = "Toggle Link Selectability"
|
bl_label = "Toggle Link Selectability"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Toggle selectability"
|
bl_description = "Toggle selectability"
|
||||||
link: bpy.props.StringProperty(name="Linked IFC Filepath")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
link = props.links[self.link]
|
link = props.links[self.link_index]
|
||||||
self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend"))
|
self.library_filepath = tool.Blender.ensure_blender_path_is_abs(
|
||||||
|
Path(link.filepath).with_suffix(".ifc.cache.blend")
|
||||||
|
)
|
||||||
link.is_selectable = (is_selectable := not link.is_selectable)
|
link.is_selectable = (is_selectable := not link.is_selectable)
|
||||||
for collection in self.get_linked_collections():
|
for collection in self.get_linked_collections():
|
||||||
collection.hide_select = not is_selectable
|
collection.hide_select = not is_selectable
|
||||||
if handle := link.empty_handle:
|
if handle := tool.Project.get_link_empty_handle(link):
|
||||||
handle.hide_select = not is_selectable
|
handle.hide_select = not is_selectable
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -1624,13 +1648,15 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
|||||||
bl_label = "Toggle Link Visibility"
|
bl_label = "Toggle Link Visibility"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Toggle visibility between SOLID and WIREFRAME"
|
bl_description = "Toggle visibility between SOLID and WIREFRAME"
|
||||||
link: bpy.props.StringProperty(name="Linked IFC Filepath")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
|
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
link = props.links[self.link]
|
link = props.links[self.link_index]
|
||||||
self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend"))
|
self.library_filepath = tool.Blender.ensure_blender_path_is_abs(
|
||||||
|
Path(link.filepath).with_suffix(".ifc.cache.blend")
|
||||||
|
)
|
||||||
if self.mode == "WIREFRAME":
|
if self.mode == "WIREFRAME":
|
||||||
self.toggle_wireframe(link)
|
self.toggle_wireframe(link)
|
||||||
elif self.mode == "VISIBLE":
|
elif self.mode == "VISIBLE":
|
||||||
@@ -1652,7 +1678,7 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
|||||||
layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections)
|
layer_collections = tool.Blender.get_layer_collections_mapping(linked_collections)
|
||||||
for layer_collection in layer_collections.values():
|
for layer_collection in layer_collections.values():
|
||||||
layer_collection.exclude = is_hidden
|
layer_collection.exclude = is_hidden
|
||||||
if handle := link.empty_handle:
|
if handle := tool.Project.get_link_empty_handle(link):
|
||||||
handle.hide_set(is_hidden)
|
handle.hide_set(is_hidden)
|
||||||
|
|
||||||
def get_linked_collections(self) -> list[bpy.types.Collection]:
|
def get_linked_collections(self) -> list[bpy.types.Collection]:
|
||||||
@@ -1663,17 +1689,95 @@ class ToggleLinkVisibility(bpy.types.Operator):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EnableEditingLink(bpy.types.Operator):
|
||||||
|
bl_idname = "bim.enable_editing_link"
|
||||||
|
bl_label = "Enable Editing Link"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
bl_description = "Enable editing link location"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
link = tool.Project.get_project_props().active_link
|
||||||
|
link.is_editing = True
|
||||||
|
tool.Geometry.unlock_object(tool.Project.get_link_empty_handle(link))
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class DisableEditingLink(bpy.types.Operator):
|
||||||
|
bl_idname = "bim.disable_editing_link"
|
||||||
|
bl_label = "Disable Editing Link"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
bl_description = "Disable editing link and restore to previously saved location"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
link = tool.Project.get_project_props().active_link
|
||||||
|
link.is_editing = False
|
||||||
|
obj = tool.Project.get_link_empty_handle(link)
|
||||||
|
obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link))
|
||||||
|
tool.Geometry.lock_object(obj)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class EditLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
bl_idname = "bim.edit_link"
|
||||||
|
bl_label = "Edit Link"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
bl_description = "Disable editing link and restore to previously saved location"
|
||||||
|
|
||||||
|
def _execute(self, context):
|
||||||
|
link = tool.Project.get_project_props().active_link
|
||||||
|
link.is_editing = False
|
||||||
|
obj = tool.Project.get_link_empty_handle(link)
|
||||||
|
new_obj_matrix = obj.matrix_world
|
||||||
|
|
||||||
|
filepath = Path(tool.Ifc.resolve_uri(link.filepath))
|
||||||
|
with open(filepath.with_suffix(".ifc.cache.json"), "r") as f:
|
||||||
|
metadata = json.load(f)
|
||||||
|
|
||||||
|
rot = ifcopenshell.util.shape_builder.np_rotation_matrix(
|
||||||
|
radians(-float(metadata["model_project_north"])), 4, "Z"
|
||||||
|
)
|
||||||
|
global_matrix = rot @ np.eye(4)
|
||||||
|
global_matrix[:, 3][:3] = [float(o) for o in metadata["model_origin_si"].split(",")]
|
||||||
|
|
||||||
|
gprops = tool.Georeference.get_georeference_props()
|
||||||
|
rot = ifcopenshell.util.shape_builder.np_rotation_matrix(radians(-float(gprops.model_project_north)), 4, "Z")
|
||||||
|
local_matrix = rot @ np.eye(4)
|
||||||
|
local_matrix[:, 3][:3] = [float(o) for o in gprops.model_origin_si.split(",")]
|
||||||
|
|
||||||
|
# obj_matrix is typically calculated as:
|
||||||
|
# obj_matrix = np.linalg.inv(local_matrix) @ transformation @ global_matrix
|
||||||
|
# So let's calculate the transformation
|
||||||
|
|
||||||
|
transformed_global_matrix = local_matrix @ np.array(new_obj_matrix)
|
||||||
|
transformation = transformed_global_matrix @ np.linalg.inv(global_matrix)
|
||||||
|
if np.allclose(transformation, np.eye(4)):
|
||||||
|
link.has_transformation = True
|
||||||
|
transformation = ",".join(map(str, np.eye(4).reshape(-1)))
|
||||||
|
else:
|
||||||
|
link.has_transformation = False
|
||||||
|
transformation = ",".join(map(str, transformation.reshape(-1)))
|
||||||
|
|
||||||
|
if tool.Ifc.get():
|
||||||
|
reference = tool.Ifc.get().by_id(link.ifc_definition_id)
|
||||||
|
reference[1] = transformation
|
||||||
|
else:
|
||||||
|
link.transformation = transformation
|
||||||
|
|
||||||
|
obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link))
|
||||||
|
tool.Geometry.lock_object(obj)
|
||||||
|
|
||||||
|
|
||||||
class SelectLinkHandle(bpy.types.Operator):
|
class SelectLinkHandle(bpy.types.Operator):
|
||||||
bl_idname = "bim.select_link_handle"
|
bl_idname = "bim.select_link_handle"
|
||||||
bl_label = "Select Link Handle"
|
bl_label = "Select Link Handle"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
bl_description = "Select link empty object handle"
|
bl_description = "Select link empty object handle"
|
||||||
index: bpy.props.IntProperty(name="Link Index")
|
link_index: bpy.props.IntProperty(name="Link Index")
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
link = props.links[self.index]
|
link = props.links[self.link_index]
|
||||||
handle = link.empty_handle
|
handle = tool.Project.get_link_empty_handle(link)
|
||||||
if not handle:
|
if not handle:
|
||||||
self.report({"ERROR"}, "Link has no empty handle (probably it was deleted).")
|
self.report({"ERROR"}, "Link has no empty handle (probably it was deleted).")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
@@ -1866,7 +1970,14 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
|||||||
print("Processing", self.filepath)
|
print("Processing", self.filepath)
|
||||||
|
|
||||||
self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath))
|
self.collection = bpy.data.collections.new("IfcProject/" + os.path.basename(self.filepath))
|
||||||
self.file = ifcopenshell.open(self.filepath)
|
|
||||||
|
try:
|
||||||
|
self.file = ifcopenshell.open(self.filepath)
|
||||||
|
except Exception as e:
|
||||||
|
self.report({"ERROR"}, f"Failed to open IFC file: {str(e)}")
|
||||||
|
bpy.data.collections.remove(self.collection)
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
tool.Ifc.set(self.file)
|
tool.Ifc.set(self.file)
|
||||||
print("Finished opening")
|
print("Finished opening")
|
||||||
|
|
||||||
@@ -1897,20 +2008,13 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
|
|||||||
if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin:
|
if tool.Loader.settings.false_origin_mode == "MANUAL" and tool.Loader.settings.false_origin:
|
||||||
tool.Loader.set_manual_blender_offset(self.file)
|
tool.Loader.set_manual_blender_offset(self.file)
|
||||||
elif tool.Loader.settings.false_origin_mode == "AUTOMATIC":
|
elif tool.Loader.settings.false_origin_mode == "AUTOMATIC":
|
||||||
if host_model_origin_si := gprops.host_model_origin_si:
|
tool.Loader.guess_false_origin(self.file)
|
||||||
host_model_origin_si = [float(o) / self.unit_scale for o in host_model_origin_si.split(",")]
|
|
||||||
tool.Loader.settings.false_origin = host_model_origin_si
|
|
||||||
tool.Loader.settings.project_north = float(gprops.host_model_project_north)
|
|
||||||
tool.Loader.set_manual_blender_offset(self.file)
|
|
||||||
else:
|
|
||||||
tool.Loader.guess_false_origin(self.file)
|
|
||||||
|
|
||||||
tool.Georeference.set_model_origin()
|
tool.Georeference.set_model_origin()
|
||||||
self.json_filepath = self.filepath + ".cache.json"
|
self.json_filepath = self.filepath + ".cache.json"
|
||||||
data = {
|
data = {
|
||||||
"host_model_origin": gprops.host_model_origin,
|
"model_is_georeferenced": gprops.model_is_georeferenced,
|
||||||
"host_model_origin_si": gprops.host_model_origin_si,
|
"model_crs": gprops.model_crs,
|
||||||
"host_model_project_north": gprops.host_model_project_north,
|
|
||||||
"model_origin": gprops.model_origin,
|
"model_origin": gprops.model_origin,
|
||||||
"model_origin_si": gprops.model_origin_si,
|
"model_origin_si": gprops.model_origin_si,
|
||||||
"model_project_north": gprops.model_project_north,
|
"model_project_north": gprops.model_project_north,
|
||||||
|
|||||||
@@ -16,8 +16,9 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
import math
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Literal, Union, assert_never, get_args
|
from typing import TYPE_CHECKING, Literal, Union, assert_never, get_args
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@@ -219,29 +220,62 @@ class FilterCategory(PropertyGroup):
|
|||||||
|
|
||||||
|
|
||||||
class Link(PropertyGroup):
|
class Link(PropertyGroup):
|
||||||
name: StringProperty(
|
name: StringProperty(name="Name")
|
||||||
name="Name",
|
filepath: StringProperty(
|
||||||
|
name="Filepath",
|
||||||
description="Filepath to linked .ifc file, stored in posix format (could be relative to .ifc file, not to .blend)",
|
description="Filepath to linked .ifc file, stored in posix format (could be relative to .ifc file, not to .blend)",
|
||||||
)
|
)
|
||||||
|
transformation: StringProperty(
|
||||||
|
name="Transformation",
|
||||||
|
description="4x4 matrix transformation as a flattened comma separated list for the linked model",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
georeferenced: EnumProperty(
|
||||||
|
name="Georeferenced",
|
||||||
|
description="Georeferencing status: compatibility between host and linked model",
|
||||||
|
items=[
|
||||||
|
("NONE", "No Georef", "Linked model has no georeferencing"),
|
||||||
|
("NOT_COMPATIBLE", "Not Compatible", "Has geo data but CRS differ from host"),
|
||||||
|
("FULL_COMPATIBLE", "Full Compatible", "Both CRS name and vertical datum match host"),
|
||||||
|
],
|
||||||
|
default="NONE",
|
||||||
|
)
|
||||||
|
has_transformation: BoolProperty(
|
||||||
|
name="Has Transformation",
|
||||||
|
description="Whether there is a transformation from its global coordinates",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
is_loaded: BoolProperty(name="Is Loaded", default=False)
|
is_loaded: BoolProperty(name="Is Loaded", default=False)
|
||||||
|
is_editing: BoolProperty(name="Is Editing", description="Whether the link is being transformed", default=False)
|
||||||
is_selectable: BoolProperty(name="Is Selectable", default=True)
|
is_selectable: BoolProperty(name="Is Selectable", default=True)
|
||||||
is_wireframe: BoolProperty(name="Is Wireframe", default=False)
|
is_wireframe: BoolProperty(name="Is Wireframe", default=False)
|
||||||
is_hidden: BoolProperty(name="Is Hidden", default=False)
|
is_hidden: BoolProperty(name="Is Hidden", default=False)
|
||||||
include_in_drawings: BoolProperty(name="Include in Drawings", default=True, options=set())
|
include_in_drawings: BoolProperty(name="Include in Drawings", default=True, options=set())
|
||||||
empty_handle: PointerProperty(
|
empty_handle: PointerProperty(
|
||||||
name="Empty Object Handle",
|
name="Empty Object Handle",
|
||||||
description="We use empty object handle to allow simple manipulations with a linked model (moving, scaling, rotating)",
|
description="Storage for empty handle. Used in non-IFC scenarios or temporarily during link creation",
|
||||||
type=bpy.types.Object,
|
type=bpy.types.Object,
|
||||||
)
|
)
|
||||||
|
ifc_definition_id: IntProperty(
|
||||||
|
name="IFC Definition ID",
|
||||||
|
description="STEP ID of the IfcDocumentReference when linked to a parent IFC project. Zero when no parent IFC exists",
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
name: str
|
name: str
|
||||||
|
filepath: str
|
||||||
|
transformation: str
|
||||||
|
georeferenced: Literal["NONE", "NOT_COMPATIBLE", "FULL_COMPATIBLE"]
|
||||||
|
has_transformation: bool
|
||||||
is_loaded: bool
|
is_loaded: bool
|
||||||
|
is_editing: bool
|
||||||
is_selectable: bool
|
is_selectable: bool
|
||||||
is_wireframe: bool
|
is_wireframe: bool
|
||||||
is_hidden: bool
|
is_hidden: bool
|
||||||
include_in_drawings: bool
|
include_in_drawings: bool
|
||||||
empty_handle: Union[bpy.types.Object, None]
|
empty_handle: Union[bpy.types.Object, None]
|
||||||
|
ifc_definition_id: int
|
||||||
|
|
||||||
|
|
||||||
class EditedObj(PropertyGroup):
|
class EditedObj(PropertyGroup):
|
||||||
@@ -424,6 +458,10 @@ class BIMProjectProperties(PropertyGroup):
|
|||||||
clipping_planes_active_index: bpy.props.IntProperty(min=0, default=0, max=5)
|
clipping_planes_active_index: bpy.props.IntProperty(min=0, default=0, max=5)
|
||||||
edited_objs: bpy.props.CollectionProperty(type=EditedObj)
|
edited_objs: bpy.props.CollectionProperty(type=EditedObj)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_link(self) -> Union[Link, None]:
|
||||||
|
return tool.Blender.get_active_uilist_element(self.links, self.active_link_index)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_clipping_plane(self) -> ObjProperty | None:
|
def active_clipping_plane(self) -> ObjProperty | None:
|
||||||
return tool.Blender.get_active_uilist_element(self.clipping_planes, self.clipping_planes_active_index)
|
return tool.Blender.get_active_uilist_element(self.clipping_planes, self.clipping_planes_active_index)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import os
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import math
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
from bpy.types import Menu, Panel, UIList
|
from bpy.types import Menu, Panel, UIList
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ import bonsai.tool as tool
|
|||||||
from bonsai.bim.helper import draw_attributes, prop_with_search
|
from bonsai.bim.helper import draw_attributes, prop_with_search
|
||||||
from bonsai.bim.ifc import IfcStore
|
from bonsai.bim.ifc import IfcStore
|
||||||
from bonsai.bim.module.project.data import LinksData, ProjectData
|
from bonsai.bim.module.project.data import LinksData, ProjectData
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bonsai.bim.module.project.prop import (
|
from bonsai.bim.module.project.prop import (
|
||||||
@@ -477,17 +479,27 @@ class BIM_PT_links(Panel):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
self.props = tool.Project.get_project_props()
|
self.props = tool.Project.get_project_props()
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.operator("bim.link_ifc")
|
row.operator("bim.link_ifc")
|
||||||
if self.props.links:
|
if self.props.links:
|
||||||
self.layout.template_list(
|
if self.props.active_link:
|
||||||
"BIM_UL_links",
|
row = self.layout.row(align=True)
|
||||||
"",
|
row.alignment = "RIGHT"
|
||||||
self.props,
|
index = self.props.active_link_index
|
||||||
"links",
|
if self.props.active_link.is_editing:
|
||||||
self.props,
|
row.operator("bim.edit_link", text="", icon="CHECKMARK")
|
||||||
"active_link_index",
|
row.operator("bim.disable_editing_link", text="", icon="CANCEL")
|
||||||
)
|
else:
|
||||||
|
row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL")
|
||||||
|
if self.props.active_link.is_loaded:
|
||||||
|
row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA").link_index = index
|
||||||
|
row.operator("bim.unload_link", text="", icon="UNLINKED").link_index = index
|
||||||
|
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
|
||||||
|
else:
|
||||||
|
row.operator("bim.load_link", text="", icon="LINKED").link_index = index
|
||||||
|
row.operator("bim.unlink_ifc", text="", icon="X").link_index = index
|
||||||
|
self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index")
|
||||||
|
|
||||||
if LinksData.enable_culling:
|
if LinksData.enable_culling:
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
@@ -607,47 +619,30 @@ class BIM_UL_links(UIList):
|
|||||||
active_propname,
|
active_propname,
|
||||||
index,
|
index,
|
||||||
):
|
):
|
||||||
if item:
|
row = layout.row(align=True)
|
||||||
row = layout.row(align=True)
|
if item.is_loaded:
|
||||||
if item.is_loaded:
|
if item.georeferenced == "NONE":
|
||||||
row.label(text=item.name)
|
row.label(text="", icon="QUESTION")
|
||||||
op = row.operator(
|
elif item.georeferenced == "NOT_COMPATIBLE":
|
||||||
"bim.toggle_link_selectability",
|
row.label(text="", icon="ERROR")
|
||||||
text="",
|
elif item.georeferenced == "FULL_COMPATIBLE":
|
||||||
icon="RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON",
|
row.label(text="", icon="WORLD")
|
||||||
emboss=False,
|
if item.has_transformation:
|
||||||
)
|
row.label(text="", icon="OBJECT_ORIGIN")
|
||||||
op.link = item.name
|
|
||||||
op = row.operator(
|
row.label(text=item.filepath)
|
||||||
"bim.toggle_link_visibility",
|
icon = "RESTRICT_SELECT_OFF" if item.is_selectable else "RESTRICT_SELECT_ON"
|
||||||
text="",
|
row.operator("bim.toggle_link_selectability", text="", icon=icon, emboss=False).link_index = index
|
||||||
icon="CUBE" if item.is_wireframe else "MESH_CUBE",
|
icon = "CUBE" if item.is_wireframe else "MESH_CUBE"
|
||||||
emboss=False,
|
op = row.operator("bim.toggle_link_visibility", text="", icon=icon, emboss=False)
|
||||||
)
|
op.link_index = index
|
||||||
op.link = item.name
|
op.mode = "WIREFRAME"
|
||||||
op.mode = "WIREFRAME"
|
icon = "HIDE_ON" if item.is_hidden else "HIDE_OFF"
|
||||||
op = row.operator(
|
op = row.operator("bim.toggle_link_visibility", text="", icon=icon, emboss=False)
|
||||||
"bim.toggle_link_visibility",
|
op.link_index = index
|
||||||
text="",
|
op.mode = "VISIBLE"
|
||||||
icon="HIDE_ON" if item.is_hidden else "HIDE_OFF",
|
else:
|
||||||
emboss=False,
|
row.label(text=item.filepath)
|
||||||
)
|
|
||||||
op.link = item.name
|
|
||||||
op.mode = "VISIBLE"
|
|
||||||
op = row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA")
|
|
||||||
op.index = index
|
|
||||||
op = row.operator("bim.unload_link", text="", icon="UNLINKED")
|
|
||||||
op.filepath = item.name
|
|
||||||
op = row.operator("bim.reload_link", text="", icon="FILE_REFRESH")
|
|
||||||
op.filepath = item.name
|
|
||||||
else:
|
|
||||||
row.prop(item, "name", text="")
|
|
||||||
op = row.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
|
|
||||||
op.attribute_data_path = tool.Blender.get_full_data_path(item, "name")
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_purge(Panel):
|
class BIM_PT_purge(Panel):
|
||||||
|
|||||||
@@ -268,63 +268,45 @@ class SaveBlendMetadataFile(bpy.types.Operator):
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
# Ensure all styles are loaded before attempting to remove them
|
# Ensure all styles are loaded before attempting to remove them
|
||||||
try:
|
bpy.ops.bim.load_styles()
|
||||||
bpy.ops.bim.load_styles()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 1. Collect all IfcStyle material names
|
# 1. Collect all IfcStyle material names
|
||||||
ifcstyle_material_names = []
|
ifcstyle_material_names = []
|
||||||
try:
|
styles_props = getattr(bpy.context.scene, "BIMStylesProperties", None)
|
||||||
styles_props = getattr(bpy.context.scene, "BIMStylesProperties", None)
|
if styles_props is None and bpy.data.scenes:
|
||||||
if styles_props is None and bpy.data.scenes:
|
styles_props = getattr(bpy.data.scenes[0], "BIMStylesProperties", None)
|
||||||
styles_props = getattr(bpy.data.scenes[0], "BIMStylesProperties", None)
|
if styles_props:
|
||||||
if styles_props:
|
for style in list(styles_props.styles):
|
||||||
for style in list(styles_props.styles):
|
material = getattr(style, "blender_material", None)
|
||||||
material = getattr(style, "blender_material", None)
|
if material and material.name:
|
||||||
if material and material.name:
|
ifcstyle_material_names.append(material.name)
|
||||||
ifcstyle_material_names.append(material.name)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 2. Purge IfcStore
|
# 2. Purge IfcStore
|
||||||
try:
|
from bonsai.bim.ifc import IfcStore
|
||||||
from bonsai.bim.ifc import IfcStore
|
IfcStore.purge()
|
||||||
except ImportError:
|
|
||||||
IfcStore = None
|
|
||||||
|
|
||||||
if IfcStore:
|
|
||||||
try:
|
|
||||||
IfcStore.purge()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 3. Remove all collections named IfcProject*
|
# 3. Remove all collections named IfcProject*
|
||||||
for collection in list(bpy.data.collections):
|
for collection in list(bpy.data.collections):
|
||||||
if collection.name.startswith('IfcProject'):
|
if collection.name.startswith('IfcProject'):
|
||||||
try:
|
bpy.data.collections.remove(collection, do_unlink=True)
|
||||||
bpy.data.collections.remove(collection, do_unlink=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4. Purge orphaned data blocks after removing IfcProject collections
|
# 4.1 Remove all collections from linked libraries (they will be recreated by bonsai)
|
||||||
try:
|
for collection in list(bpy.data.collections):
|
||||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
if collection.library:
|
||||||
except Exception:
|
bpy.data.collections.remove(collection, do_unlink=True)
|
||||||
pass
|
|
||||||
|
|
||||||
# 5. Remove all materials corresponding to the IfcStyles we collected
|
# 4.2. Remove all empty objects that are collection instances for linked models
|
||||||
materials_removed = 0
|
for obj in list(bpy.data.objects):
|
||||||
try:
|
if obj.type == 'EMPTY' and obj.instance_type == 'COLLECTION' and obj.name.startswith('IfcProject/'):
|
||||||
for mat_name in ifcstyle_material_names:
|
bpy.data.objects.remove(obj, do_unlink=True)
|
||||||
if mat_name in bpy.data.materials:
|
|
||||||
try:
|
# 5. Purge orphaned data blocks after removing IfcProject collections
|
||||||
bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True)
|
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||||
materials_removed += 1
|
|
||||||
except Exception:
|
# 6. Remove all materials corresponding to the IfcStyles we collected
|
||||||
pass
|
for mat_name in ifcstyle_material_names:
|
||||||
except Exception:
|
if mat_name in bpy.data.materials:
|
||||||
pass
|
bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True)
|
||||||
|
|
||||||
bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}')
|
bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}')
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -660,6 +660,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
|||||||
bsdd_load_test_dictionaries: BoolProperty(
|
bsdd_load_test_dictionaries: BoolProperty(
|
||||||
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
|
name="Load Test Dictionaries", description="Load dictionaries that are for testing only", default=False
|
||||||
)
|
)
|
||||||
|
bsdd_baseurl: StringProperty(
|
||||||
|
name="bSDD API Base URL", description="Base URL for data dictionary API requests, e.g. https://api.bsdd.buildingsmart.org/api/",
|
||||||
|
default="https://api.bsdd.buildingsmart.org/api/",
|
||||||
|
)
|
||||||
should_disable_undo_on_save: BoolProperty(
|
should_disable_undo_on_save: BoolProperty(
|
||||||
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
|
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
|
||||||
)
|
)
|
||||||
@@ -972,6 +976,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
|||||||
layout.prop(self, "bsdd_load_preview_dictionaries")
|
layout.prop(self, "bsdd_load_preview_dictionaries")
|
||||||
layout.prop(self, "bsdd_load_inactive_dictionaries")
|
layout.prop(self, "bsdd_load_inactive_dictionaries")
|
||||||
layout.prop(self, "bsdd_load_test_dictionaries")
|
layout.prop(self, "bsdd_load_test_dictionaries")
|
||||||
|
layout.prop(self, "bsdd_baseurl")
|
||||||
|
|
||||||
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||||
layout.prop(self, "container_hide_show_isolate")
|
layout.prop(self, "container_hide_show_isolate")
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def add_georeferencing(georeference: type[tool.Georeference]) -> None:
|
def add_georeferencing(georeference: type[tool.Georeference]) -> None:
|
||||||
georeference.add_georeferencing()
|
georeference.add_georeferencing()
|
||||||
|
georeference.set_model_origin()
|
||||||
|
|
||||||
|
|
||||||
def enable_editing_georeferencing(georeference: type[tool.Georeference]) -> None:
|
def enable_editing_georeferencing(georeference: type[tool.Georeference]) -> None:
|
||||||
@@ -37,8 +38,9 @@ def enable_editing_georeferencing(georeference: type[tool.Georeference]) -> None
|
|||||||
georeference.enable_editing()
|
georeference.enable_editing()
|
||||||
|
|
||||||
|
|
||||||
def remove_georeferencing(ifc: type[tool.Ifc]) -> None:
|
def remove_georeferencing(ifc: type[tool.Ifc], georeference: type[tool.Georeference]) -> None:
|
||||||
ifc.run("georeference.remove_georeferencing")
|
ifc.run("georeference.remove_georeferencing")
|
||||||
|
georeference.set_model_origin()
|
||||||
|
|
||||||
|
|
||||||
def disable_editing_georeferencing(georeference: type[tool.Georeference]) -> None:
|
def disable_editing_georeferencing(georeference: type[tool.Georeference]) -> None:
|
||||||
|
|||||||
@@ -2165,3 +2165,13 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
if cls.BLENDER_5:
|
if cls.BLENDER_5:
|
||||||
return np.array(mathutils_type)
|
return np.array(mathutils_type)
|
||||||
return np.array(mathutils_type, dtype=np.float32)
|
return np.array(mathutils_type, dtype=np.float32)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_selected_files(
|
||||||
|
cls, directory: str, files: bpy.types.OperatorFileListElement, use_relative_path=False
|
||||||
|
) -> list[Path]:
|
||||||
|
return [
|
||||||
|
tool.Ifc.get_uri(Path(directory) / f.name, use_relative_path=use_relative_path)
|
||||||
|
for f in files
|
||||||
|
if (Path(directory) / f.name).is_file()
|
||||||
|
]
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ class Bsdd(bonsai.core.tool.Bsdd):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]:
|
def get_dictionaries(cls) -> list[bsdd.DictionaryContractV1]:
|
||||||
prefs = tool.Blender.get_addon_preferences()
|
prefs = tool.Blender.get_addon_preferences()
|
||||||
|
baseurl = getattr(prefs, "bsdd_baseurl", "https://api.bsdd.buildingsmart.org/api/")
|
||||||
|
cls.client = bsdd.Client()
|
||||||
|
if hasattr(cls.client, "baseurl"):
|
||||||
|
cls.client.baseurl = baseurl
|
||||||
response = cls.client.get_dictionary(include_test_dictionaries=prefs.bsdd_load_test_dictionaries)
|
response = cls.client.get_dictionary(include_test_dictionaries=prefs.bsdd_load_test_dictionaries)
|
||||||
dicts = response.get("dictionaries") or []
|
dicts = response.get("dictionaries") or []
|
||||||
statuses = ["Active"]
|
statuses = ["Active"]
|
||||||
|
|||||||
@@ -251,11 +251,19 @@ class Document(bonsai.core.tool.Document):
|
|||||||
def get_document_references(
|
def get_document_references(
|
||||||
cls, document: ifcopenshell.entity_instance
|
cls, document: ifcopenshell.entity_instance
|
||||||
) -> tuple[ifcopenshell.entity_instance, ...]:
|
) -> tuple[ifcopenshell.entity_instance, ...]:
|
||||||
|
# TODO: migrate to util.document and replace all instances
|
||||||
"""Get IfcDocumentReference.ReferencedDocuments, compatible with IFC2X3."""
|
"""Get IfcDocumentReference.ReferencedDocuments, compatible with IFC2X3."""
|
||||||
if document.file.schema == "IFC2X3":
|
if document.file.schema == "IFC2X3":
|
||||||
return document.DocumentReferences or ()
|
return document.DocumentReferences or ()
|
||||||
return document.HasDocumentReferences
|
return document.HasDocumentReferences
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_reference_document(cls, reference: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None:
|
||||||
|
# TODO: migrate to util.document and replace all instances
|
||||||
|
if reference.file.schema == "IFC2X3":
|
||||||
|
return (reference.ReferenceToDocument or (None))[0]
|
||||||
|
return reference.ReferencedDocument
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clear_active_document(cls) -> None:
|
def clear_active_document(cls) -> None:
|
||||||
props = cls.get_document_props()
|
props = cls.get_document_props()
|
||||||
|
|||||||
@@ -857,9 +857,11 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None:
|
def edit_text_literals(cls, obj: bpy.types.Object, literal_attributes: dict) -> None:
|
||||||
assert (element := tool.Ifc.get_entity(obj))
|
assert (element := tool.Ifc.get_entity(obj))
|
||||||
assert (rep := cls.get_annotation_representation(element))
|
assert (rep := cls.get_annotation_representation(element))
|
||||||
for literal in cls.get_text_literal(obj, return_list=True):
|
to_remove = [i for i in rep.Items if i.is_a("IfcTextLiteral")]
|
||||||
|
new_literals = [cls.add_literal(**a) for a in literal_attributes]
|
||||||
|
rep.Items = [i for i in rep.Items if not i.is_a("IfcTextLiteral")] + new_literals
|
||||||
|
for literal in to_remove:
|
||||||
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), literal)
|
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), literal)
|
||||||
rep.Items = [cls.add_literal(**a) for a in literal_attributes]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_literal(cls, **attributes: str) -> ifcopenshell.entity_instance:
|
def add_literal(cls, **attributes: str) -> ifcopenshell.entity_instance:
|
||||||
|
|||||||
@@ -315,6 +315,21 @@ class Georeference(bonsai.core.tool.Georeference):
|
|||||||
)
|
)
|
||||||
return coordinates
|
return coordinates
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def global2local(cls, matrix, is_specified_in_map_units: bool) -> tuple[float, float, float]:
|
||||||
|
matrix = ifcopenshell.util.geolocation.auto_global2local(tool.Ifc.get(), matrix, is_specified_in_map_units=is_specified_in_map_units)
|
||||||
|
props = cls.get_georeference_props()
|
||||||
|
if props.has_blender_offset:
|
||||||
|
matrix = ifcopenshell.util.geolocation.global2local(
|
||||||
|
matrix,
|
||||||
|
float(props.blender_offset_x),
|
||||||
|
float(props.blender_offset_y),
|
||||||
|
float(props.blender_offset_z),
|
||||||
|
float(props.blender_x_axis_abscissa),
|
||||||
|
float(props.blender_x_axis_ordinate),
|
||||||
|
)
|
||||||
|
return matrix
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def import_plot(cls, filepath: str) -> None:
|
def import_plot(cls, filepath: str) -> None:
|
||||||
import bmesh
|
import bmesh
|
||||||
@@ -385,6 +400,9 @@ class Georeference(bonsai.core.tool.Georeference):
|
|||||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||||
gprops = tool.Georeference.get_georeference_props()
|
gprops = tool.Georeference.get_georeference_props()
|
||||||
e, n, h = cls.xyz2enh((0, 0, 0), should_return_in_map_units=False)
|
e, n, h = cls.xyz2enh((0, 0, 0), should_return_in_map_units=False)
|
||||||
|
crs = ifcopenshell.util.geolocation.get_crs(tool.Ifc.get()) or {}
|
||||||
|
gprops.model_is_georeferenced = bool(crs)
|
||||||
|
gprops.model_crs = crs.get("Name", "") or ""
|
||||||
gprops.model_origin = f"{e},{n},{h}"
|
gprops.model_origin = f"{e},{n},{h}"
|
||||||
gprops.model_origin_si = f"{e * unit_scale},{n * unit_scale},{h * unit_scale}"
|
gprops.model_origin_si = f"{e * unit_scale},{n * unit_scale},{h * unit_scale}"
|
||||||
angle = ifcopenshell.util.geolocation.get_grid_north(tool.Ifc.get())
|
angle = ifcopenshell.util.geolocation.get_grid_north(tool.Ifc.get())
|
||||||
|
|||||||
@@ -19,10 +19,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
import math
|
||||||
import shutil
|
import shutil
|
||||||
|
import numpy as np
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from math import radians
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, NamedTuple, Optional, Union
|
from typing import TYPE_CHECKING, NamedTuple, Optional
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
@@ -58,6 +62,47 @@ class Project(bonsai.core.tool.Project):
|
|||||||
assert (scene := bpy.context.scene)
|
assert (scene := bpy.context.scene)
|
||||||
return scene.MeasureToolSettings # pyright: ignore[reportAttributeAccessIssue]
|
return scene.MeasureToolSettings # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_link_empty_handle(cls, link) -> bpy.types.Object | None:
|
||||||
|
if tool.Ifc.get():
|
||||||
|
return tool.Ifc.get_object(tool.Ifc.get().by_id(link.ifc_definition_id))
|
||||||
|
return link.empty_handle
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_link_empty_handle(cls, link, empty: bpy.types.Object) -> None:
|
||||||
|
if tool.Ifc.get():
|
||||||
|
tool.Ifc.link(tool.Ifc.get().by_id(link.ifc_definition_id), empty)
|
||||||
|
else:
|
||||||
|
link.empty_handle = empty
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def calculate_link_matrix(cls, link) -> None:
|
||||||
|
filepath = Path(tool.Ifc.resolve_uri(link.filepath))
|
||||||
|
with open(filepath.with_suffix(".ifc.cache.json"), "r") as f:
|
||||||
|
metadata = json.load(f)
|
||||||
|
|
||||||
|
rot = ifcopenshell.util.shape_builder.np_rotation_matrix(
|
||||||
|
radians(-float(metadata["model_project_north"])), 4, "Z"
|
||||||
|
)
|
||||||
|
global_matrix = rot @ np.eye(4)
|
||||||
|
global_matrix[:, 3][:3] = [float(o) for o in metadata["model_origin_si"].split(",")]
|
||||||
|
|
||||||
|
if tool.Ifc.get():
|
||||||
|
transformation = tool.Ifc.get().by_id(link.ifc_definition_id)[1] # Identification
|
||||||
|
else:
|
||||||
|
transformation = link.transformation
|
||||||
|
|
||||||
|
if transformation:
|
||||||
|
transformation = np.fromstring(transformation, sep=",", dtype=np.float64).reshape(4, 4)
|
||||||
|
if not np.allclose(transformation, np.eye(4)):
|
||||||
|
global_matrix = transformation @ global_matrix
|
||||||
|
|
||||||
|
gprops = tool.Georeference.get_georeference_props()
|
||||||
|
rot = ifcopenshell.util.shape_builder.np_rotation_matrix(radians(-float(gprops.model_project_north)), 4, "Z")
|
||||||
|
local_matrix = rot @ np.eye(4)
|
||||||
|
local_matrix[:, 3][:3] = [float(o) for o in gprops.model_origin_si.split(",")]
|
||||||
|
return np.linalg.inv(local_matrix) @ global_matrix
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def append_all_types_from_template(cls, template: str) -> None:
|
def append_all_types_from_template(cls, template: str) -> None:
|
||||||
# TODO refactor
|
# TODO refactor
|
||||||
@@ -249,68 +294,32 @@ class Project(bonsai.core.tool.Project):
|
|||||||
tool.Root.reload_grid_decorator()
|
tool.Root.reload_grid_decorator()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_linked_models_document(cls) -> Union[ifcopenshell.entity_instance, None]:
|
def get_linked_models_documents(cls) -> dict[str, ifcopenshell.entity_instance]:
|
||||||
for document in tool.Ifc.get().by_type("IfcDocumentInformation"):
|
linked_docs = {}
|
||||||
if document.Name == "BBIM_Linked_Models":
|
for doc in tool.Ifc.get().by_type("IfcDocumentInformation"):
|
||||||
return document
|
if doc.Scope == "LINKED_MODEL":
|
||||||
|
for reference in tool.Drawing.get_document_references(doc):
|
||||||
|
linked_docs[Path(reference.Location).as_posix()] = doc
|
||||||
|
break
|
||||||
|
return linked_docs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_linked_models_from_ifc(cls) -> None:
|
def load_linked_models_from_ifc(cls) -> None:
|
||||||
links = tool.Project.get_project_props().links
|
links = tool.Project.get_project_props().links
|
||||||
links.clear()
|
links.clear()
|
||||||
links_document = cls.get_linked_models_document()
|
for doc in tool.Ifc.get().by_type("IfcDocumentInformation"):
|
||||||
if not links_document:
|
if doc.Scope != "LINKED_MODEL":
|
||||||
return
|
continue
|
||||||
|
for reference in tool.Drawing.get_document_references(doc):
|
||||||
references = tool.Document.get_document_references(links_document)
|
filepath = reference.Location
|
||||||
if not references:
|
link = links.add()
|
||||||
return
|
link.name = filepath
|
||||||
|
link.filepath = filepath
|
||||||
for reference in references:
|
link.ifc_definition_id = reference.id()
|
||||||
link = links.add()
|
link.has_transformation = False
|
||||||
link.name = reference.Location
|
if reference[1]:
|
||||||
|
m = np.fromstring(reference[1], sep=",", dtype=np.float64).reshape(4, 4)
|
||||||
@classmethod
|
link.has_transformation = not np.allclose(m, np.eye(4))
|
||||||
def save_linked_models_to_ifc(cls) -> None:
|
|
||||||
ifc_file = tool.Ifc.get()
|
|
||||||
links = tool.Project.get_project_props().links
|
|
||||||
filepaths: set[Path] = set()
|
|
||||||
for link in links:
|
|
||||||
filepaths.add(Path(link.name))
|
|
||||||
|
|
||||||
links_document = cls.get_linked_models_document()
|
|
||||||
|
|
||||||
if not filepaths and links_document is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
paths_to_add = filepaths.copy()
|
|
||||||
references_to_remove: list[ifcopenshell.entity_instance] = []
|
|
||||||
if links_document:
|
|
||||||
references = tool.Document.get_document_references(links_document)
|
|
||||||
for reference in references:
|
|
||||||
# I guess got corrupted by the user.
|
|
||||||
if not (location := reference.Location):
|
|
||||||
references_to_remove.remove(reference)
|
|
||||||
continue
|
|
||||||
path = Path(location)
|
|
||||||
if path in paths_to_add:
|
|
||||||
paths_to_add.remove(path)
|
|
||||||
else:
|
|
||||||
references_to_remove.append(reference)
|
|
||||||
|
|
||||||
if paths_to_add:
|
|
||||||
if links_document is None:
|
|
||||||
links_document = ifcopenshell.api.document.add_information(ifc_file)
|
|
||||||
links_document.Name = "BBIM_Linked_Models"
|
|
||||||
links_document.Description = "Bonsai internal document containing references to currently linked models"
|
|
||||||
|
|
||||||
for path in paths_to_add:
|
|
||||||
reference = ifcopenshell.api.document.add_reference(ifc_file, links_document)
|
|
||||||
reference.Location = path.as_posix()
|
|
||||||
|
|
||||||
if references_to_remove:
|
|
||||||
for reference in references_to_remove:
|
|
||||||
ifcopenshell.api.document.remove_reference(ifc_file, reference)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_project_library_elements(
|
def get_project_library_elements(
|
||||||
|
|||||||
@@ -118,14 +118,10 @@ class Snap(bonsai.core.tool.Snap):
|
|||||||
def get_angle_snap_value(cls, context: bpy.types.Context) -> float:
|
def get_angle_snap_value(cls, context: bpy.types.Context) -> float:
|
||||||
"""Get the angle snap increment from Blender's tool settings.
|
"""Get the angle snap increment from Blender's tool settings.
|
||||||
|
|
||||||
Uses snap_angle_increment_3d (Blender 5.0+) or snap_angle_increment (Blender 4.x).
|
|
||||||
|
|
||||||
:param context: Blender context
|
:param context: Blender context
|
||||||
:return: Angle snap increment in degrees
|
:return: Angle snap increment in degrees
|
||||||
"""
|
"""
|
||||||
if bpy.app.version >= (5, 0, 0):
|
return math.degrees(context.scene.tool_settings.snap_angle_increment_3d)
|
||||||
return math.degrees(context.scene.tool_settings.snap_angle_increment_3d)
|
|
||||||
return math.degrees(context.scene.tool_settings.snap_angle_increment)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index):
|
def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index):
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from bonsai.bim.ifc import IfcStore
|
|||||||
|
|
||||||
# Monkey-patch webbrowser opening since we want to test headlessly
|
# Monkey-patch webbrowser opening since we want to test headlessly
|
||||||
webbrowser.open = lambda x: True
|
webbrowser.open = lambda x: True
|
||||||
|
tool.Drawing.open_with_user_command = lambda x, y: True
|
||||||
|
|
||||||
|
|
||||||
variables = {"cwd": os.getcwd(), "ifc": "IfcStore.get_file()"}
|
variables = {"cwd": os.getcwd(), "ifc": "IfcStore.get_file()"}
|
||||||
|
|||||||
@@ -3,12 +3,6 @@ Feature: Drawing
|
|||||||
|
|
||||||
Scenario: Duplicate drawing
|
Scenario: Duplicate drawing
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
And I add a cube
|
|
||||||
And the object "Cube" is selected
|
|
||||||
And I look at the "Class" panel
|
|
||||||
And I set the "Products" property to "IfcElement"
|
|
||||||
And I set the "Class" property to "IfcWall"
|
|
||||||
And I click "Assign IFC Class"
|
|
||||||
And I save IFC project
|
And I save IFC project
|
||||||
And I look at the "Drawings" panel
|
And I look at the "Drawings" panel
|
||||||
And I click "IMPORT"
|
And I click "IMPORT"
|
||||||
@@ -315,3 +309,10 @@ Scenario: Create sheet - with a drawing added to it
|
|||||||
And I click "IMAGE_PLANE"
|
And I click "IMAGE_PLANE"
|
||||||
When I click "OUTPUT"
|
When I click "OUTPUT"
|
||||||
Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "IfcWall"
|
Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "IfcWall"
|
||||||
|
|
||||||
|
Scenario: Add reference image
|
||||||
|
Given an empty IFC project
|
||||||
|
And I save IFC project
|
||||||
|
When I press "bim.add_reference_image(filepath='{cwd}/test/files/image.jpg')"
|
||||||
|
Then the object "IfcAnnotation/image" exists
|
||||||
|
And the object "IfcAnnotation/image" dimensions are "1.0,0.565,0."
|
||||||
|
|||||||
@@ -677,31 +677,43 @@ Scenario: Load project elements - all georeferencing coordinate situations with
|
|||||||
And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1"
|
And the object "IfcActuator/J" has a vertex at "10.366,3.813,-1"
|
||||||
And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1"
|
And the object "IfcActuator/J" has a vertex at "12.298,4.331,-1"
|
||||||
|
|
||||||
Scenario: Link IFC
|
Scenario: Link IFC - from an empty IFC project
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
When I link IFC project from "{cwd}/test/files/basic.ifc"
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
|
||||||
And the collection "IfcProject/basic.ifc" exists
|
And the collection "IfcProject/basic.ifc" exists
|
||||||
And the object "Chunk" exists
|
And the object "Chunk" exists
|
||||||
And the object "Chunk" is placed in the collection "IfcProject/basic.ifc"
|
And the object "Chunk" is placed in the collection "IfcProject/basic.ifc"
|
||||||
|
|
||||||
Scenario: Link IFC - disabled false origin mode
|
|
||||||
Given an empty IFC project
|
|
||||||
# Not currently possible via UI
|
|
||||||
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
|
||||||
And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED"
|
|
||||||
When I link IFC project from "{cwd}/test/files/geolocation.ifc"
|
|
||||||
Then the object "Chunk" exists
|
|
||||||
And the object "Chunk" has a vertex at "2,2,-1"
|
|
||||||
And the object "Chunk" has a vertex at "9,-1,-1"
|
|
||||||
And the object "Chunk" has a vertex at "17,4,-1"
|
|
||||||
|
|
||||||
Scenario: Link IFC - from an empty IFC project - automatic false origin mode (0,0,0 will be the false origin)
|
Scenario: Link IFC - from an empty IFC project - automatic false origin mode (0,0,0 will be the false origin)
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
# Not currently possible via UI
|
# Not currently possible via UI
|
||||||
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
||||||
When I link IFC project from "{cwd}/test/files/geolocation.ifc"
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)"
|
||||||
|
Then the object "Chunk" exists
|
||||||
|
And the object "Chunk" has a vertex at "2,2,-1"
|
||||||
|
And the object "Chunk" has a vertex at "9,-1,-1"
|
||||||
|
And the object "Chunk" has a vertex at "17,4,-1"
|
||||||
|
|
||||||
|
Scenario: Link IFC - from an empty IFC project - manual false origin mode
|
||||||
|
Given an empty IFC project
|
||||||
|
# Not currently possible via UI
|
||||||
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "MANUAL"
|
||||||
|
And I set "scene.BIMProjectProperties.false_origin" to "10000,0,0"
|
||||||
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)"
|
||||||
|
Then the object "Chunk" exists
|
||||||
|
And the object "Chunk" has a vertex at "2,2,-1"
|
||||||
|
And the object "Chunk" has a vertex at "9,-1,-1"
|
||||||
|
And the object "Chunk" has a vertex at "17,4,-1"
|
||||||
|
|
||||||
|
Scenario: Link IFC - from an empty IFC project - disabled false origin mode
|
||||||
|
Given an empty IFC project
|
||||||
|
# Not currently possible via UI
|
||||||
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED"
|
||||||
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)"
|
||||||
Then the object "Chunk" exists
|
Then the object "Chunk" exists
|
||||||
And the object "Chunk" has a vertex at "2,2,-1"
|
And the object "Chunk" has a vertex at "2,2,-1"
|
||||||
And the object "Chunk" has a vertex at "9,-1,-1"
|
And the object "Chunk" has a vertex at "9,-1,-1"
|
||||||
@@ -712,31 +724,42 @@ Scenario: Link IFC - from an empty Blender session - automatic false origin mode
|
|||||||
# Not currently possible via UI
|
# Not currently possible via UI
|
||||||
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
||||||
When I link IFC project from "{cwd}/test/files/geolocation.ifc"
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)"
|
||||||
Then the object "Chunk" exists
|
Then the object "Chunk" exists
|
||||||
And the object "Chunk" has a vertex at "-11,-2,0"
|
And the object "Chunk" has a vertex at "-11,-2,0"
|
||||||
And the object "Chunk" has a vertex at "-4,-5,0"
|
And the object "Chunk" has a vertex at "-4,-5,0"
|
||||||
And the object "Chunk" has a vertex at "4,0,0"
|
And the object "Chunk" has a vertex at "4,0,0"
|
||||||
|
|
||||||
Scenario: Link IFC - manual false origin mode
|
Scenario: Link IFC - from an empty Blender session - manual false origin mode
|
||||||
Given an empty Blender session
|
Given an empty Blender session
|
||||||
# Not currently possible via UI
|
# Not currently possible via UI
|
||||||
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
And I set "scene.BIMProjectProperties.false_origin_mode" to "MANUAL"
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "MANUAL"
|
||||||
And I set "scene.BIMProjectProperties.false_origin" to "10000,0,0"
|
And I set "scene.BIMProjectProperties.false_origin" to "10000,0,0"
|
||||||
When I link IFC project from "{cwd}/test/files/geolocation.ifc"
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)"
|
||||||
Then the object "Chunk" exists
|
Then the object "Chunk" exists
|
||||||
And the object "Chunk" has a vertex at "-8,2,-1"
|
And the object "Chunk" has a vertex at "-8,2,-1"
|
||||||
And the object "Chunk" has a vertex at "-1,-1,-1"
|
And the object "Chunk" has a vertex at "-1,-1,-1"
|
||||||
And the object "Chunk" has a vertex at "7,4,-1"
|
And the object "Chunk" has a vertex at "7,4,-1"
|
||||||
|
|
||||||
Scenario: Link IFC - automatic false origin mode - two different false origins and project norths - grid north is up because we start with geolocation.ifc
|
Scenario: Link IFC - from an empty Blender session - disabled false origin mode
|
||||||
|
Given an empty Blender session
|
||||||
|
# Not currently possible via UI
|
||||||
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "DISABLED"
|
||||||
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)"
|
||||||
|
Then the object "Chunk" exists
|
||||||
|
And the object "Chunk" has a vertex at "2,2,-1"
|
||||||
|
And the object "Chunk" has a vertex at "9,-1,-1"
|
||||||
|
And the object "Chunk" has a vertex at "17,4,-1"
|
||||||
|
|
||||||
|
Scenario: Link IFC - from an empty Blender session - automatic false origin mode - two different false origins and project norths - grid north is up because we start with geolocation.ifc
|
||||||
Given an empty Blender session
|
Given an empty Blender session
|
||||||
# Not currently possible via UI
|
# Not currently possible via UI
|
||||||
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
||||||
When I link IFC project from "{cwd}/test/files/geolocation.ifc"
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)"
|
||||||
And I link IFC project from "{cwd}/test/files/geolocation-mapconversion-angle.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-mapconversion-angle.ifc', use_cache=False)"
|
||||||
Then the object "Col:IfcProject/geolocation.ifc:Chunk" exists
|
Then the object "Col:IfcProject/geolocation.ifc:Chunk" exists
|
||||||
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" exists
|
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" exists
|
||||||
And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-11,-2,0"
|
And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-11,-2,0"
|
||||||
@@ -746,13 +769,13 @@ Scenario: Link IFC - automatic false origin mode - two different false origins a
|
|||||||
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "9.294,-9.366,0"
|
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "9.294,-9.366,0"
|
||||||
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "18.722,-9.036,0"
|
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "18.722,-9.036,0"
|
||||||
|
|
||||||
Scenario: Link IFC - automatic false origin mode - two different false origins and project norths - project north is up because we start with geolocation-mapconversion-angle.ifc
|
Scenario: Link IFC - from an empty Blender session - automatic false origin mode - two different false origins and project norths - project north is up because we start with geolocation-mapconversion-angle.ifc
|
||||||
Given an empty Blender session
|
Given an empty Blender session
|
||||||
# Not currently possible via UI
|
# Not currently possible via UI
|
||||||
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
||||||
When I link IFC project from "{cwd}/test/files/geolocation-mapconversion-angle.ifc"
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-mapconversion-angle.ifc', use_cache=False)"
|
||||||
And I link IFC project from "{cwd}/test/files/geolocation.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation.ifc', use_cache=False)"
|
||||||
Then the object "Col:IfcProject/geolocation.ifc:Chunk" exists
|
Then the object "Col:IfcProject/geolocation.ifc:Chunk" exists
|
||||||
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" exists
|
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" exists
|
||||||
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "-11,-2,0"
|
And the object "Col:IfcProject/geolocation-mapconversion-angle.ifc:Chunk" has a vertex at "-11,-2,0"
|
||||||
@@ -762,14 +785,14 @@ Scenario: Link IFC - automatic false origin mode - two different false origins a
|
|||||||
And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-17.696,-7.866,0"
|
And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-17.696,-7.866,0"
|
||||||
And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-13.268,0.464,0"
|
And the object "Col:IfcProject/geolocation.ifc:Chunk" has a vertex at "-13.268,0.464,0"
|
||||||
|
|
||||||
Scenario: Link IFC - automatic false origin mode - three identical false origins but different project and map units
|
Scenario: Link IFC - from an empty Blender session - automatic false origin mode - three identical false origins but different project and map units
|
||||||
Given an empty Blender session
|
Given an empty Blender session
|
||||||
# Not currently possible via UI
|
# Not currently possible via UI
|
||||||
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
And I set "scene.BIMProjectProperties.distance_limit" to "5"
|
||||||
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
And I set "scene.BIMProjectProperties.false_origin_mode" to "AUTOMATIC"
|
||||||
When I link IFC project from "{cwd}/test/files/geolocation-unit1.ifc"
|
When I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit1.ifc', use_cache=False)"
|
||||||
And I link IFC project from "{cwd}/test/files/geolocation-unit2.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit2.ifc', use_cache=False)"
|
||||||
And I link IFC project from "{cwd}/test/files/geolocation-unit3.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/geolocation-unit3.ifc', use_cache=False)"
|
||||||
Then the object "Col:IfcProject/geolocation-unit1.ifc:Chunk" exists
|
Then the object "Col:IfcProject/geolocation-unit1.ifc:Chunk" exists
|
||||||
And the object "Col:IfcProject/geolocation-unit2.ifc:Chunk" exists
|
And the object "Col:IfcProject/geolocation-unit2.ifc:Chunk" exists
|
||||||
And the object "Col:IfcProject/geolocation-unit3.ifc:Chunk" exists
|
And the object "Col:IfcProject/geolocation-unit3.ifc:Chunk" exists
|
||||||
@@ -779,54 +802,54 @@ Scenario: Link IFC - automatic false origin mode - three identical false origins
|
|||||||
|
|
||||||
Scenario: Toggle link visibility - wireframe mode
|
Scenario: Toggle link visibility - wireframe mode
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
And I link IFC project from "{cwd}/test/files/basic.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')"
|
When I press "bim.toggle_link_visibility(link_index=0, mode='WIREFRAME')"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "True"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "True"
|
||||||
And the object "Chunk" should display as "WIRE"
|
And the object "Chunk" should display as "WIRE"
|
||||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='WIREFRAME')"
|
When I press "bim.toggle_link_visibility(link_index=0, mode='WIREFRAME')"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "False"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_wireframe" is "False"
|
||||||
And the object "Chunk" should display as "TEXTURED"
|
And the object "Chunk" should display as "TEXTURED"
|
||||||
|
|
||||||
Scenario: Toggle link selectability
|
Scenario: Toggle link selectability
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
And I link IFC project from "{cwd}/test/files/basic.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||||
When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')"
|
When I press "bim.toggle_link_selectability(link_index=0)"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "False"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "False"
|
||||||
And the collection "IfcProject/basic.ifc" is unselectable
|
And the collection "IfcProject/basic.ifc" is unselectable
|
||||||
When I press "bim.toggle_link_selectability(link='{cwd}/test/files/basic.ifc')"
|
When I press "bim.toggle_link_selectability(link_index=0)"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "True"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_selectable" is "True"
|
||||||
And the collection "IfcProject/basic.ifc" is selectable
|
And the collection "IfcProject/basic.ifc" is selectable
|
||||||
|
|
||||||
Scenario: Toggle link visibility - visible mode
|
Scenario: Toggle link visibility - visible mode
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
And I link IFC project from "{cwd}/test/files/basic.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')"
|
When I press "bim.toggle_link_visibility(link_index=0, mode='VISIBLE')"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "True"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "True"
|
||||||
And the object "IfcProject/basic.ifc" is not visible
|
And the object "IfcProject/basic.ifc" is not visible
|
||||||
When I press "bim.toggle_link_visibility(link='{cwd}/test/files/basic.ifc', mode='VISIBLE')"
|
When I press "bim.toggle_link_visibility(link_index=0, mode='VISIBLE')"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "False"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_hidden" is "False"
|
||||||
And the object "IfcProject/basic.ifc" is visible
|
And the object "IfcProject/basic.ifc" is visible
|
||||||
|
|
||||||
Scenario: Unload link
|
Scenario: Unload link
|
||||||
Given an empty Blender session
|
Given an empty Blender session
|
||||||
And I link IFC project from "{cwd}/test/files/basic.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||||
When I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
|
When I press "bim.unload_link(link_index=0)"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "False"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "False"
|
||||||
And the collection "IfcProject/basic.ifc" does not exist
|
And the collection "IfcProject/basic.ifc" does not exist
|
||||||
|
|
||||||
Scenario: Load link
|
Scenario: Load link
|
||||||
Given an empty Blender session
|
Given an empty Blender session
|
||||||
And I link IFC project from "{cwd}/test/files/basic.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||||
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
|
And I press "bim.unload_link(link_index=0)"
|
||||||
When I press "bim.load_link(filepath='{cwd}/test/files/basic.ifc')"
|
When I press "bim.load_link(link_index=0)"
|
||||||
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
|
Then "scene.BIMProjectProperties.links['{cwd}/test/files/basic.ifc'].is_loaded" is "True"
|
||||||
And the object "IfcProject/basic.ifc" exists
|
And the object "IfcProject/basic.ifc" exists
|
||||||
|
|
||||||
Scenario: Unlink IFC
|
Scenario: Unlink IFC
|
||||||
Given an empty Blender session
|
Given an empty Blender session
|
||||||
And I link IFC project from "{cwd}/test/files/basic.ifc"
|
And I press "bim.link_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
||||||
And I press "bim.unload_link(filepath='{cwd}/test/files/basic.ifc')"
|
And I press "bim.unload_link(link_index=0)"
|
||||||
When I press "bim.unlink_ifc(filepath='{cwd}/test/files/basic.ifc')"
|
When I press "bim.unlink_ifc(link_index=0)"
|
||||||
Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.ifc')" is "None"
|
Then "scene.BIMProjectProperties.links.get('{cwd}/test/files/basic.ifc')" is "None"
|
||||||
And "scene.collection.children.get('IfcProject/basic.ifc')" is "None"
|
And "scene.collection.children.get('IfcProject/basic.ifc')" is "None"
|
||||||
And the object "Chunk" does not exist
|
And the object "Chunk" does not exist
|
||||||
|
|||||||
@@ -70,10 +70,7 @@ Can be useful for debugging, but has caveats - can't use ``wm.read_homefile``
|
|||||||
as resets the ``bpy.context`` and some it's members become `None`.
|
as resets the ``bpy.context`` and some it's members become `None`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TMP = Path.cwd() / "test/files/temp"
|
TMP = Path(f"{variables['cwd']}/test/files/temp")
|
||||||
TEST_FILES_DIR = Path.cwd() / "test/files"
|
|
||||||
|
|
||||||
CLEAN_LINKED_FILES_CACHE = False
|
|
||||||
|
|
||||||
EPSET_DRAWING = Path.cwd() / "bonsai/bim/data/pset/EPset_Drawing.ifc"
|
EPSET_DRAWING = Path.cwd() / "bonsai/bim/data/pset/EPset_Drawing.ifc"
|
||||||
EPSET_DRAWING_BYTES = EPSET_DRAWING.read_bytes()
|
EPSET_DRAWING_BYTES = EPSET_DRAWING.read_bytes()
|
||||||
@@ -283,6 +280,8 @@ def create_ui_name_cache():
|
|||||||
try:
|
try:
|
||||||
panel_type = getattr(bpy.types, bl_idname)
|
panel_type = getattr(bpy.types, bl_idname)
|
||||||
if panel_type.bl_rna.base.name == "Panel":
|
if panel_type.bl_rna.base.name == "Panel":
|
||||||
|
if "_tab_" in panel_type.bl_idname:
|
||||||
|
continue # Tab panels are just groups and not relevant in testing
|
||||||
ui_name_cache[panel_type.bl_label] = panel_type.bl_idname
|
ui_name_cache[panel_type.bl_label] = panel_type.bl_idname
|
||||||
elif panel_type.bl_rna.base.name == "Operator":
|
elif panel_type.bl_rna.base.name == "Operator":
|
||||||
ui_name_cache[panel_type.bl_label] = bl_idname
|
ui_name_cache[panel_type.bl_label] = bl_idname
|
||||||
@@ -311,25 +310,6 @@ def vectors_are_equal(v1, v2):
|
|||||||
return all(is_x(v1[i], v2[i]) for i in range(len(v1)))
|
return all(is_x(v1[i], v2[i]) for i in range(len(v1)))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function", autouse=True)
|
|
||||||
def run_for_each_test() -> Generator[None]:
|
|
||||||
# Code before this runs before each test
|
|
||||||
yield
|
|
||||||
# Code after this runs after each test
|
|
||||||
|
|
||||||
global CLEAN_LINKED_FILES_CACHE
|
|
||||||
if CLEAN_LINKED_FILES_CACHE:
|
|
||||||
for filepath in TEST_FILES_DIR.glob("*.ifc.cache.*"):
|
|
||||||
filepath.unlink()
|
|
||||||
CLEAN_LINKED_FILES_CACHE = False
|
|
||||||
|
|
||||||
# pset_template tests are editing EPset_Drawing.ifc, so we need to restore it.
|
|
||||||
global RELOAD_EPSET_DRAWING
|
|
||||||
if RELOAD_EPSET_DRAWING:
|
|
||||||
EPSET_DRAWING.write_bytes(EPSET_DRAWING_BYTES)
|
|
||||||
RELOAD_EPSET_DRAWING = False
|
|
||||||
|
|
||||||
|
|
||||||
@given("an untestable scenario")
|
@given("an untestable scenario")
|
||||||
def an_untestable_scenario():
|
def an_untestable_scenario():
|
||||||
pass
|
pass
|
||||||
@@ -384,16 +364,6 @@ def saving_ifc_project() -> None:
|
|||||||
tool.Project.save_test_project()
|
tool.Project.save_test_project()
|
||||||
|
|
||||||
|
|
||||||
@given(parsers.parse('I link IFC project from "{filepath}"'))
|
|
||||||
@when(parsers.parse('I link IFC project from "{filepath}"'))
|
|
||||||
@then(parsers.parse('I link IFC project from "{filepath}"'))
|
|
||||||
def i_link_ifc_project_from_filepath(filepath: str) -> None:
|
|
||||||
global CLEAN_LINKED_FILES_CACHE
|
|
||||||
filepath = replace_variables(filepath)
|
|
||||||
CLEAN_LINKED_FILES_CACHE = True
|
|
||||||
bpy.ops.bim.link_ifc(filepath=filepath, use_cache=False)
|
|
||||||
|
|
||||||
|
|
||||||
@given("the Brickschema is stubbed")
|
@given("the Brickschema is stubbed")
|
||||||
def the_brickschema_is_stubbed():
|
def the_brickschema_is_stubbed():
|
||||||
# This makes things run faster since we don't need to load the entire brick schema
|
# This makes things run faster since we don't need to load the entire brick schema
|
||||||
@@ -1587,10 +1557,19 @@ def the_object_name_has_a_vertex_at_location(name, location):
|
|||||||
is_pass = False
|
is_pass = False
|
||||||
target = Vector([float(co) for co in location.split(",")])
|
target = Vector([float(co) for co in location.split(",")])
|
||||||
verts = []
|
verts = []
|
||||||
for v in obj.data.vertices:
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
verts.append(obj.matrix_world @ v.co)
|
obj_eval = obj.evaluated_get(depsgraph)
|
||||||
if (verts[-1] - target).length < 0.001:
|
mesh = obj_eval.to_mesh(preserve_all_data_layers=False, depsgraph=depsgraph)
|
||||||
is_pass = True
|
try:
|
||||||
|
for inst in depsgraph.object_instances:
|
||||||
|
if inst.object.original is obj:
|
||||||
|
mw = inst.matrix_world
|
||||||
|
for i, v in enumerate(mesh.vertices):
|
||||||
|
verts.append(mw @ v.co)
|
||||||
|
if (verts[-1] - target).length < 0.001:
|
||||||
|
is_pass = True
|
||||||
|
finally:
|
||||||
|
obj_eval.to_mesh_clear()
|
||||||
assert is_pass, f"No verts found at {location}: {verts}"
|
assert is_pass, f"No verts found at {location}: {verts}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ import pytest
|
|||||||
|
|
||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
|
import tempfile
|
||||||
from bonsai.tool.blender import Blender as subject
|
from bonsai.tool.blender import Blender as subject
|
||||||
from test.bim.bootstrap import NewFile
|
from test.bim.bootstrap import NewFile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import bpy.stub_internal.rna_enums as rna_enums
|
import bpy.stub_internal.rna_enums as rna_enums
|
||||||
@@ -110,3 +112,35 @@ class TestBlenderErrorMessageExtraction(NewFile):
|
|||||||
assert error_reports == []
|
assert error_reports == []
|
||||||
|
|
||||||
bpy.utils.unregister_class(OBJECT_OT_test_fail_operator)
|
bpy.utils.unregister_class(OBJECT_OT_test_fail_operator)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetSelectedFiles(NewFile):
|
||||||
|
def test_get_a_single_file(self) -> None:
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
file = type("", (object,), {"name": f.name})()
|
||||||
|
assert subject.get_selected_files(Path(f.name).parent, [file]) == [f.name]
|
||||||
|
|
||||||
|
def test_get_multiple_files(self) -> None:
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
with tempfile.NamedTemporaryFile() as g:
|
||||||
|
file = type("", (object,), {"name": f.name})()
|
||||||
|
file2 = type("", (object,), {"name": g.name})()
|
||||||
|
assert subject.get_selected_files(Path(f.name).parent, [file, file2]) == [f.name, g.name]
|
||||||
|
|
||||||
|
def test_exclude_directories(self) -> None:
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
file = type("", (object,), {"name": f.name})()
|
||||||
|
directory = type("", (object,), {"name": d})()
|
||||||
|
assert subject.get_selected_files(Path(f.name).parent, [file, directory]) == [f.name]
|
||||||
|
|
||||||
|
def test_get_relative_paths(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
base_path = Path(tmp_dir)
|
||||||
|
with tempfile.NamedTemporaryFile(dir=tmp_dir, suffix=".ifc") as f:
|
||||||
|
tool.Ifc.set_path(str(f.name))
|
||||||
|
with tempfile.NamedTemporaryFile(dir=tmp_dir) as g:
|
||||||
|
file = type("", (object,), {"name": g.name})
|
||||||
|
assert subject.get_selected_files(Path(g.name).parent, [file], use_relative_path=True) == [
|
||||||
|
Path(g.name).name
|
||||||
|
]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import xml.etree.ElementTree as ET
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import pytest
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api.drawing
|
import ifcopenshell.api.drawing
|
||||||
import ifcopenshell.api.group
|
import ifcopenshell.api.group
|
||||||
@@ -31,6 +32,7 @@ import ifcopenshell.util.element
|
|||||||
import mathutils
|
import mathutils
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
from ifcopenshell.util.shape_builder import ShapeBuilder
|
||||||
|
|
||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
@@ -150,6 +152,34 @@ class TestDisableEditingSheets(NewFile):
|
|||||||
assert props.is_editing_sheets == False
|
assert props.is_editing_sheets == False
|
||||||
|
|
||||||
|
|
||||||
|
class TestEditTextLiterals(NewFile):
|
||||||
|
def test_run(self):
|
||||||
|
ifc = ifcopenshell.file()
|
||||||
|
tool.Ifc.set(ifc)
|
||||||
|
obj = bpy.data.objects.new("Object", None)
|
||||||
|
element = ifc.createIfcAnnotation()
|
||||||
|
element.Representation = ifc.createIfcProductDefinitionShape()
|
||||||
|
context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation")
|
||||||
|
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left")
|
||||||
|
builder = ShapeBuilder(tool.Ifc.get())
|
||||||
|
polyline = builder.polyline([(0.,0.,0.), (1.,0.,0.)])
|
||||||
|
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item, polyline])
|
||||||
|
element.Representation.Representations = [representation]
|
||||||
|
tool.Ifc.link(element, obj)
|
||||||
|
literal_attributes = [
|
||||||
|
{
|
||||||
|
"Literal": "Foo",
|
||||||
|
"Path": "RIGHT",
|
||||||
|
"BoxAlignment": "bottom-left",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
subject.edit_text_literals(obj, literal_attributes)
|
||||||
|
assert len(ifc.by_type("IfcTextLiteralWithExtent")) == 1
|
||||||
|
literal = ifc.by_type("IfcTextLiteralWithExtent")[0]
|
||||||
|
assert literal in representation.Items
|
||||||
|
assert literal.Literal == "Foo"
|
||||||
|
|
||||||
|
|
||||||
class TestDisableEditingText(NewFile):
|
class TestDisableEditingText(NewFile):
|
||||||
def test_run(self):
|
def test_run(self):
|
||||||
obj = bpy.data.objects.new("Object", None)
|
obj = bpy.data.objects.new("Object", None)
|
||||||
@@ -908,7 +938,7 @@ class TestAddReferenceImage(NewFile):
|
|||||||
|
|
||||||
obj = bpy.data.objects["IfcAnnotation/image"]
|
obj = bpy.data.objects["IfcAnnotation/image"]
|
||||||
assert obj is not None
|
assert obj is not None
|
||||||
assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0)))
|
assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((1.0, 0.565, 0.0)))
|
||||||
|
|
||||||
material = obj.active_material
|
material = obj.active_material
|
||||||
assert material
|
assert material
|
||||||
@@ -928,71 +958,3 @@ class TestAddReferenceImage(NewFile):
|
|||||||
|
|
||||||
uv_node = material_nodes["Texture Coordinate"]
|
uv_node = material_nodes["Texture Coordinate"]
|
||||||
assert len(uv_node.outputs["Generated"].links[:]) == 1
|
assert len(uv_node.outputs["Generated"].links[:]) == 1
|
||||||
|
|
||||||
|
|
||||||
class TestAddReference(NewFile):
|
|
||||||
def test_add_single_reference(self):
|
|
||||||
"""Test adding a single reference file (backward compatibility)"""
|
|
||||||
bpy.ops.bim.create_project()
|
|
||||||
ifc_path = Path("test/files/temp/test.ifc").absolute()
|
|
||||||
bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True)
|
|
||||||
|
|
||||||
# Create a temporary SVG file
|
|
||||||
svg_path = Path("test/files/temp/reference.svg").absolute()
|
|
||||||
svg_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with open(svg_path, "w") as f:
|
|
||||||
f.write('<svg xmlns="http://www.w3.org/2000/svg"></svg>')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Add single reference
|
|
||||||
bpy.ops.bim.add_reference(filepath=str(svg_path))
|
|
||||||
|
|
||||||
# Verify reference was added
|
|
||||||
ifc = tool.Ifc.get()
|
|
||||||
references = [doc for doc in ifc.by_type("IfcDocumentInformation") if doc.Scope == "REFERENCE"]
|
|
||||||
assert len(references) == 1
|
|
||||||
assert references[0].Name == "reference"
|
|
||||||
finally:
|
|
||||||
# Cleanup
|
|
||||||
if svg_path.exists():
|
|
||||||
svg_path.unlink()
|
|
||||||
|
|
||||||
def test_add_multiple_references(self):
|
|
||||||
"""Test adding multiple reference files at once"""
|
|
||||||
bpy.ops.bim.create_project()
|
|
||||||
ifc_path = Path("test/files/temp/test.ifc").absolute()
|
|
||||||
bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True)
|
|
||||||
|
|
||||||
# Create temporary SVG files
|
|
||||||
temp_dir = Path("test/files/temp").absolute()
|
|
||||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
svg_files = []
|
|
||||||
for i in range(3):
|
|
||||||
svg_path = temp_dir / f"reference_{i}.svg"
|
|
||||||
with open(svg_path, "w") as f:
|
|
||||||
f.write('<svg xmlns="http://www.w3.org/2000/svg"></svg>')
|
|
||||||
svg_files.append(svg_path)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Test by directly calling core.add_document multiple times
|
|
||||||
# (simulating what the operator does with multiple files)
|
|
||||||
ifc = tool.Ifc.get()
|
|
||||||
for svg_file in svg_files:
|
|
||||||
uri = tool.Ifc.get_uri(str(svg_file), use_relative_path=True)
|
|
||||||
from bonsai.bim import core
|
|
||||||
|
|
||||||
core.drawing.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=uri)
|
|
||||||
|
|
||||||
# Verify all references were added
|
|
||||||
references = [doc for doc in ifc.by_type("IfcDocumentInformation") if doc.Scope == "REFERENCE"]
|
|
||||||
assert len(references) == 3
|
|
||||||
|
|
||||||
reference_names = {ref.Name for ref in references}
|
|
||||||
expected_names = {f"reference_{i}" for i in range(3)}
|
|
||||||
assert reference_names == expected_names
|
|
||||||
finally:
|
|
||||||
# Cleanup
|
|
||||||
for svg_file in svg_files:
|
|
||||||
if svg_file.exists():
|
|
||||||
svg_file.unlink()
|
|
||||||
|
|||||||
@@ -16,9 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import json
|
||||||
import contextlib
|
import contextlib
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import numpy as np
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
@@ -263,7 +266,7 @@ class TestLoadLinkedModels(NewFile):
|
|||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
||||||
document = ifcopenshell.api.document.add_information(ifc)
|
document = ifcopenshell.api.document.add_information(ifc)
|
||||||
document.Name = "BBIM_Linked_Models"
|
document.Name = "X"
|
||||||
tool.Ifc.set(ifc)
|
tool.Ifc.set(ifc)
|
||||||
subject.load_linked_models_from_ifc()
|
subject.load_linked_models_from_ifc()
|
||||||
assert len(props.links) == 0
|
assert len(props.links) == 0
|
||||||
@@ -273,86 +276,81 @@ class TestLoadLinkedModels(NewFile):
|
|||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
||||||
document = ifcopenshell.api.document.add_information(ifc)
|
document = ifcopenshell.api.document.add_information(ifc)
|
||||||
document.Name = "BBIM_Linked_Models"
|
document.Scope = "LINKED_MODEL"
|
||||||
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
||||||
linked_model_path = "test.ifc"
|
reference.Location = "test.ifc"
|
||||||
reference.Location = linked_model_path
|
reference.Identification = ""
|
||||||
|
reference2 = ifcopenshell.api.document.add_reference(ifc, document)
|
||||||
|
reference2.Location = "test2.ifc"
|
||||||
|
reference2.Identification = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16"
|
||||||
tool.Ifc.set(ifc)
|
tool.Ifc.set(ifc)
|
||||||
subject.load_linked_models_from_ifc()
|
subject.load_linked_models_from_ifc()
|
||||||
assert len(props.links) == 1
|
assert len(props.links) == 2
|
||||||
assert props.links[0].name == linked_model_path
|
assert props.links[0].name == "test.ifc"
|
||||||
|
assert props.links[0].ifc_definition_id == reference.id()
|
||||||
|
assert props.links[0].has_transformation is False
|
||||||
|
assert props.links[1].name == "test2.ifc"
|
||||||
|
assert props.links[1].ifc_definition_id == reference2.id()
|
||||||
|
assert props.links[1].has_transformation is True
|
||||||
|
|
||||||
|
|
||||||
class TestSaveLinkedModelsToIfc(NewFile):
|
class TestCalculateLinkMatrix(NewFile):
|
||||||
def test_save_linked_models_to_ifc_no_links(self):
|
def test_linking_a_model_without_an_offset_to_our_session_with_no_offset(self):
|
||||||
ifc = ifcopenshell.file()
|
|
||||||
tool.Ifc.set(ifc)
|
|
||||||
subject.save_linked_models_to_ifc()
|
|
||||||
assert len(ifc.by_type("IfcDocumentInformation")) == 0
|
|
||||||
assert len(ifc.by_type("IfcDocumentReference")) == 0
|
|
||||||
|
|
||||||
def test_save_linked_models_to_ifc_paths_to_add(self):
|
|
||||||
ifc = ifcopenshell.file()
|
|
||||||
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
|
||||||
props = tool.Project.get_project_props()
|
props = tool.Project.get_project_props()
|
||||||
link = props.links.add()
|
gprops = tool.Georeference.get_georeference_props()
|
||||||
linked_model_path = "test.ifc"
|
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
|
||||||
link.name = linked_model_path
|
link = props.links.add()
|
||||||
tool.Ifc.set(ifc)
|
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||||
subject.save_linked_models_to_ifc()
|
json.dump({"model_project_north": "0", "model_origin_si": "0,0,0"}, tmp)
|
||||||
assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1
|
tmp.flush()
|
||||||
assert documents[0].Name == "BBIM_Linked_Models"
|
gprops.model_project_north = "0"
|
||||||
assert len(references := ifc.by_type("IfcDocumentReference")) == 1
|
gprops.model_origin_si = "0,0,0"
|
||||||
assert references[0].Location == linked_model_path
|
assert np.allclose(subject.calculate_link_matrix(link), np.eye(4))
|
||||||
|
|
||||||
def test_save_linked_models_to_ifc_already_created_references(self):
|
def test_linking_an_offset_model_to_our_session_with_no_offset(self):
|
||||||
ifc = ifcopenshell.file()
|
props = tool.Project.get_project_props()
|
||||||
links = tool.Project.get_project_props().links
|
gprops = tool.Georeference.get_georeference_props()
|
||||||
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
|
||||||
|
link = props.links.add()
|
||||||
|
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||||
|
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
|
||||||
|
tmp.flush()
|
||||||
|
gprops.model_project_north = "0"
|
||||||
|
gprops.model_origin_si = "0,0,0"
|
||||||
|
m = np.eye(4)
|
||||||
|
m[0][3] = 5
|
||||||
|
assert np.allclose(subject.calculate_link_matrix(link), m)
|
||||||
|
|
||||||
document = ifcopenshell.api.document.add_information(ifc)
|
def test_linking_an_offset_model_to_our_session_with_offset(self):
|
||||||
document.Name = "BBIM_Linked_Models"
|
props = tool.Project.get_project_props()
|
||||||
document_id = document.id()
|
gprops = tool.Georeference.get_georeference_props()
|
||||||
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
|
||||||
linked_model_path = "test.ifc"
|
link = props.links.add()
|
||||||
reference.Location = linked_model_path
|
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||||
reference_id = reference.id()
|
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
|
||||||
|
tmp.flush()
|
||||||
|
gprops.model_project_north = "0"
|
||||||
|
gprops.model_origin_si = "2,0,0"
|
||||||
|
m = np.eye(4)
|
||||||
|
m[0][3] = 3
|
||||||
|
assert np.allclose(subject.calculate_link_matrix(link), m)
|
||||||
|
|
||||||
link = links.add()
|
def test_linking_an_offset_model_to_our_session_with_offset_and_transformation(self):
|
||||||
linked_model_path = "test.ifc"
|
props = tool.Project.get_project_props()
|
||||||
link.name = linked_model_path
|
gprops = tool.Georeference.get_georeference_props()
|
||||||
tool.Ifc.set(ifc)
|
with NamedTemporaryFile(suffix=".ifc.cache.json", mode="w", delete=True) as tmp:
|
||||||
subject.save_linked_models_to_ifc()
|
link = props.links.add()
|
||||||
|
link.filepath = tmp.name.replace(".ifc.cache.json", ".ifc")
|
||||||
# Information and references to stay intact.
|
transformation = np.eye(4)
|
||||||
assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1
|
transformation[0][3] = 4
|
||||||
assert documents[0].id() == document_id
|
link.transformation = ",".join(map(str, transformation.reshape(-1)))
|
||||||
assert documents[0].Name == "BBIM_Linked_Models"
|
json.dump({"model_project_north": "0", "model_origin_si": "5,0,0"}, tmp)
|
||||||
assert len(references := ifc.by_type("IfcDocumentReference")) == 1
|
tmp.flush()
|
||||||
assert references[0].id() == reference_id
|
gprops.model_project_north = "0"
|
||||||
assert references[0].Location == linked_model_path
|
gprops.model_origin_si = "2,0,0"
|
||||||
|
m = np.eye(4)
|
||||||
def test_save_linked_models_to_ifc_references_to_remove(self):
|
m[0][3] = 7
|
||||||
ifc = ifcopenshell.file()
|
assert np.allclose(subject.calculate_link_matrix(link), m)
|
||||||
links = tool.Project.get_project_props().links
|
|
||||||
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
|
|
||||||
|
|
||||||
document = ifcopenshell.api.document.add_information(ifc)
|
|
||||||
document.Name = "BBIM_Linked_Models"
|
|
||||||
document_id = document.id()
|
|
||||||
reference = ifcopenshell.api.document.add_reference(ifc, document)
|
|
||||||
linked_model_path = "test.ifc"
|
|
||||||
reference.Location = linked_model_path
|
|
||||||
|
|
||||||
tool.Ifc.set(ifc)
|
|
||||||
subject.save_linked_models_to_ifc()
|
|
||||||
links.clear()
|
|
||||||
|
|
||||||
# Remove reference for removed link.
|
|
||||||
assert len(documents := ifc.by_type("IfcDocumentInformation")) == 1
|
|
||||||
assert documents[0].id() == document_id
|
|
||||||
assert documents[0].Name == "BBIM_Linked_Models"
|
|
||||||
assert len(ifc.by_type("IfcDocumentReference")) == 0
|
|
||||||
|
|
||||||
|
|
||||||
class TestLoadingIfcSqlite(NewFile):
|
class TestLoadingIfcSqlite(NewFile):
|
||||||
|
|||||||
@@ -61,33 +61,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
|
|||||||
|
|
||||||
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(true) * length_unit_);
|
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(true) * length_unit_);
|
||||||
|
|
||||||
// Corresponds to the profile X, Y directions (hopefully).
|
auto linear_placement = taxonomy::cast<taxonomy::matrix4>(map(csp));
|
||||||
Eigen::Vector3d po(
|
profile_offsets.push_back(linear_placement->ccomponents().block<3, 1>(0, 3));
|
||||||
pbde->OffsetLateral().get_value_or(0.),
|
boost::optional<Eigen::Matrix3d> rot(linear_placement->ccomponents().block<3,3>(0,0));
|
||||||
// @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane
|
|
||||||
pbde->OffsetVertical().get_value_or(0.),
|
|
||||||
0.
|
|
||||||
);
|
|
||||||
|
|
||||||
profile_offsets.push_back(po);
|
|
||||||
|
|
||||||
boost::optional<Eigen::Matrix3d> rot;
|
|
||||||
if (csp->Axis() && csp->RefDirection()) {
|
|
||||||
rot = taxonomy::matrix4(
|
|
||||||
Eigen::Vector3d(0, 0, 0),
|
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents(),
|
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents()).ccomponents().block<3,3>(0,0);
|
|
||||||
} else if (csp->Axis()) {
|
|
||||||
rot = taxonomy::matrix4(
|
|
||||||
Eigen::Vector3d(0, 0, 0),
|
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
|
|
||||||
} else if (csp->RefDirection()) {
|
|
||||||
rot = taxonomy::matrix4(
|
|
||||||
Eigen::Vector3d(0, 0, 0),
|
|
||||||
Eigen::Vector3d(0, 0, 1),
|
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents()
|
|
||||||
).ccomponents().block<3, 3>(0, 0);
|
|
||||||
}
|
|
||||||
profile_rotations.push_back(rot);
|
profile_rotations.push_back(rot);
|
||||||
}
|
}
|
||||||
if (faces.size() != profile_offsets.size()) {
|
if (faces.size() != profile_offsets.size()) {
|
||||||
|
|||||||
@@ -63,35 +63,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
|
|||||||
|
|
||||||
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(true) * length_unit_);
|
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(true) * length_unit_);
|
||||||
|
|
||||||
// Corresponds to the profile X, Y directions (hopefully).
|
auto linear_placement = taxonomy::cast<taxonomy::matrix4>(map(csp));
|
||||||
Eigen::Vector3d po(
|
profile_offsets.push_back(linear_placement->ccomponents().block<3, 1>(0, 3));
|
||||||
pbde->OffsetLateral().get_value_or(0.),
|
boost::optional<Eigen::Matrix3d> rot(linear_placement->ccomponents().block<3, 3>(0, 0));
|
||||||
// @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane
|
profile_rotations.push_back(rot);
|
||||||
pbde->OffsetVertical().get_value_or(0.),
|
|
||||||
0.
|
|
||||||
);
|
|
||||||
|
|
||||||
profile_offsets.push_back(po);
|
|
||||||
|
|
||||||
boost::optional<Eigen::Matrix3d> rot;
|
|
||||||
if (csp->Axis() && csp->RefDirection()) {
|
|
||||||
rot = taxonomy::matrix4(
|
|
||||||
Eigen::Vector3d(0, 0, 0),
|
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents(),
|
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
|
|
||||||
} else if (csp->Axis()) {
|
|
||||||
rot = taxonomy::matrix4(
|
|
||||||
Eigen::Vector3d(0, 0, 0),
|
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0);
|
|
||||||
} else if (csp->RefDirection()) {
|
|
||||||
rot = taxonomy::matrix4(
|
|
||||||
Eigen::Vector3d(0, 0, 0),
|
|
||||||
Eigen::Vector3d(0, 0, 1),
|
|
||||||
taxonomy::cast<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents())
|
|
||||||
.ccomponents()
|
|
||||||
.block<3, 3>(0, 0);
|
|
||||||
}
|
|
||||||
profile_rotations.push_back(rot);
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
from decimal import ROUND_HALF_UP, Decimal
|
from decimal import ROUND_HALF_UP, Decimal
|
||||||
from typing import NamedTuple, Optional, Union
|
from typing import NamedTuple, Optional, Union, Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -268,6 +268,15 @@ def get_helmert_transformation_parameters(ifc_file: ifcopenshell.file) -> Option
|
|||||||
return HelmertTransformation(e, n, h, xaa, xao, scale, factor_x, factor_y, factor_z)
|
return HelmertTransformation(e, n, h, xaa, xao, scale, factor_x, factor_y, factor_z)
|
||||||
|
|
||||||
|
|
||||||
|
def get_crs(ifc_file: ifcopenshell.file) -> dict[str, Any]:
|
||||||
|
"""Get CRS information from an IFC file."""
|
||||||
|
if ifc_file.schema == "IFC2X3":
|
||||||
|
return ifcopenshell.util.element.get_pset(ifc_file.by_type("IfcProject")[0], "ePSet_ProjectedCRS")
|
||||||
|
for context in ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False):
|
||||||
|
if operation := context.HasCoordinateOperation:
|
||||||
|
return operation[0].TargetCRS.get_info()
|
||||||
|
|
||||||
|
|
||||||
def auto_z2e(ifc_file: ifcopenshell.file, z: float, should_return_in_map_units: bool = True) -> float:
|
def auto_z2e(ifc_file: ifcopenshell.file, z: float, should_return_in_map_units: bool = True) -> float:
|
||||||
"""Convert a Z coordinate to an elevation using model georeferencing data
|
"""Convert a Z coordinate to an elevation using model georeferencing data
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user