diff --git a/src/bonsai/bonsai/bim/module/project/data.py b/src/bonsai/bonsai/bim/module/project/data.py index 56a77d1880..8ecf30eef5 100644 --- a/src/bonsai/bonsai/bim/module/project/data.py +++ b/src/bonsai/bonsai/bim/module/project/data.py @@ -170,6 +170,6 @@ class ProjectLibraryData: class LinksData: - linked_data = {} + linked_data: dict[str, Any] = {} enable_culling = False is_loaded = False diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index 8c2ec0e08b..cb1ee4b4bd 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -from typing import Union - import bmesh import bpy import gpu @@ -54,7 +52,7 @@ class ProjectDecorator: installed = None @classmethod - def install(cls, context): + def install(cls, context: bpy.types.Context) -> None: if cls.installed: cls.uninstall() handler = cls() @@ -99,9 +97,9 @@ class ProjectDecorator: # general shader self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - selected_vertices = [] - selected_edges = [] - selected_tris = [] + selected_vertices: list[tuple[float, float, float]] = [] + selected_edges: list[tuple[int, int]] = [] + selected_tris: list[tuple[int, int, int]] = [] props = tool.Project.get_project_props() try: @@ -112,7 +110,7 @@ class ProjectDecorator: except: return - root_obj: Union[bpy.types.Object, None] = props.queried_obj_root + root_obj = props.queried_obj_root if root_obj and not (m := root_obj.matrix_world).is_identity: selected_vertices = [m @ Vector(v) for v in selected_vertices] diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index da3771ba9c..ea074a2e96 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -27,11 +27,12 @@ import traceback from collections import defaultdict from math import radians from pathlib import Path -from typing import TYPE_CHECKING, Literal, Union, get_args +from typing import TYPE_CHECKING, Any, Literal, Union, get_args import bpy import ifcopenshell import ifcopenshell.api.attribute +import ifcopenshell.api.document import ifcopenshell.api.nest import ifcopenshell.api.project import ifcopenshell.api.root @@ -1438,8 +1439,13 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Load Link" bl_options = {"REGISTER", "UNDO"} bl_description = "Load the selected file" - link_index: bpy.props.IntProperty(name="Link Index") - use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) + + link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + link_index: int + use_cache: bool def _execute(self, context): self.link = tool.Project.get_project_props().links[self.link_index] @@ -1464,9 +1470,10 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator): empty = bpy.data.objects.new(empty_name, None) empty.instance_type = "COLLECTION" empty.instance_collection = collection - empty.matrix_world = Matrix(tool.Project.calculate_link_matrix(self.link)) + empty.matrix_world = tool.Project.calculate_link_matrix(self.link) tool.Project.set_link_empty_handle(self.link, empty) + assert bpy.context.scene bpy.context.scene.collection.objects.link(empty) self.link.is_loaded = True if tool.Ifc.get(): # For non-IFC projects, locking has no meaning @@ -1635,8 +1642,16 @@ class ToggleLinkVisibility(bpy.types.Operator): bl_label = "Toggle Link Visibility" bl_options = {"REGISTER", "UNDO"} bl_description = "Toggle visibility between SOLID and WIREFRAME" - link_index: bpy.props.IntProperty(name="Link Index") - mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE"))) + + link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration] + mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] + name="Visibility Mode", + items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")), + ) + + if TYPE_CHECKING: + link_index: int + mode: Literal["WIREFRAME", "VISIBLE"] def execute(self, context): props = tool.Project.get_project_props() @@ -1688,8 +1703,11 @@ class EnableEditingLink(bpy.types.Operator): def execute(self, context): link = tool.Project.get_project_props().active_link + assert link link.is_editing = True - tool.Geometry.unlock_object(tool.Project.get_link_empty_handle(link)) + obj = tool.Project.get_link_empty_handle(link) + assert obj + tool.Geometry.unlock_object(obj) return {"FINISHED"} @@ -1701,9 +1719,11 @@ class DisableEditingLink(bpy.types.Operator): def execute(self, context): link = tool.Project.get_project_props().active_link + assert link link.is_editing = False obj = tool.Project.get_link_empty_handle(link) - obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link)) + assert obj + obj.matrix_world = tool.Project.calculate_link_matrix(link) tool.Geometry.lock_object(obj) return {"FINISHED"} @@ -1716,8 +1736,10 @@ class EditLink(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): link = tool.Project.get_project_props().active_link + assert link link.is_editing = False obj = tool.Project.get_link_empty_handle(link) + assert obj new_obj_matrix = obj.matrix_world filepath = Path(tool.Ifc.resolve_uri(link.filepath)) @@ -1753,7 +1775,7 @@ class EditLink(bpy.types.Operator, tool.Ifc.Operator): else: link.transformation = transformation - obj.matrix_world = Matrix(tool.Project.calculate_link_matrix(link)) + obj.matrix_world = tool.Project.calculate_link_matrix(link) tool.Geometry.lock_object(obj) @@ -2270,7 +2292,8 @@ class QueryLinkedElement(bpy.types.Operator): props = tool.Project.get_project_props() props.queried_obj = None - for area in bpy.context.screen.areas: + assert context.screen + for area in context.screen.areas: if area.type == "PROPERTIES": for region in area.regions: if region.type == "WINDOW": @@ -2278,6 +2301,7 @@ class QueryLinkedElement(bpy.types.Operator): elif area.type == "VIEW_3D": area.tag_redraw() + assert context.region and context.region_data region = context.region rv3d = context.region_data coord = (self.mouse_x, self.mouse_y) @@ -2294,18 +2318,20 @@ class QueryLinkedElement(bpy.types.Operator): guid = None guid_start_index = 0 - for i, guid_end_index in enumerate(obj["guid_ids"]): + guid_ids: list[int] = obj["guid_ids"] + for i, guid_end_index in enumerate(guid_ids): if face_index < guid_end_index: guid = obj["guids"][i] props.queried_obj = obj props.queried_obj_root = self.find_obj_root(obj, instance_matrix) - selected_tris = [] - selected_edges = [] - vert_indices = set() + selected_tris: list[tuple[int, ...]] = [] + selected_edges: list[tuple[int, ...]] = [] + vert_indices_set: set[int] = set() + assert isinstance(obj.data, bpy.types.Mesh) for polygon in obj.data.polygons[guid_start_index:guid_end_index]: - vert_indices.update(polygon.vertices) - vert_indices = list(vert_indices) + vert_indices_set.update(polygon.vertices) + vert_indices = list(vert_indices_set) vert_map = {k: v for v, k in enumerate(vert_indices)} selected_vertices = [tuple(obj.matrix_world @ obj.data.vertices[vi].co) for vi in vert_indices] for polygon in obj.data.polygons[guid_start_index:guid_end_index]: @@ -2319,13 +2345,14 @@ class QueryLinkedElement(bpy.types.Operator): break guid_start_index = guid_end_index + assert guid is not None self.db = sqlite3.connect(obj["db"]) self.c = self.db.cursor() self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1") element = self.c.fetchone() - attributes = {} + attributes: dict[str, Any] = {} for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]): if element[i + 1] is not None: attributes[attr] = element[i + 1] @@ -2415,6 +2442,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement): return {"CANCELLED"} queried_obj = props.queried_obj + assert queried_obj ifc_file = tool.Ifc.get() linked_ifc_file: ifcopenshell.file @@ -2683,10 +2711,12 @@ class CreateClippingPlane(bpy.types.Operator): self.report({"INFO"}, "Maximum of six clipping planes allowed.") return {"FINISHED"} + assert context.screen for area in context.screen.areas: if area.type == "VIEW_3D": area.tag_redraw() + assert context.region and context.region_data region = context.region rv3d = context.region_data if rv3d: # Called from a 3D viewport diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 7bc27a20e0..1d0e8742dd 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -774,6 +774,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): bsdd_load_preview_dictionaries: bool bsdd_load_inactive_dictionaries: bool bsdd_load_test_dictionaries: bool + bsdd_baseurl: str should_disable_undo_on_save: bool should_stream: bool should_always_cache: bool @@ -789,6 +790,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): mass_time_units_in_wizard: bool chain_filter_with_set_operations: bool save_metadata_blend_file: bool + metadata_blend_file_suffix: str decorator_font_scale: float def draw(self, context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index dc47b977f8..5e53223158 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -31,6 +31,7 @@ import ifcopenshell import ifcopenshell.api.document import ifcopenshell.util.element import ifcopenshell.util.representation +import ifcopenshell.util.shape_builder import numpy as np from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES @@ -45,7 +46,7 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore if TYPE_CHECKING: - from bonsai.bim.module.project.prop import BIMProjectProperties, MeasureToolSettings + from bonsai.bim.module.project.prop import BIMProjectProperties, MeasureToolSettings, Link HiearchyDict = dict[ifcopenshell.entity_instance, "HiearchyDict"] @@ -61,20 +62,20 @@ class Project(bonsai.core.tool.Project): return scene.MeasureToolSettings # pyright: ignore[reportAttributeAccessIssue] @classmethod - def get_link_empty_handle(cls, link) -> bpy.types.Object | None: + def get_link_empty_handle(cls, link: 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: + def set_link_empty_handle(cls, link: 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: + def calculate_link_matrix(cls, link: Link) -> Matrix: filepath = Path(tool.Ifc.resolve_uri(link.filepath)) with open(filepath.with_suffix(".ifc.cache.json"), "r") as f: metadata = json.load(f) @@ -99,7 +100,7 @@ class Project(bonsai.core.tool.Project): 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 + return Matrix(np.linalg.inv(local_matrix) @ global_matrix) @classmethod def append_all_types_from_template(cls, template: str) -> None: diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py index 0f9dc8544d..c7ca60610b 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractPropertiesToSQLite.py @@ -17,21 +17,24 @@ # along with IfcPatch. If not, see . +import logging import tempfile import ifcopenshell.util.element +import ifcpatch + try: import sqlite3 except: print("No SQLite support") -class Patcher: +class Patcher(ifcpatch.BasePatcher): def __init__( self, - file, - logger, + file: ifcopenshell.file, + logger: logging.Logger | None = None, ): """Extracts properties and relationships from a IFC-SPF model to SQLite. @@ -45,10 +48,11 @@ class Patcher: result = ifcpatch.execute({"input": fn, "file": model, "recipe": "ExtractPropertiesToSQLite"}) ifcpatch.write(result, "output.sqlite") """ - self.file = file - self.logger = logger + super().__init__(file, logger) def patch(self): + import sqlite3 + tmp = tempfile.NamedTemporaryFile(delete=False) db_file = tmp.name self.db = sqlite3.connect(db_file) diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index 256ffa99ce..d1405e9a33 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -125,7 +125,6 @@ class Patcher(ifcpatch.BasePatcher): ) """ super().__init__(file, logger) - self.logger = logger self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower() self.host = host self.username = username