Merge branch 'v0.8.0' into ifctester-improvements

This commit is contained in:
Sayan Jyoti Das
2026-03-17 19:49:09 +05:30
15 changed files with 253 additions and 110 deletions
+19 -10
View File
@@ -13,6 +13,7 @@ import hashlib
import os import os
import pathlib import pathlib
import re import re
import subprocess
from typing import NoReturn from typing import NoReturn
from urllib import request from urllib import request
@@ -20,7 +21,7 @@ from github import Github
def get_repo_tag_names() -> list[str]: def get_repo_tag_names() -> list[str]:
git_return = os.popen("git tag -l").read() git_return = subprocess.check_output("git tag -l", text=True)
tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name] tag_names = [tag_name for tag_name in git_return.split("\n") if tag_name]
print(f"{len(tag_names)} tag_names found in repo") print(f"{len(tag_names)} tag_names found in repo")
return tag_names return tag_names
@@ -78,6 +79,10 @@ def get_release_zip(tag: str) -> tuple[str, str]:
raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.") raise Exception(f"Couldn't find the release matching '{python_version}' and '{TARGET_OS}' in tag '{tag}'.")
def run(command: str) -> None:
subprocess.check_output(command)
start = datetime.datetime.now() start = datetime.datetime.now()
URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender" URL_CHOCO_PACKAGE = "https://community.chocolatey.org/packages/blender"
@@ -97,7 +102,7 @@ should_release = False
target_release_tag = "" target_release_tag = ""
TARGET_OS = "windows-x64" TARGET_OS = "windows-x64"
git_status = os.popen("git status").read() git_status = subprocess.check_output("git status", text=True)
print(git_status) print(git_status)
for tag_name in get_repo_tag_names(): for tag_name in get_repo_tag_names():
@@ -147,7 +152,7 @@ blenderbim_build_version = target_release_tag.replace("blenderbim-", "")
# url_blenderbim_py3x_win_zip # url_blenderbim_py3x_win_zip
release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag) release_zip_file_name, url_blenderbim_py3x_win_zip = get_release_zip(target_release_tag)
os.popen(f"wget {url_blenderbim_py3x_win_zip} --no-verbose").read() subprocess.check_call(f"wget {url_blenderbim_py3x_win_zip} --no-verbose")
# sha256sum_blenderbim_py310_win_zip # sha256sum_blenderbim_py310_win_zip
sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name) sha256sum_blenderbim_py3x_win_zip = get_file_sha256_hash(release_zip_file_name)
@@ -201,13 +206,13 @@ print("[INFO] inserting dynamic chocolatey package parameters successful")
print("\n_____ build choco.exe with mono") print("\n_____ build choco.exe with mono")
choco_version = "1.1.0" choco_version = "1.1.0"
os.popen(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet").read() run(f"wget https://github.com/chocolatey/choco/archive/refs/tags/{choco_version}.tar.gz --quiet")
os.popen(f"tar -xzf {choco_version}.tar.gz").read() run(f"tar -xzf {choco_version}.tar.gz")
print("choco tar unpack successful") print("choco tar unpack successful")
os.chdir("choco-1.1.0") os.chdir("choco-1.1.0")
os.popen("./build.sh").read() run("./build.sh")
os.popen("cp -r build_output/chocolatey /opt/chocolatey").read() run("cp -r build_output/chocolatey /opt/chocolatey")
os.chdir(BLENDERBIM_DIR) os.chdir(BLENDERBIM_DIR)
if pathlib.Path("/opt/chocolatey/choco.exe").exists(): if pathlib.Path("/opt/chocolatey/choco.exe").exists():
@@ -215,11 +220,15 @@ if pathlib.Path("/opt/chocolatey/choco.exe").exists():
print("\n_____ build choco pack") print("\n_____ build choco pack")
os.popen("mono /opt/chocolatey/choco.exe pack --allow-unofficial").read() run("mono /opt/chocolatey/choco.exe pack --allow-unofficial")
os.popen('mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial').read() run(
'mono /opt/chocolatey/choco.exe setapikey --key="{choco_token}" --source="https://push.chocolatey.org/" --allow-unofficial'
)
print("\n_____ build choco push") print("\n_____ build choco push")
os.popen('mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose').read() run(
'mono /opt/chocolatey/choco.exe push --source="https://push.chocolatey.org/" --key="$CHOCO_TOKEN" --allow-unofficial --verbose'
)
print(f"choco push of version: {target_release_tag} successful!") print(f"choco push of version: {target_release_tag} successful!")
print(f"it took: {datetime.datetime.now() - start}") print(f"it took: {datetime.datetime.now() - start}")
+13
View File
@@ -78,6 +78,17 @@ ignore = [
"UP032", # Replace .format with f-string "UP032", # Replace .format with f-string
] ]
[tool.ty.rules]
# We only use `ty` currently to check for deprecated methods.
all = "ignore"
deprecated = "error"
[tool.ty.src]
exclude = [
"src/ifc2ca/templates",
"src/svgfill/3rdparty",
]
[tool.poe.tasks] [tool.poe.tasks]
ruff-main = "ruff check --extend-exclude nix/build-all.py" ruff-main = "ruff check --extend-exclude nix/build-all.py"
@@ -87,6 +98,8 @@ ruff.sequence = ["ruff-main", "ruff-old"]
black = "black ." black = "black ."
ty = "ty check"
format.sequence = ["black", "ruff-main", "ruff-old"] format.sequence = ["black", "ruff-main", "ruff-old"]
cmake-format = "gersemi . --in-place" cmake-format = "gersemi . --in-place"
+1 -2
View File
@@ -232,8 +232,7 @@ endif
cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl cd build/bonsai/bim/data/brick/ && wget https://github.com/BrickSchema/Brick/releases/download/nightly/Brick.ttl
# Required for hipped roof generation # Required for hipped roof generation
# TODO: Use official repo once https://github.com/prochitecture/bpypolyskel/pull/22 is merged. cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/prochitecture/bpypolyskel" --no-deps -w wheels/
cd build && . env/$(VENV_ACTIVATE) && $(PYTHON) -m pip wheel "git+https://github.com/Andrej730/bpypolyskel.git@pyproject_toml" --no-deps -w wheels/
# folder for executable files # folder for executable files
mkdir -p build/bonsai/libs/bin mkdir -p build/bonsai/libs/bin
+1 -3
View File
@@ -72,9 +72,7 @@ class IfcExporter:
def set_header(self): def set_header(self):
self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file) self.file.header.file_name.name = os.path.basename(self.ifc_export_settings.output_file)
self.file.header.file_name.time_stamp = ( self.file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
datetime.datetime.utcnow().replace(tzinfo=datetime.UTC).astimezone().replace(microsecond=0).isoformat()
)
self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version) self.file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
self.file.header.file_name.originating_system = "{} {}".format( self.file.header.file_name.originating_system = "{} {}".format(
self.get_application_name(), tool.Blender.get_bonsai_version() self.get_application_name(), tool.Blender.get_bonsai_version()
@@ -27,7 +27,6 @@ import ifcopenshell.api.pset
import ifcopenshell.util.element import ifcopenshell.util.element
from bpy.props import ( from bpy.props import (
BoolProperty, BoolProperty,
BoolVectorProperty,
CollectionProperty, CollectionProperty,
EnumProperty, EnumProperty,
FloatProperty, FloatProperty,
@@ -267,6 +267,8 @@ class BIMGeoreferenceProperties(PropertyGroup):
x_axis_ordinate: str x_axis_ordinate: str
x_axis_is_null: bool x_axis_is_null: bool
model_is_georeferenced: bool
model_crs: str
model_origin: str model_origin: str
model_origin_si: str model_origin_si: str
model_project_north: str model_project_north: str
@@ -76,7 +76,7 @@ classes = (
operator.UnlinkIfc, operator.UnlinkIfc,
operator.UnloadLink, operator.UnloadLink,
workspace.ExploreHotkey, workspace.ExploreHotkey,
workspace.GenerateUVMap, operator.GenerateUVMap,
prop.LibraryBreadcrumb, prop.LibraryBreadcrumb,
prop.LibraryElement, prop.LibraryElement,
prop.FilterCategory, prop.FilterCategory,
+198 -50
View File
@@ -178,9 +178,18 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_description = ( bl_description = (
"Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file." "Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file."
) )
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(
append_all: bpy.props.BoolProperty(default=False) default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) ) # pyright: ignore[reportRedeclaration]
append_all: bpy.props.BoolProperty(default=False) # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path", default=False
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filter_glob: str
append_all: bool
use_relative_path: bool
reload_previous_file = False reload_previous_file = False
@@ -558,7 +567,11 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator):
class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator): class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.append_library_element_by_query" bl_idname = "bim.append_library_element_by_query"
bl_label = "Append Library Element By Query" bl_label = "Append Library Element By Query"
query: bpy.props.StringProperty(name="Query")
query: bpy.props.StringProperty(name="Query") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
query: str
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -587,9 +600,16 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
"Append element to the current project.\n\n" "Append element to the current project.\n\n"
"ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)" "ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)"
) )
definition: bpy.props.IntProperty() definition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
prop_index: bpy.props.IntProperty() prop_index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"}) assume_unique_by_name: bpy.props.BoolProperty(
name="Assume Unique By Name", default=True, options={"SKIP_SAVE"}
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
definition: int
prop_index: int
assume_unique_by_name: bool
file: ifcopenshell.file file: ifcopenshell.file
@@ -939,24 +959,28 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_label = "Load Project" bl_label = "Load Project"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Load an existing IFC project" bl_description = "Load an existing IFC project"
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"}) filepath: bpy.props.StringProperty(
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"}) subtype="FILE_PATH", options={"SKIP_SAVE"}
is_advanced: bpy.props.BoolProperty( ) # pyright: ignore[reportRedeclaration]
filter_glob: bpy.props.StringProperty(
default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
is_advanced: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Enable Advanced Mode", name="Enable Advanced Mode",
description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings", description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings",
default=False, default=False,
) )
use_relative_path: bpy.props.BoolProperty( use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Use Relative Path", name="Use Relative Path",
description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved", description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved",
default=False, default=False,
) )
should_start_fresh_session: bpy.props.BoolProperty( should_start_fresh_session: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Should Start Fresh Session", name="Should Start Fresh Session",
description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option", description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option",
default=True, default=True,
) )
import_without_ifc_data: bpy.props.BoolProperty( import_without_ifc_data: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
name="Import Without IFC Data", name="Import Without IFC Data",
description=( description=(
"Import IFC objects as Blender objects without any IFC metadata and authoring capabilities." "Import IFC objects as Blender objects without any IFC metadata and authoring capabilities."
@@ -964,9 +988,20 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
), ),
default=False, default=False,
) )
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) use_detailed_tooltip: bpy.props.BoolProperty(
default=False, options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
filename_ext = ".ifc" filename_ext = ".ifc"
if TYPE_CHECKING:
filepath: str
filter_glob: str
is_advanced: bool
use_relative_path: bool
should_start_fresh_session: bool
import_without_ifc_data: bool
use_detailed_tooltip: bool
@classmethod @classmethod
def description(cls, context, properties): def description(cls, context, properties):
tooltip = cls.bl_description tooltip = cls.bl_description
@@ -1265,7 +1300,10 @@ class ToggleFilterCategories(bpy.types.Operator):
bl_idname = "bim.toggle_filter_categories" bl_idname = "bim.toggle_filter_categories"
bl_label = "Toggle Filter Categories" bl_label = "Toggle Filter Categories"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
should_select: bpy.props.BoolProperty(name="Should Select", default=True) should_select: bpy.props.BoolProperty(name="Should Select", default=True) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
should_select: bool
def execute(self, context): def execute(self, context):
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
@@ -1289,6 +1327,14 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
default=False, default=False,
) )
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
query: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
name="Query",
description=(
"Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n"
"Default query - IfcElement, but excluding IfcProxy, IfcSpatialStructureElement, IfcSpatialElement, IfcFeatureElement."
),
)
filename_ext = ".ifc" filename_ext = ".ifc"
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -1298,20 +1344,25 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
filter_glob: str filter_glob: str
use_relative_path: bool use_relative_path: bool
use_cache: bool use_cache: bool
query: str
def draw(self, context): def draw(self, context):
assert self.layout
pprops = tool.Project.get_project_props() pprops = tool.Project.get_project_props()
row = self.layout.row() row = self.layout.row()
row.prop(self, "use_relative_path") row.prop(self, "use_relative_path")
row = self.layout.row() row = self.layout.row()
row.prop(self, "use_cache") row.prop(self, "use_cache")
row = self.layout.row() row = self.layout.row()
row.prop(pprops, "false_origin_mode") row.label(text="False Origin Mode:")
row = self.layout.row()
row.prop(pprops, "false_origin_mode", text="")
if pprops.false_origin_mode == "MANUAL": if pprops.false_origin_mode == "MANUAL":
row = self.layout.row() row = self.layout.row()
row.prop(pprops, "false_origin") row.prop(pprops, "false_origin")
row = self.layout.row() row = self.layout.row()
row.prop(pprops, "project_north") row.prop(pprops, "project_north")
self.layout.prop(self, "query", placeholder="IfcElement")
def _execute(self, context): def _execute(self, context):
start = time.time() start = time.time()
@@ -1344,7 +1395,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
new.ifc_definition_id = reference.id() new.ifc_definition_id = reference.id()
new.name = filepath new.name = filepath
new.filepath = filepath new.filepath = filepath
bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache) bpy.ops.bim.load_link(link_index=-1, use_cache=self.use_cache, query=self.query)
class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator): class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
@@ -1352,7 +1403,11 @@ class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
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"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def _execute(self, context): def _execute(self, context):
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
@@ -1372,7 +1427,11 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
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"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def _execute(self, context): def _execute(self, context):
link = tool.Project.get_project_props().links[self.link_index] link = tool.Project.get_project_props().links[self.link_index]
@@ -1397,10 +1456,12 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration] use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration]
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING: if TYPE_CHECKING:
link_index: int link_index: int
use_cache: bool use_cache: bool
query: str
def _execute(self, context): def _execute(self, context):
self.link = tool.Project.get_project_props().links[self.link_index] self.link = tool.Project.get_project_props().links[self.link_index]
@@ -1442,8 +1503,20 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
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")
h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5") h5_filepath = self.filepath_.with_suffix(".ifc.cache.h5")
json_filepath = self.filepath_.with_suffix(".ifc.cache.json")
if not self.use_cache and blend_filepath.exists(): def should_clear_cache() -> bool:
if not self.use_cache:
return True
if not blend_filepath.exists():
return False
data = json.loads(json_filepath.read_text())
# Empty 'query' - model loaded without custom query.
# Missing 'query' - model was loaded before custom queries were introduced in Bonsai.
query = data.get("query", "")
return query != self.query
if should_clear_cache():
os.remove(blend_filepath) os.remove(blend_filepath)
if not blend_filepath.exists(): if not blend_filepath.exists():
@@ -1471,7 +1544,7 @@ def run():
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.
try: try:
bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}") bpy.ops.bim.load_linked_project(filepath=r"{str(self.filepath_)}", query={repr(self.query)})
except RuntimeError as e: except RuntimeError as e:
# Operator failed (returned CANCELLED with error report) # Operator failed (returned CANCELLED with error report)
print(f"Failed to load linked project: {{e}}") print(f"Failed to load linked project: {{e}}")
@@ -1557,7 +1630,11 @@ class ReloadLink(bpy.types.Operator):
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"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context): def execute(self, context):
bpy.ops.bim.unload_link(link_index=self.link_index) bpy.ops.bim.unload_link(link_index=self.link_index)
@@ -1569,7 +1646,11 @@ 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_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context): def execute(self, context):
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
@@ -1739,7 +1820,11 @@ class SelectLinkHandle(bpy.types.Operator):
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"
link_index: bpy.props.IntProperty(name="Link Index")
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
link_index: int
def execute(self, context): def execute(self, context):
props = tool.Project.get_project_props() props = tool.Project.get_project_props()
@@ -1797,11 +1882,28 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc" filename_ext = ".ifc"
supported_filexts = (".ifc", ".ifczip", ".ifcjson") supported_filexts = (".ifc", ".ifczip", ".ifcjson")
filter_glob: bpy.props.StringProperty(default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version") default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False) ) # pyright: ignore[reportRedeclaration]
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"}) json_version: bpy.props.EnumProperty(
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version"
) # pyright: ignore[reportRedeclaration]
json_compact: bpy.props.BoolProperty(
name="Export Compact IFCJSON", default=False
) # pyright: ignore[reportRedeclaration]
should_save_as: bpy.props.BoolProperty(
name="Should Save As", default=False, options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path", default=False
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filter_glob: str
json_version: str
json_compact: bool
should_save_as: bool
use_relative_path: bool
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -1951,6 +2053,12 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file." bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
"""See ``bim.link_ifc``."""
if TYPE_CHECKING:
query: str
file: ifcopenshell.file file: ifcopenshell.file
meshes: dict[str, bpy.types.Mesh] meshes: dict[str, bpy.types.Mesh]
# Material names is derived from diffuse as in 'r-g-b-a'. # Material names is derived from diffuse as in 'r-g-b-a'.
@@ -2000,14 +2108,17 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
tool.Loader.settings.context_settings = tool.Loader.create_settings() tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
self.elements = set(self.file.by_type("IfcElement")) if self.query:
if self.file.schema in ("IFC2X3", "IFC4"): self.elements = ifcopenshell.util.selector.filter_elements(self.file, self.query)
self.elements |= set(self.file.by_type("IfcProxy"))
if self.file.schema == "IFC2X3":
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
else: else:
self.elements |= set(self.file.by_type("IfcSpatialElement")) self.elements = set(self.file.by_type("IfcElement"))
self.elements -= set(self.file.by_type("IfcFeatureElement")) if self.file.schema in ("IFC2X3", "IFC4"):
self.elements |= set(self.file.by_type("IfcProxy"))
if self.file.schema == "IFC2X3":
self.elements |= set(self.file.by_type("IfcSpatialStructureElement"))
else:
self.elements |= set(self.file.by_type("IfcSpatialElement"))
self.elements -= set(self.file.by_type("IfcFeatureElement"))
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)
@@ -2032,6 +2143,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
"false_origin_mode": pprops.false_origin_mode, "false_origin_mode": pprops.false_origin_mode,
"false_origin": pprops.false_origin, "false_origin": pprops.false_origin,
"project_north": pprops.project_north, "project_north": pprops.project_north,
"query": self.query,
} }
with open(self.json_filepath, "w") as f: with open(self.json_filepath, "w") as f:
json.dump(data, f) json.dump(data, f)
@@ -2446,7 +2558,7 @@ class EnableCulling(bpy.types.Operator):
self.total_mousemoves = 0 self.total_mousemoves = 0
self.cullable_objects = [] self.cullable_objects = []
def modal(self, context, event): def modal(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
if not LinksData.enable_culling: if not LinksData.enable_culling:
for obj in bpy.context.visible_objects: for obj in bpy.context.visible_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc"): if obj.type == "MESH" and obj.name.startswith("Ifc"):
@@ -2477,7 +2589,7 @@ class EnableCulling(bpy.types.Operator):
return {"PASS_THROUGH"} return {"PASS_THROUGH"}
def is_view_changed(self, context): def is_view_changed(self, context: bpy.types.Context) -> bool:
view_matrix = context.region_data.view_matrix view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix projection_matrix = context.region_data.window_matrix
vp_matrix = projection_matrix @ view_matrix vp_matrix = projection_matrix @ view_matrix
@@ -2492,7 +2604,7 @@ class EnableCulling(bpy.types.Operator):
return True return True
return False return False
def is_object_in_view(self, obj, context, camera_position): def is_object_in_view(self, obj: bpy.types.Object, context: bpy.types.Context, camera_position: Vector) -> bool:
# Get the view matrix and the projection matrix from the active viewport # Get the view matrix and the projection matrix from the active viewport
view_matrix = context.region_data.view_matrix view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix projection_matrix = context.region_data.window_matrix
@@ -2519,7 +2631,7 @@ class EnableCulling(bpy.types.Operator):
return False return False
return True return True
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set["rna_enums.OperatorReturnItems"]:
LinksData.enable_culling = True LinksData.enable_culling = True
self.cullable_objects = [] self.cullable_objects = []
for obj in bpy.context.visible_objects: for obj in bpy.context.visible_objects:
@@ -2806,8 +2918,16 @@ class IFCFileHandlerOperator(bpy.types.Operator):
bl_label = "Import .ifc file" bl_label = "Import .ifc file"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"}) directory: bpy.props.StringProperty(
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"}) subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"}
) # pyright: ignore[reportRedeclaration]
files: bpy.props.CollectionProperty(
type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"}
) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
directory: str
files: list[bpy.types.OperatorFileListElement]
def invoke(self, context, event): def invoke(self, context, event):
# Keeping code in .invoke() as we'll probably add some # Keeping code in .invoke() as we'll probably add some
@@ -2858,7 +2978,10 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Tool" bl_label = "Measure Tool"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty() measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
measure_type: str
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -2954,7 +3077,10 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Face Area Tool" bl_label = "Measure Face Area Tool"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty() measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
measure_type: str
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -3058,7 +3184,10 @@ class ClearMeasurement(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
polyline_props = tool.Model.get_polyline_props() polyline_props = tool.Model.get_polyline_props()
return len(polyline_props.measurement_polyline) > 0 if len(polyline_props.measurement_polyline) > 0:
return True
cls.poll_message_set("No measurement to clear.")
return False
def execute(self, context): def execute(self, context):
polyline_props = tool.Model.get_polyline_props() polyline_props = tool.Model.get_polyline_props()
@@ -3162,7 +3291,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
super().invoke(context, event) super().invoke(context, event)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
def cancel_tool(self, context): def cancel_tool(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
context.workspace.status_text_set(text=None) context.workspace.status_text_set(text=None)
if hasattr(self, "tool_state"): if hasattr(self, "tool_state"):
self.tool_state.plane_method = None self.tool_state.plane_method = None
@@ -3170,7 +3299,7 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
tool.Blender.update_viewport() tool.Blender.update_viewport()
return {"CANCELLED"} return {"CANCELLED"}
def handle_custom_instructions(self, context): def handle_custom_instructions(self, context: bpy.types.Context) -> None:
if len(self.selected_points) == 0: if len(self.selected_points) == 0:
instruction_text = "Click First Point on Image" instruction_text = "Click First Point on Image"
elif len(self.selected_points) == 1: elif len(self.selected_points) == 1:
@@ -3185,14 +3314,14 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
context.workspace.status_text_set(text=instruction_text) context.workspace.status_text_set(text=instruction_text)
def calculate_distance(self): def calculate_distance(self) -> None:
if len(self.selected_points) == 2: if len(self.selected_points) == 2:
point1 = self.selected_points[0] point1 = self.selected_points[0]
point2 = self.selected_points[1] point2 = self.selected_points[1]
distance_3d = (point2 - point1).length distance_3d = (point2 - point1).length
self.calculated_distance = distance_3d / self.unit_scale self.calculated_distance = distance_3d / self.unit_scale
def apply_scaling(self, context): def apply_scaling(self, context: bpy.types.Context) -> set["rna_enums.OperatorReturnItems"]:
if len(self.selected_points) != 2: if len(self.selected_points) != 2:
self.report({"ERROR"}, "Two points must be selected") self.report({"ERROR"}, "Two points must be selected")
return {"CANCELLED"} return {"CANCELLED"}
@@ -3250,7 +3379,10 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
bl_idname = "bim.load_blend_metadata_and_ifc" bl_idname = "bim.load_blend_metadata_and_ifc"
bl_label = "Load Blend Metadata and IFC" bl_label = "Load Blend Metadata and IFC"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(name="IFC File Path", default="") filepath: bpy.props.StringProperty(name="IFC File Path", default="") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
filepath: str
def execute(self, context): def execute(self, context):
ifc_file = self.filepath ifc_file = self.filepath
@@ -3284,3 +3416,19 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
bpy.app.handlers.load_post.append(load_handler) bpy.app.handlers.load_post.append(load_handler)
bpy.ops.wm.open_mainfile(filepath=metadata_path) bpy.ops.wm.open_mainfile(filepath=metadata_path)
return {"FINISHED"} return {"FINISHED"}
class GenerateUVMap(bpy.types.Operator):
bl_idname = "bim.generate_uv_map"
bl_label = "Generate UV Map"
bl_description = "Generate UV map for selected mesh."
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
if not obj or not isinstance(obj.data, bpy.types.Mesh):
self.report({"ERROR"}, "No valid mesh selected.")
return {"CANCELLED"}
tool.Loader.load_generated_uv_map(obj.data)
self.report({"INFO"}, "Generated UV map for selected mesh.")
return {"FINISHED"}
+1 -1
View File
@@ -496,7 +496,7 @@ class BIM_PT_links(Panel):
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
else: else:
row.operator("bim.load_link", text="", icon="LINKED").link_index = index row.operator("bim.load_link", text="", icon="LINKED").link_index = index
row.operator("bim.unlink_ifc", text="", icon="X").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") self.layout.template_list("BIM_UL_links", "", self.props, "links", self.props, "active_link_index")
if LinksData.enable_culling: if LinksData.enable_culling:
@@ -71,24 +71,26 @@ class ExploreTool(bpy.types.WorkSpaceTool):
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_M") row.label(text="", icon="EVENT_M")
row = layout.row(align=True)
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT") op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
op.hotkey = "S_M" op.hotkey = "S_M"
row = layout.row(align=True) row = layout.row(align=True)
row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=True) row.prop(prop, "measurement_type", text="Measure Type", expand=True, icon_only=True, emboss=True)
row = layout.row(align=True)
op = row.operator("bim.clear_measurement", text="", icon="X") op = row.operator("bim.clear_measurement", text="", icon="X")
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_S") row.label(text="", icon="EVENT_S")
row = layout.row(align=True)
op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE") op = row.operator("bim.explore_hotkey", text="Image Scaling Tool", icon="IMAGE_PLANE")
op.hotkey = "S_S" op.hotkey = "S_S"
op.description = "Scale Image Annotation. Allows to scale an IfcReferenceImage. Select image, select tool. Check lower left corner instructions to select two points and provide real distance between them" op.description = (
"Scale Image Annotation.\n\n"
"Allows to scale an IfcReferenceImage.\n\n"
"Select image, select tool. "
"Check lower left corner instructions to select two points and provide real distance between them"
)
op = row.operator("bim.generate_uv_map", text="Generate UV Map", icon="UV") row = layout.row(align=True)
op.description = "Generate UV map for selected mesh." row.operator("bim.generate_uv_map", icon="UV")
class ExploreHotkey(bpy.types.Operator): class ExploreHotkey(bpy.types.Operator):
@@ -156,20 +158,3 @@ class ExploreHotkey(bpy.types.Operator):
def hotkey_A_H(self) -> None: def hotkey_A_H(self) -> None:
bpy.ops.bim.hide_queried_linked_element(unhide_all=True) bpy.ops.bim.hide_queried_linked_element(unhide_all=True)
class GenerateUVMap(bpy.types.Operator):
bl_idname = "bim.generate_uv_map"
bl_label = "Generate UV Map"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
description: bpy.props.StringProperty()
def execute(self, context):
obj = context.active_object
if not obj or not hasattr(obj, "data") or not hasattr(obj.data, "polygons"):
self.report({"ERROR"}, "No valid mesh selected.")
return {"CANCELLED"}
tool.Loader.load_generated_uv_map(obj.data)
self.report({"INFO"}, "Generated UV map for selected mesh.")
return {"FINISHED"}
+1 -1
View File
@@ -365,7 +365,7 @@ class TestLoadingIfcSqlite(NewFile):
sql_type="SQLite", sql_type="SQLite",
) )
patcher.patch() patcher.patch()
tmp_file = Path(tempfile.mktemp(suffix=".ifcsqlite")) tmp_file = Path(tempfile.mkstemp(suffix=".ifcsqlite")[1])
ifcpatch.write(patcher.get_output(), tmp_file) ifcpatch.write(patcher.get_output(), tmp_file)
elements_with_meshes = [ elements_with_meshes = [
@@ -53,9 +53,7 @@ def create_file(version: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4") -> ifcope
""" """
file = ifcopenshell.file(schema=version) file = ifcopenshell.file(schema=version)
file.header.file_name.name = "/dev/null" # Hehehe file.header.file_name.name = "/dev/null" # Hehehe
file.header.file_name.time_stamp = ( file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat()
)
file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version) file.header.file_name.preprocessor_version = "IfcOpenShell {}".format(ifcopenshell.version)
file.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version) file.header.file_name.originating_system = "IfcOpenShell {}".format(ifcopenshell.version)
file.header.file_name.authorization = "Nobody" file.header.file_name.authorization = "Nobody"
+1 -7
View File
@@ -57,13 +57,7 @@ class Patcher:
def patch(self): def patch(self):
self.file.header.file_name.name = "Rabbit" self.file.header.file_name.name = "Rabbit"
self.file.header.file_name.time_stamp = ( self.file.header.file_name.time_stamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
datetime.datetime.utcnow()
.replace(tzinfo=datetime.timezone.utc)
.astimezone()
.replace(microsecond=0)
.isoformat()
)
self.file.header.file_name.preprocessor_version = "Rabbit" self.file.header.file_name.preprocessor_version = "Rabbit"
self.file.header.file_name.originating_system = "Rabbit" self.file.header.file_name.originating_system = "Rabbit"
+3 -3
View File
@@ -52,10 +52,10 @@ class Test:
assert output.by_type("IfcProject")[0].GlobalId == project.GlobalId assert output.by_type("IfcProject")[0].GlobalId == project.GlobalId
assert output.by_type("IfcWall")[0].GlobalId == wall.GlobalId assert output.by_type("IfcWall")[0].GlobalId == wall.GlobalId
output_path = Path(tempfile.mktemp()) output_path = Path(tempfile.mkstemp()[1])
try: try:
assert not output_path.exists() assert output_path.stat().st_size == 0
ifcpatch.write(patcher.get_output(), output_path) ifcpatch.write(patcher.get_output(), output_path)
assert output_path.exists() assert output_path.stat().st_size != 0
finally: finally:
output_path.unlink() output_path.unlink()
+3 -5
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from database.neo4j import db from database.neo4j import db
from fastapi import Depends, HTTPException, Security, status from fastapi import Depends, HTTPException, Security, status
@@ -33,10 +33,8 @@ credentials_exception = HTTPException(
def create_access_token(data: dict, expires_delta: timedelta | None = None): def create_access_token(data: dict, expires_delta: timedelta | None = None):
payload = data.copy() payload = data.copy()
if expires_delta: expires_delta = expires_delta or timedelta(minutes=15)
expire = datetime.utcnow() + expires_delta expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
payload.update({"expires": str(expire)}) payload.update({"expires": str(expire)})
encoded_jwt = jwt.encode(payload, secrets["security_secret_key"], algorithm=os.environ["SECURITY_ALGORITHM"]) encoded_jwt = jwt.encode(payload, secrets["security_secret_key"], algorithm=os.environ["SECURITY_ALGORITHM"])
return encoded_jwt return encoded_jwt