diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index d0750b36c7..7b10f8c3fd 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -224,7 +224,7 @@ def refresh_ui_data(): props = tool.Drawing.get_document_props() props.should_draw_decorations = props.should_draw_decorations - if bpy.context.scene.WebProperties.is_connected: + if tool.Web.get_web_props().is_connected: tool.Web.send_webui_data() diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index e7dd8ab8b9..9e1170f98c 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -884,7 +884,8 @@ class IfcImporter: for material in self.material_creator.materials.values(): bpy.data.materials.remove(material) - def add_project_to_scene(self): + def add_project_to_scene(self) -> None: + assert bpy.context.scene try: bpy.context.scene.collection.children.link(self.project["blender"]) except: @@ -909,6 +910,7 @@ class IfcImporter: bpy.ops.mesh.normals_make_consistent() bpy.ops.object.editmode_toggle() + assert bpy.context.view_layer bpy.context.view_layer.objects.active = last_obj IfcStore.edited_objs.clear() @@ -927,6 +929,7 @@ class IfcImporter: if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext): return # Geometry is optional in IFC props = tool.Blender.get_bim_props() + assert bpy.context.scene for unit in assignment.Units: if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT": if unit.is_a("IfcSIUnit"): @@ -1065,11 +1068,11 @@ class IfcImporter: def create_mesh( self, element: ifcopenshell.entity_instance, - shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType], + shape: Union[W.Triangulation, W.TriangulationElement], cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None, ) -> Union[bpy.types.Mesh, None]: try: - if isinstance(shape, ifcopenshell.geom.ShapeElementType): + if isinstance(shape, W.TriangulationElement): # shape is ShapeElementType geometry = shape.geometry else: diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index 618c022cf4..34e20f6d9c 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -269,7 +269,7 @@ class ExportIfcCsv(bpy.types.Operator, ExportHelper): schedule_creator = scheduler.Scheduler() schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg")) if props.format == "web": - if not context.scene.WebProperties.is_connected: + if not tool.Web.get_web_props().is_connected: bpy.ops.bim.connect_websocket_server() df = ifc_csv.dataframe assert df is not None diff --git a/src/bonsai/bonsai/bim/module/demo/operator.py b/src/bonsai/bonsai/bim/module/demo/operator.py index 0621f542d2..001c8de692 100644 --- a/src/bonsai/bonsai/bim/module/demo/operator.py +++ b/src/bonsai/bonsai/bim/module/demo/operator.py @@ -88,7 +88,7 @@ class SendWebUiDemoMessage(bpy.types.Operator): # First, we need to make sure that we are connected to the Web UI # We do that by checking the Web module properties' is_connected prop # and calling the operator connect_websocket_server if we aren't connected to a Web UI - if not context.scene.WebProperties.is_connected: + if not tool.Web.get_web_props().is_connected: bpy.ops.bim.connect_websocket_server(page="demo") props = tool.Demo.get_demo_props() core.send_webui_demo_message(tool.Web, message=props.webui_message) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index ad6bfe2542..6999c8536e 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -134,6 +134,7 @@ def format_distance( custom_unit=None, ): # Get Blender Scene Unit Settings + assert bpy.context.scene unit_scale = bpy.context.scene.unit_settings.scale_length unit_system = bpy.context.scene.unit_settings.system unit_length = bpy.context.scene.unit_settings.length_unit diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 8f8e094e7e..ed4479dddb 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3735,7 +3735,7 @@ class OpenDocumentationWebUi(bpy.types.Operator): bl_description = "Open the documentation web UI page" def execute(self, context): - if not context.scene.WebProperties.is_connected: + if not tool.Web.get_web_props().is_connected: bpy.ops.bim.connect_websocket_server(page="documentation") else: bpy.ops.bim.open_web_browser(page="documentation") diff --git a/src/bonsai/bonsai/bim/module/web/operator.py b/src/bonsai/bonsai/bim/module/web/operator.py index b3cc3c8264..1c8bb21e9b 100644 --- a/src/bonsai/bonsai/bim/module/web/operator.py +++ b/src/bonsai/bonsai/bim/module/web/operator.py @@ -29,11 +29,12 @@ class ConnectToWebsocketServer(bpy.types.Operator): page: bpy.props.StringProperty(default="") def execute(self, context): - port = context.scene.WebProperties.webserver_port + props = tool.Web.get_web_props() + port = props.webserver_port if port == 0: - context.scene.WebProperties.webserver_port = core.generate_port_number(tool.Web) + props.webserver_port = core.generate_port_number(tool.Web) - port = context.scene.WebProperties.webserver_port + port = props.webserver_port core.connect_websocket_server(tool.Web, port, self.page) return {"FINISHED"} @@ -65,6 +66,6 @@ class OpenWebBrowser(bpy.types.Operator): page: bpy.props.StringProperty(default="") def execute(self, context): - port = context.scene.WebProperties.webserver_port + port = tool.Web.get_web_props().webserver_port core.open_web_browser(tool.Web, port, self.page) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/web/prop.py b/src/bonsai/bonsai/bim/module/web/prop.py index cecd3c9311..66ffdd4995 100644 --- a/src/bonsai/bonsai/bim/module/web/prop.py +++ b/src/bonsai/bonsai/bim/module/web/prop.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from typing import TYPE_CHECKING import bpy from bpy.types import PropertyGroup from bpy.props import ( @@ -31,6 +32,21 @@ from bpy.props import ( class WebProperties(PropertyGroup): - webserver_port: IntProperty(name="Webserver Port", min=0, max=65535) - is_running: BoolProperty(name="Webserver Running Status", default=False) - is_connected: BoolProperty(name="Connection Status", default=False) + webserver_port: IntProperty( # pyright: ignore[reportRedeclaration] + name="Webserver Port", + min=0, + max=65535, + ) + is_running: BoolProperty( # pyright: ignore[reportRedeclaration] + name="Webserver Running Status", + default=False, + ) + is_connected: BoolProperty( # pyright: ignore[reportRedeclaration] + name="Connection Status", + default=False, + ) + + if TYPE_CHECKING: + webserver_port: int + is_running: bool + is_connected: bool diff --git a/src/bonsai/bonsai/bim/module/web/ui.py b/src/bonsai/bonsai/bim/module/web/ui.py index 7af5cf84f7..0a6fca55ae 100644 --- a/src/bonsai/bonsai/bim/module/web/ui.py +++ b/src/bonsai/bonsai/bim/module/web/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . from bpy.types import Panel +import bonsai.tool as tool from bonsai.bim.module.web.data import WebData @@ -34,10 +35,10 @@ class BIM_PT_webui(Panel): if not WebData.is_loaded: WebData.load() + assert self.layout layout = self.layout - scene = context.scene - props = scene.WebProperties + props = tool.Web.get_web_props() row = layout.row() row.prop(props, "webserver_port", text="Websocket server Port") diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 7e1a3f7d74..c65b2a29e2 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -1085,8 +1085,8 @@ class Cost(bonsai.core.tool.Cost): return {"id": unit.id(), "name": unit.Currency} @classmethod - def generate_cost_schedule_browser(cls, cost_chedule) -> None: - if not bpy.context.scene.WebProperties.is_connected: + def generate_cost_schedule_browser(cls, cost_chedule: ifcopenshell.entity_instance) -> None: + if not tool.Web.get_web_props().is_connected: bpy.ops.bim.connect_websocket_server(page="costing") tool.Web.load_cost_schedule_web_ui(cost_chedule) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 3b632c28db..92814b4c03 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -37,6 +37,7 @@ import ifcopenshell.api.root import ifcopenshell.api.style import ifcopenshell.geom import ifcopenshell.guid +import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation @@ -883,6 +884,7 @@ class Geometry(bonsai.core.tool.Geometry): if iterator and iterator.initialize(): while True: shape = iterator.get() + assert isinstance(shape, W.TriangulationElement) element = tool.Ifc.get().by_id(shape.id) if obj := tool.Ifc.get_object(element): # It's possible that there will be multiple shapes for the same context, diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index bc04415f19..f748c88616 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -23,6 +23,7 @@ import bpy import bmesh import logging import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.util.placement @@ -814,7 +815,7 @@ class Loader(bonsai.core.tool.Loader): cls, element: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance, - shape: ifcopenshell.geom.ShapeElementType, + shape: W.TriangulationElement, ) -> bpy.types.Camera: """Create camera data. @@ -968,7 +969,7 @@ class Loader(bonsai.core.tool.Loader): @classmethod def convert_geometry_to_mesh( cls, - geometry: ifcopenshell.geom.ShapeType, + geometry: W.Triangulation, mesh: bpy.types.Mesh, verts: Optional[npt.NDArray[np.float64]] = None, *, diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 96dd8d4f38..fed5d918a2 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -30,6 +30,7 @@ import ifcopenshell.api.geometry import ifcopenshell.api.grid import ifcopenshell.api.pset import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation @@ -1864,6 +1865,7 @@ class Model(bonsai.core.tool.Model): polygons = {} for curve in curves: geometry = ifcopenshell.geom.create_shape(settings, curve) + assert isinstance(geometry, W.Triangulation) v = ifcopenshell.util.shape.get_vertices(geometry, is_2d=True) v = np.round(v, 4) # Round to nearest 0.1mm, otherwise things like circles don't polygonise reliably edges = ifcopenshell.util.shape.get_edges(geometry) diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index 8e467dd182..cfb36a06ab 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -21,6 +21,7 @@ import bpy import ifcopenshell import ifcopenshell.api.profile import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.element import ifcopenshell.util.unit import ifcopenshell.util.placement @@ -52,6 +53,7 @@ class Profile(bonsai.core.tool.Profile): settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) shape = ifcopenshell.geom.create_shape(settings, profile) + assert isinstance(shape, W.Triangulation) verts = ifcopenshell.util.shape.get_vertices(shape) if verts.size == 0: raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.") diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index 1f361cf704..284a919557 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -1650,7 +1650,7 @@ class Sequence(bonsai.core.tool.Sequence): def generate_gantt_browser_chart( cls, task_json: list[dict[str, Any]], work_schedule: ifcopenshell.entity_instance ) -> None: - if not bpy.context.scene.WebProperties.is_connected: + if not tool.Web.get_web_props().is_connected: bpy.ops.bim.connect_websocket_server(page="sequencing") gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)} tool.Web.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data") diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index c89ca76eae..9a1924aa4e 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -27,7 +27,7 @@ import bonsai.tool as tool import ifcopenshell.api.sequence import ifcopenshell.api.classification import ifcopenshell.api.cost -from typing import Any, Optional +from typing import Any, Optional, TYPE_CHECKING import time import socket import sys @@ -47,6 +47,9 @@ import bonsai.core.cost from ifc5d.ifc2json import ifc5D2json from bonsai.bim.ifc import IfcStore +if TYPE_CHECKING: + from bonsai.bim.module.web.prop import WebProperties + sio = None ws_process = None ws_thread = None @@ -62,6 +65,11 @@ IFC_TASK_ATTRIBUTE_MAP = { class Web(bonsai.core.tool.Web): + @classmethod + def get_web_props(cls) -> WebProperties: + assert (scene := bpy.context.scene) + return scene.BIMProperties # pyright: ignore[reportAttributeAccessIssue] + @classmethod def generate_port_number(cls) -> int: """ @@ -151,7 +159,7 @@ class Web(bonsai.core.tool.Web): """ global ws_thread, sio - if bpy.context.scene.WebProperties.is_connected: + if tool.Web.get_web_props().is_connected: print(f"Already connected to websocket server on port: {port}") return @@ -200,7 +208,7 @@ class Web(bonsai.core.tool.Web): print("No Websocket server running") return - if bpy.context.scene.WebProperties.is_connected: + if tool.Web.get_web_props().is_connected: cls.disconnect_websocket_server() # sleep(0.5) @@ -282,7 +290,7 @@ class Web(bonsai.core.tool.Web): if data is not None: payload[data_key] = data - if ws_thread is not None and bpy.context.scene.WebProperties.is_connected: + if ws_thread is not None and tool.Web.get_web_props().is_connected: ws_thread.run_coro(cls.sio_send(payload, event, namespace)) @classmethod @@ -295,7 +303,7 @@ class Web(bonsai.core.tool.Web): Returns: (Optional[float]): Returns None if the WebProperties.is_connected is False, otherwise returns 1.0 to continue the timer. """ - if not bpy.context.scene.WebProperties.is_connected: + if not tool.Web.get_web_props().is_connected: with web_operator_queue.mutex: web_operator_queue.queue.clear() return None # unregister timer if not connected @@ -879,11 +887,13 @@ class Web(bonsai.core.tool.Web): @classmethod def set_is_running(cls, is_running: bool) -> None: - bpy.context.scene.WebProperties.is_running = is_running + props = tool.Web.get_web_props() + props.is_running = is_running @classmethod def set_is_connected(cls, is_connected: bool) -> None: - bpy.context.scene.WebProperties.is_connected = is_connected + props = tool.Web.get_web_props() + props.is_connected = is_connected class AsyncioThread(threading.Thread): diff --git a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi index 73d22ce836..659a062cfd 100644 --- a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi +++ b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi @@ -94,6 +94,7 @@ class FileDescription(HeaderEntity): class FileName(HeaderEntity): name: str + """Updated automatically only on ``file.write``.""" time_stamp: str author: tuple[str, ...] organization: tuple[str, ...] @@ -358,7 +359,7 @@ class Iterator: """ ... - def set_cache(self, cache: GeometrySerializer) -> None: ... + def set_cache(self, cache: Union[GeometrySerializer, None]) -> None: ... def unit_magnitude(self): ... def unit_name(self): ... @@ -1351,8 +1352,8 @@ class style(item): - IFC style id if style assigned to the representation items directly or through material with a style; - IFC material id if both true: - - element has a material without a style; - - there are parts of the geometry that has no other style assigned to them; + - element has a material without a style; + - there are parts of the geometry that has no other style assigned to them; - -1 in case if there is no material; - 0 in case if there are default materials used. """