From bc64e5e4ee8def04f0fa7699e2fed280efe3d1eb Mon Sep 17 00:00:00 2001 From: Brady Johnston Date: Thu, 21 Nov 2024 11:41:18 +0800 Subject: [PATCH 1/4] Update session.py --- molecularnodes/session.py | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/molecularnodes/session.py b/molecularnodes/session.py index 87415006..b9718070 100644 --- a/molecularnodes/session.py +++ b/molecularnodes/session.py @@ -3,6 +3,7 @@ from typing import Dict, Union import bpy +from pathlib import Path from bpy.app.handlers import persistent from bpy.props import StringProperty from bpy.types import Context @@ -40,9 +41,22 @@ def trim(dictionary: dict): return dictionary +def path_relative_to_blend_wd(filepath: str | Path) -> Path: + "Get the path of something, relative to the working directory of the current .blend file" + blend_working_directory = bpy.path.abspath("//") + if blend_working_directory == "": + raise ValueError( + "Unable to get current working directly, .blend file not saved" + ) + + return Path(filepath).relative_to(Path(blend_working_directory)) + + def make_paths_relative(trajectories: Dict[str, Trajectory]) -> None: for key, traj in trajectories.items(): - traj.universe.load_new(make_path_relative(traj.universe.trajectory.filename)) + traj.universe.load_new( + path_relative_to_blend_wd(traj.universe.trajectory.filename) + ) traj.save_filepaths_on_object() @@ -51,27 +65,6 @@ def trim_root_folder(filename): return os.sep.join(filename.split(os.sep)[1:]) -def make_path_relative(filepath): - "Take a path and make it relative, in an actually usable way" - try: - filepath = os.path.relpath(filepath) - except ValueError: - return filepath - - # count the number of "../../../" there are to remove - n_to_remove = int(filepath.count("..") - 2) - # get the filepath without the huge number of "../../../../" at the start - sans_relative = filepath.split("..")[-1] - - if n_to_remove < 1: - return filepath - - for i in range(n_to_remove): - sans_relative = trim_root_folder(sans_relative) - - return f"./{sans_relative}" - - class MNSession: def __init__(self) -> None: self.molecules: Dict[str, Molecule] = {} From fb0d7d06ce12c38fa9913b1267caa1c1a69d2897 Mon Sep 17 00:00:00 2001 From: Brady Johnston Date: Thu, 21 Nov 2024 13:34:49 +0800 Subject: [PATCH 2/4] working refactor --- molecularnodes/bpyd/object.py | 28 +-- molecularnodes/entities/entity.py | 22 +- molecularnodes/entities/molecule/molecule.py | 15 +- .../entities/trajectory/trajectory.py | 2 +- molecularnodes/session.py | 191 ++++++++---------- molecularnodes/ui/panel.py | 2 - tests/test_trajectory.py | 3 +- 7 files changed, 125 insertions(+), 138 deletions(-) diff --git a/molecularnodes/bpyd/object.py b/molecularnodes/bpyd/object.py index e6f5cd26..92514343 100644 --- a/molecularnodes/bpyd/object.py +++ b/molecularnodes/bpyd/object.py @@ -95,7 +95,7 @@ def __init__(self, obj: Object | None): """ if not isinstance(obj, Object): raise ValueError(f"{obj} must be a Blender object of type Object") - self._object = obj + self._object_name = obj.name @property def object(self) -> Object | None: @@ -107,30 +107,8 @@ def object(self) -> Object | None: Object | None The Blender object, or None if not found. """ - # If we don't have connection to an object, attempt to re-stablish to a new - # object in the scene with the same UUID. This helps if duplicating / deleting - # objects in the scene, but sometimes Blender just loses reference to the object - # we are working with because we are manually setting the data on the mesh, - # which can wreak havoc on the object database. To protect against this, - # if we have a broken link we just attempt to find a new suitable object for it - try: - # if the connection is broken then trying to the name will raise a connection - # error. If we are loading from a saved session then the object_ref will be - # None and get an AttributeError - self._object.name - return self._object - except (ReferenceError, AttributeError): - for obj in bpy.data.objects: - if obj.mn.uuid == self.uuid: - print( - Warning( - f"Lost connection to object: {self._object}, now connected to {obj}" - ) - ) - self._object = obj - return obj - return None + return bpy.data.objects.get(self._object_name) @object.setter def object(self, value: Object) -> None: @@ -142,7 +120,7 @@ def object(self, value: Object) -> None: value : Object The Blender object to set. """ - self._object = value + self._object_name = value.name def store_named_attribute( self, diff --git a/molecularnodes/entities/entity.py b/molecularnodes/entities/entity.py index 0cd3f3de..3ffc2144 100644 --- a/molecularnodes/entities/entity.py +++ b/molecularnodes/entities/entity.py @@ -1,6 +1,7 @@ from abc import ABCMeta import bpy from uuid import uuid1 +from bpy.types import Object from ..bpyd import ( BlenderObject, ) @@ -13,8 +14,27 @@ class MolecularEntity( def __init__(self) -> None: self.uuid: str = str(uuid1()) self.type: str = "" - self._object: bpy.types.Object | None @property def bob(self) -> BlenderObject: return BlenderObject(self.object) + + @property + def object(self) -> Object: + try: + return bpy.data.objects[self._object_name] + except KeyError: + # if we can't find a refernce to the object via a name, then we look via the + # unique uuids that were assigned to the object and the entity to match up + for obj in bpy.data.objects: + if obj.mn.uuid == self.uuid: + self._object_name = obj.name + return obj + + @object.setter + def object(self, value) -> None: + if not isinstance(value, Object): + raise ValueError( + f"Can only set object to be of type bpy.types.Object, not {type(value)=}" + ) + self._object_name = value.name diff --git a/molecularnodes/entities/molecule/molecule.py b/molecularnodes/entities/molecule/molecule.py index 79999f34..039c37f2 100644 --- a/molecularnodes/entities/molecule/molecule.py +++ b/molecularnodes/entities/molecule/molecule.py @@ -4,6 +4,7 @@ from abc import ABCMeta from pathlib import Path from typing import Optional, Tuple, Union +from bpy.types import Collection import biotite.structure as struc import bpy @@ -71,11 +72,21 @@ def __init__(self, file_path: Union[str, Path, io.BytesIO]): self._parse_filepath(file_path=file_path) self.file: str self.array: np.ndarray - self.frames: bpy.types.Collection | None = None - self.frames_name: str = "" + self._frames_collection_name: str = "" bpy.context.scene.MNSession.molecules[self.uuid] = self + @property + def frames(self) -> Collection: + return bpy.data.collections.get(self._frames_collection_name) + + @frames.setter + def frames(self, value) -> None: + if value is None: + self._frames_collection_name = None + else: + self._frames_collection_name = value.name + @classmethod def _read(self, file_path: Union[Path, io.BytesIO]): """ diff --git a/molecularnodes/entities/trajectory/trajectory.py b/molecularnodes/entities/trajectory/trajectory.py index bad87105..a728ec64 100644 --- a/molecularnodes/entities/trajectory/trajectory.py +++ b/molecularnodes/entities/trajectory/trajectory.py @@ -21,7 +21,7 @@ def __init__(self, universe: mda.Universe, world_scale: float = 0.01): self.calculations: Dict[str, Callable] = {} self.world_scale = world_scale self.frame_mapping: npt.NDArray[np.in64] | None = None - bpy.context.scene.MNSession.trajectories[self.uuid] = self + bpy.context.scene.MNSession.entities[self.uuid] = self def selection_from_ui(self, ui_item: TrajectorySelectionItem) -> Selection: self.selections[ui_item.name] = Selection( diff --git a/molecularnodes/session.py b/molecularnodes/session.py index b9718070..eab2148b 100644 --- a/molecularnodes/session.py +++ b/molecularnodes/session.py @@ -13,34 +13,6 @@ from .entities.trajectory.trajectory import Trajectory -def trim(dictionary: dict): - to_pop = [] - for name, item in dictionary.items(): - # currently there are problems with pickling the functions so we have to just - # clean up any calculations that are created on saving. Could potentially convert - # it to a string and back but that is likely a job for better implementations - if hasattr(item, "calculations"): - item.calculations = {} - try: - item.object = None - if hasattr(item, "frames"): - if isinstance(item.frames, bpy.types.Collection): - item.frames_name = item.frames.name - item.frames = None - - except ReferenceError as e: - to_pop.append(name) - print( - Warning( - f"Object reference for {item} broken, removing this item from the session: `{e}`" - ) - ) - - for name in to_pop: - dictionary.pop(name) - return dictionary - - def path_relative_to_blend_wd(filepath: str | Path) -> Path: "Get the path of something, relative to the working directory of the current .blend file" blend_working_directory = bpy.path.abspath("//") @@ -60,124 +32,130 @@ def make_paths_relative(trajectories: Dict[str, Trajectory]) -> None: traj.save_filepaths_on_object() -def trim_root_folder(filename): - "Remove one of the prefix folders from a filepath" - return os.sep.join(filename.split(os.sep)[1:]) +def find_matching_object(uuid): + for obj in bpy.data.objects: + if obj.mn.uuid == uuid: + return obj + + return None class MNSession: def __init__(self) -> None: - self.molecules: Dict[str, Molecule] = {} - self.trajectories: Dict[str, Trajectory] = {} - self.ensembles: Dict[str, Ensemble] = {} - - def items(self): - "Return UUID and item for all molecules, trajectories and ensembles being tracked." - return ( - list(self.molecules.items()) - + list(self.trajectories.items()) - + list(self.ensembles.items()) - ) - - def get_object(self, uuid: str) -> bpy.types.Object | None: - """ - Try and get an object from Blender's object database that matches the uuid given. + self.entities: Dict[str, Molecule | Trajectory | Ensemble] = {} - If nothing is be found to match, return None. - """ - for obj in bpy.data.objects: - try: - if obj.mn.uuid == uuid: - return obj - except Exception as e: - print(e) + @property + def molecules(self) -> dict: + return { + key: mol for key, mol in self.entities.items() if isinstance(mol, Molecule) + } - return None + @property + def trajectories(self) -> dict: + return { + key: traj + for key, traj in self.entities.items() + if isinstance(traj, Trajectory) + } - def remove(self, uuid: str) -> None: - "Remove the item from the list." - self.molecules.pop(uuid, None) - self.trajectories.pop(uuid, None) - self.ensembles.pop(uuid, None) + @property + def ensembles(self) -> dict: + return { + key: ens for key, ens in self.entities.items() if isinstance(ens, Ensemble) + } def get(self, uuid: str) -> Union[Molecule, Trajectory, Ensemble]: - for id, item in self.items(): - if item.uuid == uuid: - return item + return self.entities.get(uuid) - return None + def __repr__(self) -> str: + return f"MNSession with {len(self.molecules)} molecules, {len(self.trajectories)} trajectories and {len(self.ensembles)} ensembles." - @property - def n_items(self) -> int: - "The number of items being tracked by this session." - length = 0 + def __len__(self) -> int: + return len(self.entities) - for dic in [self.molecules, self.trajectories, self.ensembles]: - length += len(dic) - return length + def trim(self) -> None: + to_pop = [] + for name, item in self.entities.items(): + # currently there are problems with pickling the functions so we have to just + # clean up any calculations that are created on saving. Could potentially convert + # it to a string and back but that is likely a job for better implementations + if hasattr(item, "calculations"): + item.calculations = {} - def __repr__(self) -> str: - return f"MNSession with {len(self.molecules)} molecules, {len(self.trajectories)} trajectories and {len(self.ensembles)} ensembles." + if item.object is None: + to_pop.append(name) + + for name in to_pop: + self.entities.pop(name) def pickle(self, filepath) -> None: - pickle_path = self.stashpath(filepath) + path = Path(filepath) + self.trim() + if len(self) == 0: + return None make_paths_relative(self.trajectories) - self.molecules = trim(self.molecules) - self.trajectories = trim(self.trajectories) - self.ensembles = trim(self.ensembles) # don't save anything if there is nothing to save - if self.n_items == 0: + if len(self) == 0: + # if we aren't saving anything, remove the currently existing session file + # so that it isn't reloaded when we load the save with old session information + if path.exists() and path.suffix == ".MNSession": + os.remove(filepath) return None - with open(pickle_path, "wb") as f: + with open(filepath, "wb") as f: pk.dump(self, f) - print(f"Saved session to: {pickle_path}") + print(f"Saved MNSession to: {filepath}") def load(self, filepath) -> None: - pickle_path = self.stashpath(filepath) - if not os.path.exists(pickle_path): - raise FileNotFoundError(f"MNSession file `{pickle_path}` not found") - with open(pickle_path, "rb") as f: - session = pk.load(f) - - for uuid, item in session.items(): - item.object = bpy.data.objects[item.name] - if hasattr(item, "frames") and hasattr(item, "frames_name"): - item.frames = bpy.data.collections[item.frames_name] - - for uuid, mol in session.molecules.items(): - self.molecules[uuid] = mol + "Load all of the entities from a previously saved MNSession" + path = Path(filepath) - for uuid, uni in session.trajectories.items(): - self.trajectories[uuid] = uni + if not path.exists(): + raise FileNotFoundError(f"MNSession file `{path}` not found") - for uuid, ens in session.ensembles.items(): - self.ensembles[uuid] = ens + with open(path, "rb") as f: + loaded_session = pk.load(f) + current_session = bpy.context.scene.MNSession - print(f"Loaded a MNSession from: {pickle_path}") + # merge the loaded session with current session, handling if they used the old + # structure of separating entities into different categories + if hasattr(loaded_session, "entities"): + current_session.entites + loaded_session.entities + else: + items = [] + for attr in ["molecules", "trajectories", "ensembles"]: + try: + items.append((getattr(loaded_session, attr))) + except AttributeError: + pass + print(f"{items=}") + for key, item in items: + current_session.entities[key] = item + + print(f"Loaded a MNSession from: {filepath}") def stashpath(self, filepath) -> str: return f"{filepath}.MNSession" def clear(self) -> None: """Remove references to all molecules, trajectories and ensembles.""" - self.molecules.clear() - self.trajectories.clear() - self.ensembles.clear() + self.entities.clear() def get_session(context: Context | None = None) -> MNSession: - if not context: - context = bpy.context - return context.scene.MNSession + if isinstance(context, Context): + return context.scene.MNSession + else: + return bpy.context.scene.MNSession @persistent def _pickle(filepath) -> None: - get_session().pickle(filepath) + session = get_session() + session.pickle(session.stashpath(filepath)) @persistent @@ -210,7 +188,8 @@ def _load(filepath: str, printing: str = "quiet") -> None: if filepath == "": return None try: - get_session().load(filepath) + session = get_session() + session.load(session.stashpath(filepath)) except FileNotFoundError: if printing == "verbose": print("No MNSession found to load for this .blend file.") diff --git a/molecularnodes/ui/panel.py b/molecularnodes/ui/panel.py index a04db68f..0a4e5f0a 100644 --- a/molecularnodes/ui/panel.py +++ b/molecularnodes/ui/panel.py @@ -237,8 +237,6 @@ def item_ui(layout, item): def panel_session(layout, context): session = get_session(context) - # if session.n_items > 0: - # return None row = layout.row() row.label(text="Loaded items in the session") # row.operator("mn.session_reload") diff --git a/tests/test_trajectory.py b/tests/test_trajectory.py index b1320dbd..8b533c90 100644 --- a/tests/test_trajectory.py +++ b/tests/test_trajectory.py @@ -200,7 +200,8 @@ def test_save_persistance( assert os.path.exists(session.stashpath(filepath)) bpy.ops.wm.open_mainfile(filepath=filepath) - traj = mn.session.get_session().trajectories[uuid] + traj = mn.session.get_session().get(uuid) + assert traj is not None verts_frame_0 = traj.named_attribute("position") bpy.context.scene.frame_set(4) verts_frame_4 = traj.named_attribute("position") From 676ba47852b89830c539ccb62ba222f95e94344b Mon Sep 17 00:00:00 2001 From: Brady Johnston Date: Thu, 21 Nov 2024 16:05:35 +0800 Subject: [PATCH 3/4] cleanup and refactoring --- molecularnodes/entities/__init__.py | 15 ++++--- molecularnodes/entities/ensemble/__init__.py | 1 + molecularnodes/entities/ensemble/ensemble.py | 2 - molecularnodes/entities/ensemble/star.py | 1 - molecularnodes/entities/entity.py | 2 +- molecularnodes/entities/molecule/molecule.py | 2 - .../entities/trajectory/trajectory.py | 1 - molecularnodes/entities/trajectory/ui.py | 3 +- molecularnodes/session.py | 42 +++++-------------- 9 files changed, 23 insertions(+), 46 deletions(-) diff --git a/molecularnodes/entities/__init__.py b/molecularnodes/entities/__init__.py index b2cec060..f99a6d20 100644 --- a/molecularnodes/entities/__init__.py +++ b/molecularnodes/entities/__init__.py @@ -1,14 +1,19 @@ -from . import molecule, trajectory +from .molecule import CLASSES as MOL_CLASSES +from .trajectory import CLASSES as TRAJ_CLASSES from .density import MN_OT_Import_Map from .trajectory.dna import MN_OT_Import_OxDNA_Trajectory +from .ensemble.ui import MN_OT_Import_Cell_Pack, MN_OT_Import_Star_File + from .ensemble.cellpack import CellPack from .ensemble.star import StarFile -from .ensemble.ui import MN_OT_Import_Cell_Pack, MN_OT_Import_Star_File from .molecule.pdb import PDB from .molecule.pdbx import BCIF, CIF from .molecule.sdf import SDF from .molecule.ui import fetch, load_local -from .trajectory.trajectory import Trajectory +from .trajectory import Trajectory +from .molecule import Molecule +from .ensemble import Ensemble + CLASSES = ( [ @@ -17,6 +22,6 @@ MN_OT_Import_OxDNA_Trajectory, MN_OT_Import_Star_File, ] - + trajectory.CLASSES - + molecule.CLASSES + + TRAJ_CLASSES + + MOL_CLASSES ) diff --git a/molecularnodes/entities/ensemble/__init__.py b/molecularnodes/entities/ensemble/__init__.py index 5937f6d1..922e9200 100644 --- a/molecularnodes/entities/ensemble/__init__.py +++ b/molecularnodes/entities/ensemble/__init__.py @@ -1 +1,2 @@ from .ui import load_starfile, load_cellpack +from .ensemble import Ensemble diff --git a/molecularnodes/entities/ensemble/ensemble.py b/molecularnodes/entities/ensemble/ensemble.py index af41062d..a3d68889 100644 --- a/molecularnodes/entities/ensemble/ensemble.py +++ b/molecularnodes/entities/ensemble/ensemble.py @@ -18,11 +18,9 @@ def __init__(self, file_path: Union[str, Path]): """ super().__init__() - self.type: str = "ensemble" self.file_path: Path = bl.path_resolve(file_path) self.instances: bpy.types.Collection = None self.frames: bpy.types.Collection = None - bpy.context.scene.MNSession.ensembles[self.uuid] = self @classmethod def create_object( diff --git a/molecularnodes/entities/ensemble/star.py b/molecularnodes/entities/ensemble/star.py index 2f195d54..80c6eb1a 100644 --- a/molecularnodes/entities/ensemble/star.py +++ b/molecularnodes/entities/ensemble/star.py @@ -15,7 +15,6 @@ class StarFile(Ensemble): def __init__(self, file_path): super().__init__(file_path) - self.type = "starfile" @classmethod def from_starfile(cls, file_path): diff --git a/molecularnodes/entities/entity.py b/molecularnodes/entities/entity.py index 3ffc2144..653ce078 100644 --- a/molecularnodes/entities/entity.py +++ b/molecularnodes/entities/entity.py @@ -13,7 +13,7 @@ class MolecularEntity( ): def __init__(self) -> None: self.uuid: str = str(uuid1()) - self.type: str = "" + bpy.context.scene.MNSession.entities[self.uuid] = self @property def bob(self) -> BlenderObject: diff --git a/molecularnodes/entities/molecule/molecule.py b/molecularnodes/entities/molecule/molecule.py index 039c37f2..fe363ba7 100644 --- a/molecularnodes/entities/molecule/molecule.py +++ b/molecularnodes/entities/molecule/molecule.py @@ -74,8 +74,6 @@ def __init__(self, file_path: Union[str, Path, io.BytesIO]): self.array: np.ndarray self._frames_collection_name: str = "" - bpy.context.scene.MNSession.molecules[self.uuid] = self - @property def frames(self) -> Collection: return bpy.data.collections.get(self._frames_collection_name) diff --git a/molecularnodes/entities/trajectory/trajectory.py b/molecularnodes/entities/trajectory/trajectory.py index a728ec64..0091979d 100644 --- a/molecularnodes/entities/trajectory/trajectory.py +++ b/molecularnodes/entities/trajectory/trajectory.py @@ -21,7 +21,6 @@ def __init__(self, universe: mda.Universe, world_scale: float = 0.01): self.calculations: Dict[str, Callable] = {} self.world_scale = world_scale self.frame_mapping: npt.NDArray[np.in64] | None = None - bpy.context.scene.MNSession.entities[self.uuid] = self def selection_from_ui(self, ui_item: TrajectorySelectionItem) -> Selection: self.selections[ui_item.name] = Selection( diff --git a/molecularnodes/entities/trajectory/ui.py b/molecularnodes/entities/trajectory/ui.py index bc69e1ba..e09414c4 100644 --- a/molecularnodes/entities/trajectory/ui.py +++ b/molecularnodes/entities/trajectory/ui.py @@ -9,7 +9,6 @@ import MDAnalysis as mda from ... import blender as bl -from ...session import get_session from .trajectory import Trajectory from bpy.props import StringProperty @@ -63,7 +62,7 @@ class MN_OT_Reload_Trajectory(bpy.types.Operator): @classmethod def poll(cls, context): obj = context.active_object - traj = get_session(context).trajectories.get(obj.mn.uuid) + traj = context.scene.MNSession.trajectories.get(obj.mn.uuid) return not traj def execute(self, context): diff --git a/molecularnodes/session.py b/molecularnodes/session.py index eab2148b..cc7daae1 100644 --- a/molecularnodes/session.py +++ b/molecularnodes/session.py @@ -6,22 +6,23 @@ from pathlib import Path from bpy.app.handlers import persistent from bpy.props import StringProperty -from bpy.types import Context +from bpy.types import Context, Operator -from .entities.ensemble.ensemble import Ensemble -from .entities.molecule.molecule import Molecule -from .entities.trajectory.trajectory import Trajectory +from .entities import Ensemble, Molecule, Trajectory def path_relative_to_blend_wd(filepath: str | Path) -> Path: "Get the path of something, relative to the working directory of the current .blend file" - blend_working_directory = bpy.path.abspath("//") + blend_working_directory = Path(bpy.data.filepath).parent if blend_working_directory == "": raise ValueError( "Unable to get current working directly, .blend file not saved" ) - - return Path(filepath).relative_to(Path(blend_working_directory)) + try: + return Path(filepath).relative_to(Path(blend_working_directory)) + except ValueError as e: + print(Warning("Unable to make ")) + return Path(filepath) def make_paths_relative(trajectories: Dict[str, Trajectory]) -> None: @@ -160,29 +161,6 @@ def _pickle(filepath) -> None: @persistent def _load(filepath: str, printing: str = "quiet") -> None: - """ - Load a session from the specified file path. - - This function attempts to load a session from the given file path using the - `get_session().load(filepath)` method. If the file path is empty, the function - returns immediately without attempting to load anything. If the file is not found, - it handles the `FileNotFoundError` exception and optionally prints a message - based on the `printing` parameter. - - Args: - filepath (str): The path to the file from which to load the session. If this - is an empty string, the function will return without doing anything. - printing (str, optional): Controls the verbosity of the function. If set to - "verbose", a message will be printed when the file is not found. Defaults - to "quiet". - - Returns: - None: This function does not return any value. - - Raises: - FileNotFoundError: If the file specified by `filepath` does not exist and - `printing` is set to "verbose", a message will be printed. - """ # the file hasn't been saved or we are opening a fresh file, so don't # attempt to load anything if filepath == "": @@ -197,7 +175,7 @@ def _load(filepath: str, printing: str = "quiet") -> None: pass -class MN_OT_Session_Remove_Item(bpy.types.Operator): +class MN_OT_Session_Remove_Item(Operator): bl_idname = "mn.session_remove_item" bl_label = "Remove" bl_description = "Remove this item from the internal Molecular Nodes session" @@ -221,7 +199,7 @@ def execute(self, context: Context): return {"FINISHED"} -class MN_OT_Session_Create_Object(bpy.types.Operator): +class MN_OT_Session_Create_Object(Operator): bl_idname = "mn.session_create_object" bl_label = "Create Object" bl_description = "Create a new object linked to this item" From 5885eb0921347e7da58238331c7e54697e40001d Mon Sep 17 00:00:00 2001 From: Brady Johnston Date: Thu, 21 Nov 2024 16:39:38 +0800 Subject: [PATCH 4/4] I am in path-related hell --- molecularnodes/session.py | 48 +++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/molecularnodes/session.py b/molecularnodes/session.py index cc7daae1..3894912e 100644 --- a/molecularnodes/session.py +++ b/molecularnodes/session.py @@ -11,26 +11,39 @@ from .entities import Ensemble, Molecule, Trajectory -def path_relative_to_blend_wd(filepath: str | Path) -> Path: - "Get the path of something, relative to the working directory of the current .blend file" - blend_working_directory = Path(bpy.data.filepath).parent - if blend_working_directory == "": +def path_relative_to_blend(target_path: str | Path) -> Path: + """Get a path relative to the current .blend file""" + blend_path = bpy.data.filepath + if blend_path == "": raise ValueError( - "Unable to get current working directly, .blend file not saved" + ".blend file has not yet been saved, unable to get relative path" ) + + blender_folder = Path(blend_path).parent.absolute() + + target_path = Path(target_path) + if not target_path.is_absolute(): + target_path = (blender_folder / target_path).resolve() + + # Get the relative path try: - return Path(filepath).relative_to(Path(blend_working_directory)) + relative_path = Path(os.path.relpath(target_path, blender_folder)) + return relative_path except ValueError as e: - print(Warning("Unable to make ")) - return Path(filepath) + # Handle case where paths are on different drives (Windows) + return target_path def make_paths_relative(trajectories: Dict[str, Trajectory]) -> None: for key, traj in trajectories.items(): - traj.universe.load_new( - path_relative_to_blend_wd(traj.universe.trajectory.filename) - ) - traj.save_filepaths_on_object() + newpath = path_relative_to_blend(traj.universe.trajectory.filename) + cwd = Path.cwd() + try: + os.chdir(Path(bpy.data.filepath).parent) + traj.universe.load_new(newpath) + traj.save_filepaths_on_object() + finally: + os.chdir(cwd) def find_matching_object(uuid): @@ -118,13 +131,18 @@ def load(self, filepath) -> None: raise FileNotFoundError(f"MNSession file `{path}` not found") with open(path, "rb") as f: - loaded_session = pk.load(f) - current_session = bpy.context.scene.MNSession + loaded_session: MNSession = pk.load(f) + if not isinstance(loaded_session, MNSession): + raise ValueError( + f"Loaded .pkl object is not a MNSession, instead: {loaded_session=}" + ) + + current_session: MNSession = bpy.context.scene.MNSession # merge the loaded session with current session, handling if they used the old # structure of separating entities into different categories if hasattr(loaded_session, "entities"): - current_session.entites + loaded_session.entities + current_session.entities | loaded_session.entities else: items = [] for attr in ["molecules", "trajectories", "ensembles"]: