From 5ec1ebf4d3e290340e13197653818bbbf6b3ad56 Mon Sep 17 00:00:00 2001 From: krande Date: Tue, 1 Oct 2024 10:56:42 +0200 Subject: [PATCH] make file node objects viewable --- examples/procedure_example/.gitignore | 1 + examples/procedure_example/ada_config.toml | 6 +- .../list_server_file_objects.py | 6 +- src/ada/comms/fb_deserializer.py | 13 ++- src/ada/comms/fb_model_gen.py | 10 ++- src/ada/comms/fb_serializer.py | 42 +++++++-- .../comms/msg_handling/default_on_message.py | 5 +- src/ada/comms/msg_handling/run_procedure.py | 23 ++++- src/ada/comms/msg_handling/update_scene.py | 3 +- src/ada/comms/msg_handling/update_server.py | 47 ++++++---- .../comms/msg_handling/view_file_object.py | 53 ++++++++++++ src/ada/comms/procedures.py | 39 ++++++++- src/ada/comms/scene_model.py | 77 +++++++++++++++++ src/ada/comms/wsock/CommandType.py | 1 + src/ada/comms/wsock/FileObject.py | 77 ++++++++++++++++- src/ada/comms/wsock/Server.py | 42 +++++++-- src/ada/comms/wsock/ServerReply.py | 30 +++++-- src/ada/comms/wsock_client_async.py | 5 ++ src/ada/comms/wsock_client_base.py | 15 +++- src/ada/comms/wsock_server.py | 11 +-- src/ada/config.py | 1 + src/ada/visit/renderer_manager.py | 10 ++- src/flatbuffers/schemas/commands.fbs | 11 ++- .../src/components/node_editor/InfoPanel.tsx | 55 ------------ .../node_editor/NodeEditorComponent.tsx | 14 +-- .../node_editor/customFileObjectNode.tsx | 27 +++--- .../node_editor/customProcedureNode.tsx | 17 ++-- src/frontend/src/external/svg_content.tsx | 7 ++ .../src/flatbuffers/wsock/command-type.ts | 3 +- .../src/flatbuffers/wsock/file-object.ts | 86 +++++++++++++++---- .../src/flatbuffers/wsock/server-reply.ts | 25 ++++-- src/frontend/src/flatbuffers/wsock/server.ts | 62 ++++++++++--- .../fb_handling/handle_incoming_buffers.ts | 7 ++ .../src/utils/node_editor/update_nodes.ts | 2 + .../scene/get_file_object_from_server.ts | 5 -- .../scene/view_file_object_from_server.ts | 40 +++++++++ 36 files changed, 682 insertions(+), 196 deletions(-) create mode 100644 examples/procedure_example/.gitignore create mode 100644 src/ada/comms/msg_handling/view_file_object.py create mode 100644 src/ada/comms/scene_model.py delete mode 100644 src/frontend/src/components/node_editor/InfoPanel.tsx create mode 100644 src/frontend/src/external/svg_content.tsx delete mode 100644 src/frontend/src/utils/scene/get_file_object_from_server.ts create mode 100644 src/frontend/src/utils/scene/view_file_object_from_server.ts diff --git a/examples/procedure_example/.gitignore b/examples/procedure_example/.gitignore new file mode 100644 index 000000000..a737d4df9 --- /dev/null +++ b/examples/procedure_example/.gitignore @@ -0,0 +1 @@ +server_temp/ \ No newline at end of file diff --git a/examples/procedure_example/ada_config.toml b/examples/procedure_example/ada_config.toml index bd9194345..7427e5cba 100644 --- a/examples/procedure_example/ada_config.toml +++ b/examples/procedure_example/ada_config.toml @@ -1,2 +1,6 @@ [procedures] -script_dir = "./procedures" \ No newline at end of file +script_dir = "./procedures" + +[websockets] +server_temp_dir = "./server_temp" +auto_load_files = true \ No newline at end of file diff --git a/examples/procedure_example/list_server_file_objects.py b/examples/procedure_example/list_server_file_objects.py index 246a0b802..c42986818 100644 --- a/examples/procedure_example/list_server_file_objects.py +++ b/examples/procedure_example/list_server_file_objects.py @@ -1,11 +1,7 @@ import asyncio import pathlib -from ada.comms.fb_model_gen import ( - FileObjectDC, - FilePurposeDC, - FileTypeDC, -) +from ada.comms.fb_model_gen import FileObjectDC, FilePurposeDC, FileTypeDC from ada.comms.wsock_client_async import WebSocketClientAsync THIS_DIR = pathlib.Path(__file__).parent diff --git a/src/ada/comms/fb_deserializer.py b/src/ada/comms/fb_deserializer.py index e8479efeb..f23b73ed0 100644 --- a/src/ada/comms/fb_deserializer.py +++ b/src/ada/comms/fb_deserializer.py @@ -44,6 +44,10 @@ def deserialize_fileobject(fb_obj) -> FileObjectDC | None: purpose=FilePurposeDC(fb_obj.Purpose()), filepath=fb_obj.Filepath().decode("utf-8") if fb_obj.Filepath() is not None else None, filedata=bytes(fb_obj.FiledataAsNumpy()) if fb_obj.FiledataLength() > 0 else None, + glb_file=deserialize_fileobject(fb_obj.GlbFile()), + ifcsqlite_file=deserialize_fileobject(fb_obj.IfcsqliteFile()), + is_procedure_output=fb_obj.IsProcedureOutput(), + procedure_parent=deserialize_procedurestart(fb_obj.ProcedureParent()), ) @@ -89,12 +93,18 @@ def deserialize_server(fb_obj) -> ServerDC | None: return None return ServerDC( - add_file_object=deserialize_fileobject(fb_obj.AddFileObject()), + new_file_object=deserialize_fileobject(fb_obj.NewFileObject()), all_file_objects=( [deserialize_fileobject(fb_obj.AllFileObjects(i)) for i in range(fb_obj.AllFileObjectsLength())] if fb_obj.AllFileObjectsLength() > 0 else None ), + get_file_object_by_name=( + fb_obj.GetFileObjectByName().decode("utf-8") if fb_obj.GetFileObjectByName() is not None else None + ), + get_file_object_by_path=( + fb_obj.GetFileObjectByPath().decode("utf-8") if fb_obj.GetFileObjectByPath() is not None else None + ), ) @@ -174,6 +184,7 @@ def deserialize_serverreply(fb_obj) -> ServerReplyDC | None: return ServerReplyDC( message=fb_obj.Message().decode("utf-8") if fb_obj.Message() is not None else None, + file_object=deserialize_fileobject(fb_obj.FileObject()), reply_to=CommandTypeDC(fb_obj.ReplyTo()), error=deserialize_error(fb_obj.Error()), ) diff --git a/src/ada/comms/fb_model_gen.py b/src/ada/comms/fb_model_gen.py index da63623ab..50cc45986 100644 --- a/src/ada/comms/fb_model_gen.py +++ b/src/ada/comms/fb_model_gen.py @@ -19,6 +19,7 @@ class CommandTypeDC(Enum): RUN_PROCEDURE = 9 ERROR = 10 SERVER_REPLY = 11 + VIEW_FILE_OBJECT = 12 class TargetTypeDC(Enum): @@ -67,6 +68,10 @@ class FileObjectDC: purpose: Optional[FilePurposeDC] = None filepath: pathlib.Path | str = "" filedata: bytes = None + glb_file: Optional[FileObjectDC] = None + ifcsqlite_file: Optional[FileObjectDC] = None + is_procedure_output: bool = None + procedure_parent: Optional[ProcedureStartDC] = None @dataclass @@ -96,8 +101,10 @@ class SceneDC: @dataclass class ServerDC: - add_file_object: Optional[FileObjectDC] = None + new_file_object: Optional[FileObjectDC] = None all_file_objects: Optional[List[FileObjectDC]] = None + get_file_object_by_name: str = "" + get_file_object_by_path: pathlib.Path | str = "" @dataclass @@ -140,6 +147,7 @@ class ErrorDC: @dataclass class ServerReplyDC: message: str = "" + file_object: Optional[FileObjectDC] = None reply_to: Optional[CommandTypeDC] = None error: Optional[ErrorDC] = None diff --git a/src/ada/comms/fb_serializer.py b/src/ada/comms/fb_serializer.py index a886f2c9a..633ef77ab 100644 --- a/src/ada/comms/fb_serializer.py +++ b/src/ada/comms/fb_serializer.py @@ -67,6 +67,15 @@ def serialize_fileobject(builder: flatbuffers.Builder, obj: Optional[FileObjectD filedata_vector = None if obj.filedata is not None: filedata_vector = builder.CreateByteVector(obj.filedata) + glb_file_obj = None + if obj.glb_file is not None: + glb_file_obj = serialize_fileobject(builder, obj.glb_file) + ifcsqlite_file_obj = None + if obj.ifcsqlite_file is not None: + ifcsqlite_file_obj = serialize_fileobject(builder, obj.ifcsqlite_file) + procedure_parent_obj = None + if obj.procedure_parent is not None: + procedure_parent_obj = serialize_procedurestart(builder, obj.procedure_parent) FileObject.Start(builder) if name_str is not None: @@ -79,6 +88,14 @@ def serialize_fileobject(builder: flatbuffers.Builder, obj: Optional[FileObjectD FileObject.AddFilepath(builder, filepath_str) if filedata_vector is not None: FileObject.AddFiledata(builder, filedata_vector) + if obj.glb_file is not None: + FileObject.AddGlbFile(builder, glb_file_obj) + if obj.ifcsqlite_file is not None: + FileObject.AddIfcsqliteFile(builder, ifcsqlite_file_obj) + if obj.is_procedure_output is not None: + FileObject.AddIsProcedureOutput(builder, obj.is_procedure_output) + if obj.procedure_parent is not None: + FileObject.AddProcedureParent(builder, procedure_parent_obj) return FileObject.End(builder) @@ -147,9 +164,9 @@ def serialize_scene(builder: flatbuffers.Builder, obj: Optional[SceneDC]) -> Opt def serialize_server(builder: flatbuffers.Builder, obj: Optional[ServerDC]) -> Optional[int]: if obj is None: return None - add_file_object_obj = None - if obj.add_file_object is not None: - add_file_object_obj = serialize_fileobject(builder, obj.add_file_object) + new_file_object_obj = None + if obj.new_file_object is not None: + new_file_object_obj = serialize_fileobject(builder, obj.new_file_object) all_file_objects_vector = None if obj.all_file_objects is not None and len(obj.all_file_objects) > 0: all_file_objects_list = [serialize_fileobject(builder, item) for item in obj.all_file_objects] @@ -157,12 +174,22 @@ def serialize_server(builder: flatbuffers.Builder, obj: Optional[ServerDC]) -> O for item in reversed(all_file_objects_list): builder.PrependUOffsetTRelative(item) all_file_objects_vector = builder.EndVector(len(all_file_objects_list)) + get_file_object_by_name_str = None + if obj.get_file_object_by_name is not None: + get_file_object_by_name_str = builder.CreateString(str(obj.get_file_object_by_name)) + get_file_object_by_path_str = None + if obj.get_file_object_by_path is not None: + get_file_object_by_path_str = builder.CreateString(str(obj.get_file_object_by_path)) Server.Start(builder) - if obj.add_file_object is not None: - Server.AddAddFileObject(builder, add_file_object_obj) + if obj.new_file_object is not None: + Server.AddNewFileObject(builder, new_file_object_obj) if obj.all_file_objects is not None and len(obj.all_file_objects) > 0: Server.AddAllFileObjects(builder, all_file_objects_vector) + if get_file_object_by_name_str is not None: + Server.AddGetFileObjectByName(builder, get_file_object_by_name_str) + if get_file_object_by_path_str is not None: + Server.AddGetFileObjectByPath(builder, get_file_object_by_path_str) return Server.End(builder) @@ -297,6 +324,9 @@ def serialize_serverreply(builder: flatbuffers.Builder, obj: Optional[ServerRepl message_str = None if obj.message is not None: message_str = builder.CreateString(str(obj.message)) + file_object_obj = None + if obj.file_object is not None: + file_object_obj = serialize_fileobject(builder, obj.file_object) error_obj = None if obj.error is not None: error_obj = serialize_error(builder, obj.error) @@ -304,6 +334,8 @@ def serialize_serverreply(builder: flatbuffers.Builder, obj: Optional[ServerRepl ServerReply.Start(builder) if message_str is not None: ServerReply.AddMessage(builder, message_str) + if obj.file_object is not None: + ServerReply.AddFileObject(builder, file_object_obj) if obj.reply_to is not None: ServerReply.AddReplyTo(builder, obj.reply_to.value) if obj.error is not None: diff --git a/src/ada/comms/msg_handling/default_on_message.py b/src/ada/comms/msg_handling/default_on_message.py index 5f9f26933..db304bbd0 100644 --- a/src/ada/comms/msg_handling/default_on_message.py +++ b/src/ada/comms/msg_handling/default_on_message.py @@ -11,6 +11,7 @@ from ada.comms.msg_handling.run_procedure import run_procedure from ada.comms.msg_handling.update_scene import update_scene from ada.comms.msg_handling.update_server import update_server +from ada.comms.msg_handling.view_file_object import view_file_object from ada.config import logger if TYPE_CHECKING: @@ -23,7 +24,7 @@ def default_on_message(server: WebSocketAsyncServer, client: ConnectedClient, me if message.command_type == CommandTypeDC.UPDATE_SCENE: update_scene(server, client, message) elif message.command_type == CommandTypeDC.UPDATE_SERVER: - update_server(server, client, message) + update_server(server, client, message.server.new_file_object) elif message.command_type == CommandTypeDC.MESH_INFO_CALLBACK: mesh_info_callback(server, client, message) elif message.command_type == CommandTypeDC.LIST_PROCEDURES: @@ -32,6 +33,8 @@ def default_on_message(server: WebSocketAsyncServer, client: ConnectedClient, me run_procedure(server, client, message) elif message.command_type == CommandTypeDC.LIST_FILE_OBJECTS: list_file_objects(server, client, message) + elif message.command_type == CommandTypeDC.VIEW_FILE_OBJECT: + view_file_object(server, client, message) else: logger.error(f"Unknown command type: {message.command_type}") on_error_reply(server, client, error_message=f"Unknown command type: {message.command_type}") diff --git a/src/ada/comms/msg_handling/run_procedure.py b/src/ada/comms/msg_handling/run_procedure.py index 08b4cb676..fe0dae7b2 100644 --- a/src/ada/comms/msg_handling/run_procedure.py +++ b/src/ada/comms/msg_handling/run_procedure.py @@ -1,8 +1,10 @@ from __future__ import annotations +import pathlib from typing import TYPE_CHECKING -from ada.comms.fb_model_gen import MessageDC +from ada.comms.fb_model_gen import FileObjectDC, MessageDC +from ada.comms.msg_handling.update_server import update_server from ada.comms.procedures import Procedure from ada.config import logger @@ -21,3 +23,22 @@ def run_procedure(server: WebSocketAsyncServer, client: ConnectedClient, message procedure(**procedure.params) logger.info(f"Procedure {procedure.name} ran successfully") + update_server_on_successful_procedure_run(server, procedure, client, message) + + +def update_server_on_successful_procedure_run( + server: WebSocketAsyncServer, procedure: Procedure, client: ConnectedClient, message: MessageDC +) -> None: + input_file_path = pathlib.Path(procedure.params.get(procedure.input_file_var)) + server_file_object = server.scene.get_file_object(input_file_path.stem) + output_file = procedure.get_procedure_output(input_file_path.stem) + new_file_object = FileObjectDC( + name=output_file.name, + filepath=output_file, + file_type=procedure.export_file_type, + purpose=server_file_object.purpose, + is_procedure_output=True, + procedure_parent=message.procedure_store.start_procedure, + ) + update_server(server, client, new_file_object) + logger.info(f"Added {procedure.name} to the server") diff --git a/src/ada/comms/msg_handling/update_scene.py b/src/ada/comms/msg_handling/update_scene.py index e32b20452..5b1a3d25a 100644 --- a/src/ada/comms/msg_handling/update_scene.py +++ b/src/ada/comms/msg_handling/update_scene.py @@ -33,4 +33,5 @@ def update_scene(server: WebSocketAsyncServer, client: ConnectedClient, message: file_type=glb_file_data.file_type, purpose=glb_file_data.purpose, ) - server.scene.file_objects.append(file_object) + + server.scene.add_file_object(file_object) diff --git a/src/ada/comms/msg_handling/update_server.py b/src/ada/comms/msg_handling/update_server.py index 41956f387..92c18a834 100644 --- a/src/ada/comms/msg_handling/update_server.py +++ b/src/ada/comms/msg_handling/update_server.py @@ -1,35 +1,50 @@ from __future__ import annotations import pathlib +import platform +import shutil +import subprocess from typing import TYPE_CHECKING +import ada from ada.cadit.ifc.ifc2sql import Ifc2SqlPatcher from ada.cadit.ifc.sql_model import IfcSqlModel -from ada.comms.fb_model_gen import FileTypeDC, MessageDC +from ada.comms.fb_model_gen import FileObjectDC, FileTypeDC from ada.config import logger if TYPE_CHECKING: from ada.comms.wsock_server import ConnectedClient, WebSocketAsyncServer -def update_server(server: WebSocketAsyncServer, client: ConnectedClient, message: MessageDC) -> None: +def update_server(server: WebSocketAsyncServer, client: ConnectedClient, add_file: FileObjectDC) -> None: logger.info(f"Received message from {client} to update server") - logger.info(f"Message: {message}") - add_file = message.server.add_file_object if add_file.file_type == FileTypeDC.IFC and add_file.filepath: tmp_ifc_fp = pathlib.Path(add_file.filepath) if not tmp_ifc_fp.exists(): raise FileNotFoundError(f"File not found: {tmp_ifc_fp}") - tmp_sql_fp = tmp_ifc_fp.with_suffix(".sqlite") - Ifc2SqlPatcher(tmp_ifc_fp, logger, dest_sql_file=tmp_sql_fp).patch() - - server.scene.ifc_sql_store = IfcSqlModel(tmp_sql_fp) - remove_existing_idx = None - for i, fo in enumerate(server.scene.file_objects): - if fo.name == add_file.name: - remove_existing_idx = i - if remove_existing_idx is not None: - server.scene.file_objects.pop(remove_existing_idx) - - server.scene.file_objects.append(add_file) + if add_file.ifcsqlite_file is None: + tmp_sql_fp = tmp_ifc_fp.with_suffix(".sqlite") + Ifc2SqlPatcher(tmp_ifc_fp, logger, dest_sql_file=tmp_sql_fp).patch() + add_file.ifcsqlite_file = FileObjectDC( + name=add_file.name, filepath=tmp_sql_fp, file_type=FileTypeDC.SQLITE, purpose=add_file.purpose + ) + + if add_file.glb_file is None: + tmp_glb_fp = tmp_ifc_fp.with_suffix(".glb") + ifc_convert_exe = shutil.which("ifcconvert") + if platform.platform().startswith("Windows"): + ifc_convert_exe = shutil.which("ifcconvert.exe") + + if ifc_convert_exe: + subprocess.run([ifc_convert_exe, tmp_ifc_fp.as_posix(), tmp_glb_fp.as_posix()]) + else: + a = ada.from_ifc(add_file.filepath) + a.to_gltf(tmp_glb_fp) + + add_file.glb_file = FileObjectDC( + name=add_file.name, filepath=tmp_glb_fp, file_type=FileTypeDC.GLB, purpose=add_file.purpose + ) + + server.scene.ifc_sql_store = IfcSqlModel(add_file.ifcsqlite_file.filepath) + server.scene.add_file_object(add_file) diff --git a/src/ada/comms/msg_handling/view_file_object.py b/src/ada/comms/msg_handling/view_file_object.py new file mode 100644 index 000000000..dc69a3a3e --- /dev/null +++ b/src/ada/comms/msg_handling/view_file_object.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import asyncio +import io +from typing import TYPE_CHECKING + +import trimesh + +from ada.comms.exceptions import ServerError +from ada.comms.fb_model_gen import ( + CommandTypeDC, + FileObjectDC, + FileTypeDC, + MessageDC, + SceneDC, + ServerReplyDC, +) +from ada.comms.fb_serializer import serialize_message +from ada.config import logger + +if TYPE_CHECKING: + from ada.comms.wsock_server import ConnectedClient, WebSocketAsyncServer + + +def view_file_object(server: WebSocketAsyncServer, client: ConnectedClient, message: MessageDC) -> None: + logger.info(f"Received message from {client} to get file object") + file_object_name = message.server.get_file_object_by_name + result = server.scene.get_file_object(file_object_name) + if result is None: + raise ServerError(f"File object {file_object_name} not found") + + glb_file_obj = result.glb_file + scene = trimesh.load(glb_file_obj.filepath) + with io.BytesIO() as data: + scene.export( + file_obj=data, + file_type="glb", + ) + file_object = FileObjectDC( + name=glb_file_obj.name, file_type=FileTypeDC.GLB, purpose=glb_file_obj.purpose, filedata=data.getvalue() + ) + + msg = MessageDC( + instance_id=server.instance_id, + command_type=CommandTypeDC.SERVER_REPLY, + server_reply=ServerReplyDC(reply_to=message.command_type, file_object=file_object), + scene=SceneDC(current_file=file_object), + target_id=client.instance_id, + target_group=client.group_type, + ) + + fb_message = serialize_message(msg) + asyncio.run(client.websocket.send(fb_message)) diff --git a/src/ada/comms/procedures.py b/src/ada/comms/procedures.py index ea962a7c0..d4a0727b4 100644 --- a/src/ada/comms/procedures.py +++ b/src/ada/comms/procedures.py @@ -6,7 +6,10 @@ from dataclasses import dataclass, field from typing import Callable, Optional -import typer +try: + from typer import Typer +except ImportError: + Typer = None from ada.comms.fb_model_gen import ( FileTypeDC, @@ -64,13 +67,40 @@ def to_procedure_dc(self) -> ProcedureDC: description=self.description, script_file_location=self.script_path.as_posix() if self.script_path is not None else "", parameters=( - [ParameterDC(name=key, value=val) for key, val in self.params.items()] if self.params is not None else None + [ParameterDC(name=key, value=val) for key, val in self.params.items()] + if self.params is not None + else None ), input_file_var=self.input_file_var, input_file_type=self.input_file_type, export_file_type=self.export_file_type, ) + def get_procedure_output(self, input_file_name: str): + from ada.comms.scene_model import Scene + + temp_dir = Scene.get_temp_dir() + procedure_dir = temp_dir / "procedural" + + output_file_path = None + for fp in procedure_dir.iterdir(): + if not fp.is_dir(): + continue + dir_name = fp.stem + if dir_name == input_file_name: + output_file_path = fp + break + + if output_file_path is None: + raise FileNotFoundError(f"Output file for procedure {self.name} not found") + + if self.export_file_type == FileTypeDC.IFC: + return (output_file_path / self.name).with_suffix(".ifc") + elif self.export_file_type == FileTypeDC.GLB: + return (output_file_path / self.name).with_suffix(".glb") + else: + raise NotImplementedError(f"Export file type {self.export_file_type} not implemented") + @dataclass class ProcedureStore: @@ -107,7 +137,7 @@ def get_procedure_from_function(func: Callable) -> Procedure: def procedure_decorator( - app: typer.Typer, + app: Typer, input_file_var: str | None = None, input_file_type: FileTypeDC | None = None, export_file_type: FileTypeDC | None = None, @@ -116,7 +146,8 @@ def wrapper(func: Callable) -> Callable: func.input_file_var = input_file_var func.input_file_type = input_file_type func.export_file_type = export_file_type - app.command()(func) # Apply the app.command decorator + if Typer is not None: + app.command()(func) # Apply the app.command decorator return func return wrapper diff --git a/src/ada/comms/scene_model.py b/src/ada/comms/scene_model.py new file mode 100644 index 000000000..7e67e9bd6 --- /dev/null +++ b/src/ada/comms/scene_model.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import pathlib +from dataclasses import dataclass, field + +from ada.cadit.ifc.sql_model import IfcSqlModel +from ada.comms.fb_model_gen import FileObjectDC, FilePurposeDC, FileTypeDC +from ada.config import Config + + +@dataclass +class Scene: + file_objects: list[FileObjectDC] = field(default_factory=list) + ifc_sql_store: IfcSqlModel = None + mesh_meta: dict = None + + def get_file_object(self, name: str) -> FileObjectDC | None: + for fo in self.file_objects: + if fo.name == name: + return fo + return None + + def add_file_object(self, file_object: FileObjectDC): + remove_existing_idx = None + for i, fo in enumerate(self.file_objects): + if fo.name == file_object.name: + remove_existing_idx = i + + if remove_existing_idx is not None: + self.file_objects.pop(remove_existing_idx) + + self.file_objects.append(file_object) + + def __post_init__(self): + # check temp directory for any file objects + if Config().websockets_server_temp_dir is not None and Config().websockets_auto_load_files is True: + temp_dir = Config().websockets_server_temp_dir + if not temp_dir.exists(): + return + for fp in temp_dir.iterdir(): + if not fp.is_file(): + continue + if fp.suffix == ".ifc": + glb_fp = fp.with_suffix(".glb") + ifc_sqlite_fp = fp.with_suffix(".sqlite") + ifc_sqlite_file = None + if ifc_sqlite_fp.exists(): + ifc_sqlite_file = FileObjectDC( + name=fp.stem, + filepath=ifc_sqlite_fp, + file_type=FileTypeDC.IFC, + purpose=FilePurposeDC.DESIGN, + ) + glb_file_object = None + if glb_fp.exists(): + glb_file_object = FileObjectDC( + name=fp.stem, + filepath=glb_fp, + file_type=FileTypeDC.GLB, + purpose=FilePurposeDC.DESIGN, + ) + file_object = FileObjectDC( + name=fp.stem, + filepath=fp, + file_type=FileTypeDC.IFC, + purpose=FilePurposeDC.DESIGN, + glb_file=glb_file_object, + ifcsqlite_file=ifc_sqlite_file, + ) + self.add_file_object(file_object) + + @staticmethod + def get_temp_dir() -> pathlib.Path: + if Config().websockets_server_temp_dir is not None: + return Config().websockets_server_temp_dir + + return pathlib.Path("temp") diff --git a/src/ada/comms/wsock/CommandType.py b/src/ada/comms/wsock/CommandType.py index bbca44c62..227e2977a 100644 --- a/src/ada/comms/wsock/CommandType.py +++ b/src/ada/comms/wsock/CommandType.py @@ -16,3 +16,4 @@ class CommandType(object): RUN_PROCEDURE = 9 ERROR = 10 SERVER_REPLY = 11 + VIEW_FILE_OBJECT = 12 diff --git a/src/ada/comms/wsock/FileObject.py b/src/ada/comms/wsock/FileObject.py index fb54bfcb9..5a2123412 100644 --- a/src/ada/comms/wsock/FileObject.py +++ b/src/ada/comms/wsock/FileObject.py @@ -84,9 +84,52 @@ def FiledataIsNone(self): o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) return o == 0 + # FileObject + def GlbFile(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + if o != 0: + x = self._tab.Indirect(o + self._tab.Pos) + from ada.comms.wsock.FileObject import FileObject + + obj = FileObject() + obj.Init(self._tab.Bytes, x) + return obj + return None + + # FileObject + def IfcsqliteFile(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + x = self._tab.Indirect(o + self._tab.Pos) + from ada.comms.wsock.FileObject import FileObject + + obj = FileObject() + obj.Init(self._tab.Bytes, x) + return obj + return None + + # FileObject + def IsProcedureOutput(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # FileObject + def ProcedureParent(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20)) + if o != 0: + x = self._tab.Indirect(o + self._tab.Pos) + from ada.comms.wsock.ProcedureStart import ProcedureStart + + obj = ProcedureStart() + obj.Init(self._tab.Bytes, x) + return obj + return None + def FileObjectStart(builder): - builder.StartObject(5) + builder.StartObject(9) def Start(builder): @@ -141,6 +184,38 @@ def StartFiledataVector(builder, numElems): return FileObjectStartFiledataVector(builder, numElems) +def FileObjectAddGlbFile(builder, glbFile): + builder.PrependUOffsetTRelativeSlot(5, flatbuffers.number_types.UOffsetTFlags.py_type(glbFile), 0) + + +def AddGlbFile(builder, glbFile): + FileObjectAddGlbFile(builder, glbFile) + + +def FileObjectAddIfcsqliteFile(builder, ifcsqliteFile): + builder.PrependUOffsetTRelativeSlot(6, flatbuffers.number_types.UOffsetTFlags.py_type(ifcsqliteFile), 0) + + +def AddIfcsqliteFile(builder, ifcsqliteFile): + FileObjectAddIfcsqliteFile(builder, ifcsqliteFile) + + +def FileObjectAddIsProcedureOutput(builder, isProcedureOutput): + builder.PrependBoolSlot(7, isProcedureOutput, 0) + + +def AddIsProcedureOutput(builder, isProcedureOutput): + FileObjectAddIsProcedureOutput(builder, isProcedureOutput) + + +def FileObjectAddProcedureParent(builder, procedureParent): + builder.PrependUOffsetTRelativeSlot(8, flatbuffers.number_types.UOffsetTFlags.py_type(procedureParent), 0) + + +def AddProcedureParent(builder, procedureParent): + FileObjectAddProcedureParent(builder, procedureParent) + + def FileObjectEnd(builder): return builder.EndObject() diff --git a/src/ada/comms/wsock/Server.py b/src/ada/comms/wsock/Server.py index 376837cd9..c6fd4ab01 100644 --- a/src/ada/comms/wsock/Server.py +++ b/src/ada/comms/wsock/Server.py @@ -28,7 +28,7 @@ def Init(self, buf, pos): self._tab = flatbuffers.table.Table(buf, pos) # Server - def AddFileObject(self): + def NewFileObject(self): o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) if o != 0: x = self._tab.Indirect(o + self._tab.Pos) @@ -65,21 +65,35 @@ def AllFileObjectsIsNone(self): o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) return o == 0 + # Server + def GetFileObjectByName(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # Server + def GetFileObjectByPath(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + def ServerStart(builder): - builder.StartObject(2) + builder.StartObject(4) def Start(builder): ServerStart(builder) -def ServerAddAddFileObject(builder, addFileObject): - builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(addFileObject), 0) +def ServerAddNewFileObject(builder, newFileObject): + builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(newFileObject), 0) -def AddAddFileObject(builder, addFileObject): - ServerAddAddFileObject(builder, addFileObject) +def AddNewFileObject(builder, newFileObject): + ServerAddNewFileObject(builder, newFileObject) def ServerAddAllFileObjects(builder, allFileObjects): @@ -98,6 +112,22 @@ def StartAllFileObjectsVector(builder, numElems): return ServerStartAllFileObjectsVector(builder, numElems) +def ServerAddGetFileObjectByName(builder, getFileObjectByName): + builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(getFileObjectByName), 0) + + +def AddGetFileObjectByName(builder, getFileObjectByName): + ServerAddGetFileObjectByName(builder, getFileObjectByName) + + +def ServerAddGetFileObjectByPath(builder, getFileObjectByPath): + builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(getFileObjectByPath), 0) + + +def AddGetFileObjectByPath(builder, getFileObjectByPath): + ServerAddGetFileObjectByPath(builder, getFileObjectByPath) + + def ServerEnd(builder): return builder.EndObject() diff --git a/src/ada/comms/wsock/ServerReply.py b/src/ada/comms/wsock/ServerReply.py index d16934b65..c3a5f9271 100644 --- a/src/ada/comms/wsock/ServerReply.py +++ b/src/ada/comms/wsock/ServerReply.py @@ -35,15 +35,27 @@ def Message(self): return None # ServerReply - def ReplyTo(self): + def FileObject(self): o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + x = self._tab.Indirect(o + self._tab.Pos) + from ada.comms.wsock.FileObject import FileObject + + obj = FileObject() + obj.Init(self._tab.Bytes, x) + return obj + return None + + # ServerReply + def ReplyTo(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) if o != 0: return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) return 0 # ServerReply def Error(self): - o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) if o != 0: x = self._tab.Indirect(o + self._tab.Pos) from ada.comms.wsock.Error import Error @@ -55,7 +67,7 @@ def Error(self): def ServerReplyStart(builder): - builder.StartObject(3) + builder.StartObject(4) def Start(builder): @@ -70,8 +82,16 @@ def AddMessage(builder, message): ServerReplyAddMessage(builder, message) +def ServerReplyAddFileObject(builder, fileObject): + builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(fileObject), 0) + + +def AddFileObject(builder, fileObject): + ServerReplyAddFileObject(builder, fileObject) + + def ServerReplyAddReplyTo(builder, replyTo): - builder.PrependInt8Slot(1, replyTo, 0) + builder.PrependInt8Slot(2, replyTo, 0) def AddReplyTo(builder, replyTo): @@ -79,7 +99,7 @@ def AddReplyTo(builder, replyTo): def ServerReplyAddError(builder, error): - builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(error), 0) + builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(error), 0) def AddError(builder, error): diff --git a/src/ada/comms/wsock_client_async.py b/src/ada/comms/wsock_client_async.py index 68eba8ff5..bd65575a7 100644 --- a/src/ada/comms/wsock_client_async.py +++ b/src/ada/comms/wsock_client_async.py @@ -101,6 +101,11 @@ async def list_server_file_objects(self) -> list[FileObjectDC]: msg = await self.receive() return msg.server.all_file_objects + async def get_file_object(self, name: str) -> FileObjectDC: + await self.websocket.send(self._get_file_object_prep(name)) + msg = await self.receive() + return msg.server.file + async def run_procedure(self, procedure: ProcedureStartDC) -> None: """Runs a procedure with the given name and arguments.""" await self.websocket.send(self._run_procedure_prep(procedure)) diff --git a/src/ada/comms/wsock_client_base.py b/src/ada/comms/wsock_client_base.py index 806886f98..b288a4374 100644 --- a/src/ada/comms/wsock_client_base.py +++ b/src/ada/comms/wsock_client_base.py @@ -86,7 +86,7 @@ def _update_file_server_prep(self, file_object: FileObjectDC) -> bytes: message = MessageDC( instance_id=self.instance_id, command_type=CommandTypeDC.UPDATE_SERVER, - server=ServerDC(add_file_object=file_object), + server=ServerDC(new_file_object=file_object), target_group=TargetTypeDC.SERVER, ) return serialize_message(message) @@ -116,6 +116,15 @@ def _list_server_file_objects_prep(self) -> bytes: ) return serialize_message(message) + def _get_file_object_prep(self, file_name): + message = MessageDC( + instance_id=self.instance_id, + command_type=CommandTypeDC.GET_FILE_OBJECT, + target_group=TargetTypeDC.SERVER, + server=ServerDC(get_file_object_by_name=file_name), + ) + return serialize_message(message) + @abstractmethod def connect(self): pass @@ -155,6 +164,10 @@ def list_procedures(self) -> list[ProcedureDC]: def list_server_file_objects(self) -> list[FileObjectDC]: pass + @abstractmethod + def get_file_object(self, name: str) -> FileObjectDC: + pass + @abstractmethod def run_procedure(self, procedure: ProcedureStartDC) -> None: pass diff --git a/src/ada/comms/wsock_server.py b/src/ada/comms/wsock_server.py index 8260c1050..0f572b68f 100644 --- a/src/ada/comms/wsock_server.py +++ b/src/ada/comms/wsock_server.py @@ -8,10 +8,10 @@ import websockets -from ada.cadit.ifc.sql_model import IfcSqlModel -from ada.comms.fb_model_gen import CommandTypeDC, FileObjectDC, MessageDC, TargetTypeDC +from ada.comms.fb_model_gen import CommandTypeDC, MessageDC, TargetTypeDC from ada.comms.msg_handling.default_on_message import default_on_message from ada.comms.procedures import ProcedureStore +from ada.comms.scene_model import Scene from ada.comms.wsock import Message from ada.comms.wsockets_utils import client_from_str from ada.config import logger @@ -53,13 +53,6 @@ async def process_client(websocket, path) -> ConnectedClient: return client -@dataclass -class Scene: - file_objects: list[FileObjectDC] = field(default_factory=list) - ifc_sql_store: IfcSqlModel = None - mesh_meta: dict = None - - async def retry_message_sending( server: WebSocketAsyncServer, message: bytes, sender: ConnectedClient, msg: MessageDC, num_retries: int = 3 ): diff --git a/src/ada/config.py b/src/ada/config.py index b3f71914d..768c44f10 100644 --- a/src/ada/config.py +++ b/src/ada/config.py @@ -147,6 +147,7 @@ class Config: "websockets", [ ConfigEntry("server_temp_dir", pathlib.Path, None, False), + ConfigEntry("auto_load_files", bool, False, False), ], ), ] diff --git a/src/ada/visit/renderer_manager.py b/src/ada/visit/renderer_manager.py index 56572c5be..080ddbafc 100644 --- a/src/ada/visit/renderer_manager.py +++ b/src/ada/visit/renderer_manager.py @@ -14,6 +14,7 @@ SceneDC, SceneOperationsDC, ) +from ada.config import Config if TYPE_CHECKING: from IPython.display import HTML @@ -260,10 +261,13 @@ def render(self, obj: BackendGeom | Part | Assembly | FEAResult, params: RenderP ) if params.add_ifc_backend is True and type(obj) is Assembly: - if params.backend_file_dir is None: - backend_file_dir = pathlib.Path.cwd() / "temp" - else: + server_temp = Config().websockets_server_temp_dir + if server_temp is not None: + backend_file_dir = server_temp + elif params.backend_file_dir is not None: backend_file_dir = params.backend_file_dir + else: + backend_file_dir = pathlib.Path.cwd() / "temp" if isinstance(backend_file_dir, str): backend_file_dir = pathlib.Path(backend_file_dir) diff --git a/src/flatbuffers/schemas/commands.fbs b/src/flatbuffers/schemas/commands.fbs index 696910c49..2135aeda9 100644 --- a/src/flatbuffers/schemas/commands.fbs +++ b/src/flatbuffers/schemas/commands.fbs @@ -13,6 +13,7 @@ enum CommandType : byte { RUN_PROCEDURE = 9, ERROR = 10, SERVER_REPLY = 11, + VIEW_FILE_OBJECT = 12, } enum TargetType : byte { @@ -59,6 +60,11 @@ table FileObject { purpose: FilePurpose; // Purpose of the file filepath: string; // Path to the file filedata: [ubyte]; // Binary data (e.g., IFC, GLB, SQLite) + glb_file: FileObject; // GLB file object converted from original file + ifcsqlite_file: FileObject; // IF, the file object is IFC, this is a Boolean to indicate if the ifc file has a corresponding sqlite file + // procedure related configs + is_procedure_output: bool; // Boolean to indicate if the file object is the output of a procedure + procedure_parent: ProcedureStart; // Name of the procedure that generated the file object } table MeshInfo { @@ -84,8 +90,10 @@ table Scene { } table Server { - add_file_object: FileObject; + new_file_object: FileObject; all_file_objects: [FileObject]; + get_file_object_by_name: string; + get_file_object_by_path: string; } // Procedures are picked up by the server and executed @@ -125,6 +133,7 @@ table Error { table ServerReply { message: string; + file_object: FileObject; reply_to: CommandType; error: Error; } diff --git a/src/frontend/src/components/node_editor/InfoPanel.tsx b/src/frontend/src/components/node_editor/InfoPanel.tsx deleted file mode 100644 index fb396b42c..000000000 --- a/src/frontend/src/components/node_editor/InfoPanel.tsx +++ /dev/null @@ -1,55 +0,0 @@ -// InfoPanel.tsx -import React from 'react'; -import { Rnd } from 'react-rnd'; - -type InfoPanelProps = { - show: boolean; - onClose: () => void; -}; - -const InfoPanel: React.FC = ({ show, onClose }) => { - if (!show) return null; // Return null if the panel should not be displayed - - return ( - - {/* Info Panel Header */} -
-
-
Info Panel
- -
-
- {/* Info Panel Content */} -
-

Procedure Information

-

- This panel displays detailed information about the selected node or procedure. - Use this space to show properties, descriptions, and other metadata. -

- {/* Add more detailed information here as needed */} -
-
- ); -}; - -export default InfoPanel; diff --git a/src/frontend/src/components/node_editor/NodeEditorComponent.tsx b/src/frontend/src/components/node_editor/NodeEditorComponent.tsx index d4c6cdd03..d83ed586f 100644 --- a/src/frontend/src/components/node_editor/NodeEditorComponent.tsx +++ b/src/frontend/src/components/node_editor/NodeEditorComponent.tsx @@ -1,17 +1,9 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import {Rnd} from 'react-rnd'; import {Background, Controls, MiniMap, ReactFlow} from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import {request_list_of_nodes} from "../../utils/node_editor/request_list_of_nodes"; import {useNodeEditorStore} from '../../state/useNodeEditorStore'; // Import the Zustand store - - -const info_svg = - - - import ProcedureNode from './customProcedureNode'; import CustomFileObjectNode from './customFileObjectNode'; @@ -54,10 +46,6 @@ const NodeEditorComponent: React.FC = () => { > Update - {/* Content Area */} diff --git a/src/frontend/src/components/node_editor/customFileObjectNode.tsx b/src/frontend/src/components/node_editor/customFileObjectNode.tsx index d0e9c54a5..70a396267 100644 --- a/src/frontend/src/components/node_editor/customFileObjectNode.tsx +++ b/src/frontend/src/components/node_editor/customFileObjectNode.tsx @@ -1,12 +1,17 @@ import React, {memo} from 'react'; -import {Handle, NodeProps, Position, Connection} from '@xyflow/react'; -import {run_sequence} from "../../utils/node_editor/run_sequence"; -import {get_file_object_from_server} from "../../utils/scene/get_file_object_from_server"; +import {Handle, Position} from '@xyflow/react'; +import {view_file_object_from_server} from "../../utils/scene/view_file_object_from_server"; +import {FileObject} from "../../flatbuffers/wsock/file-object"; const doc_icon = +const view_icon = + + -function CustomFileObjectNode(props: { id: string, data: Record }) { +function CustomFileObjectNode(props: { id: string, data: Record }) { // Custom connection validation function for `file_object` handle const isValidConnection = () => { console.log("isValidConnection"); @@ -15,19 +20,19 @@ function CustomFileObjectNode(props: { id: string, data: Record return (
{/* Header Row */} -
+
{doc_icon}
-
{props.data.label}
-
[{props.data.filetype}]
+
{props.data.label.toString()}
+
[{props.data.filetype.toString()}]
-
+
diff --git a/src/frontend/src/components/node_editor/customProcedureNode.tsx b/src/frontend/src/components/node_editor/customProcedureNode.tsx index 99af09f08..8cc8de1b1 100644 --- a/src/frontend/src/components/node_editor/customProcedureNode.tsx +++ b/src/frontend/src/components/node_editor/customProcedureNode.tsx @@ -2,15 +2,10 @@ import React, {memo} from 'react'; import {Handle, NodeProps, Position, Connection} from '@xyflow/react'; import {run_sequence} from "../../utils/node_editor/run_sequence"; -const procedure_icon = - +const procedure_icon = - + d="M8.5,6H6.7C8.2,4.7,10,4,12,4c0.3,0,0.6,0,0.9,0.1c0,0,0,0,0,0c0.5,0.1,1-0.3,1.1-0.9c0.1-0.5-0.3-1-0.9-1.1C12.7,2,12.4,2,12,2C9.6,2,7.3,2.9,5.5,4.4V3c0-0.6-0.4-1-1-1s-1,0.4-1,1v4c0,0.6,0.4,1,1,1h4c0.6,0,1-0.4,1-1S9.1,6,8.5,6z M7,14.5c-0.6,0-1,0.4-1,1v1.8C4.7,15.8,4,14,4,12c0-0.3,0-0.6,0.1-0.9c0,0,0,0,0,0c0.1-0.5-0.3-1-0.9-1.1c-0.5-0.1-1,0.3-1.1,0.9C2,11.3,2,11.6,2,12c0,2.4,0.9,4.7,2.4,6.5H3c-0.6,0-1,0.4-1,1s0.4,1,1,1h4c0.3,0,0.6-0.2,0.8-0.4c0,0,0,0,0,0c0,0,0,0,0,0c0-0.1,0.1-0.2,0.1-0.3c0-0.1,0-0.1,0-0.2c0,0,0-0.1,0-0.1v-4C8,14.9,7.6,14.5,7,14.5z M21,5.5c0.6,0,1-0.4,1-1s-0.4-1-1-1h-4c-0.1,0-0.1,0-0.2,0c0,0,0,0,0,0c-0.1,0-0.2,0.1-0.3,0.1c0,0,0,0,0,0c-0.1,0.1-0.2,0.1-0.2,0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0.1-0.1,0.2-0.1,0.2c0,0.1,0,0.1,0,0.2c0,0,0,0.1,0,0.1v4c0,0.6,0.4,1,1,1s1-0.4,1-1V6.7c1.3,1.4,2,3.3,2,5.3c0,0.3,0,0.6-0.1,0.9c-0.1,0.5,0.3,1,0.9,1.1c0,0,0.1,0,0.1,0c0.5,0,0.9-0.4,1-0.9c0-0.4,0.1-0.7,0.1-1.1c0-2.4-0.9-4.7-2.4-6.5H21z M20.3,16.5c-0.1-0.1-0.2-0.2-0.3-0.3c0,0,0,0,0,0c0,0,0,0,0,0c-0.1-0.1-0.2-0.1-0.3-0.1c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0h-4c-0.6,0-1,0.4-1,1s0.4,1,1,1h1.8c-1.4,1.3-3.3,2-5.3,2c-0.3,0-0.6,0-0.9-0.1c0,0,0,0,0,0c-0.5-0.1-1,0.3-1.1,0.9s0.3,1,0.9,1.1c0.4,0,0.7,0.1,1.1,0.1c2.4,0,4.7-0.9,6.5-2.4V21c0,0.6,0.4,1,1,1s1-0.4,1-1v-4C20.5,16.8,20.4,16.6,20.3,16.5C20.3,16.5,20.3,16.5,20.3,16.5z"/> @@ -25,9 +20,9 @@ function ProcedureNode(props: { id: string, data: Record }) {
{/* Header Row */}
-
-
{procedure_icon}
-
{props.data.label}
+
+
{procedure_icon}
+
{props.data.label}