# File: _decoder_.py
# Author: Ryoichi Ando (ryoichi.ando@zozo.com)
# License: Apache v2.0
import os
import pickle
import numpy as np
from ._asset_ import AssetManager
from ._mesh_ import MeshManager
from ._plot_ import PlotManager
from ._scene_ import FixedScene, Scene
from ._session_ import FixedSession, Session
from ._utils_ import Utils
[docs]
class BlenderApp:
def __init__(self, name: str, verbose: bool = False):
"""Initialize the BlenderDecoder.
Args:
name (str): The name of the Blender project.
verbose (bool): Enable verbose logging.
"""
self._verbose = verbose
self._name = name
# Get current git branch name for directory structure
import subprocess
try:
git_branch = subprocess.check_output(
["git", "branch", "--show-current"],
cwd=os.path.dirname(os.path.abspath(__file__)),
text=True
).strip()
if not git_branch:
git_branch = "unknown"
except:
git_branch = "unknown"
self._root = os.path.join(
os.path.expanduser("~"), ".local", "share", "ppf-cts", f"git-{git_branch}", self._name
)
cache_root = os.path.join(self._root, ".cash")
os.makedirs(cache_root, exist_ok=True)
self._asset_manager = AssetManager()
self._mesh_manager = MeshManager(cache_root)
self._scene = None
self._session = None
self._fixed_scene = None
self._fixed_session = None
[docs]
def populate(self) -> "BlenderApp":
"""Populate the scene with objects from a Blender project."""
data_path = os.path.join(self._root, "data.pickle")
assert os.path.exists(data_path)
self._scene = Scene("scene", PlotManager(), self._asset_manager)
scene_decoder = SceneDecoder(data_path, self._asset_manager, self._mesh_manager)
scene_decoder.populate_objects(self._scene, verbose=self._verbose)
return self
[docs]
def make(self) -> "BlenderApp":
"""Make a runnable session from the populated scene with parameters updated."""
assert self._scene is not None, "Scene must be populated before making the app"
param_path = os.path.join(self._root, "param.pickle")
assert os.path.exists(param_path)
param_decoder = ParamDecoder().set_path(param_path)
param_decoder.apply_to_objects(self._scene, verbose=self._verbose)
self._fixed_scene = self._scene.build()
if Utils.in_jupyter_notebook():
self._fixed_scene.preview()
self._session = Session(
self._name,
self._root,
os.path.dirname(os.path.dirname(__file__)),
None,
"session",
).init(self._fixed_scene)
param_decoder.apply_to_session(self._session, verbose=self._verbose)
self._fixed_session = self._session.build()
return self
@property
def scene(self) -> Scene:
"""Get the scene object."""
assert self._scene is not None, "Scene must be populated before accessing it"
return self._scene
@property
def session(self) -> Session:
"""Get the session object."""
assert self._session is not None, "Session must be made before accessing it"
return self._session
@property
def fixed_scene(self) -> FixedScene:
"""Get the fixed scene object."""
assert self._fixed_scene is not None, (
"Fixed scene must be made before accessing it"
)
return self._fixed_scene
@property
def fixed_session(self) -> FixedSession:
"""Get the fixed session object."""
assert self._fixed_session is not None, (
"Fixed session must be made before accessing it"
)
return self._fixed_session
[docs]
class ParamDecoder:
def __init__(self):
self._data = None
[docs]
def set_path(self, filepath: str) -> "ParamDecoder":
"""Set the path to the parameter file."""
assert filepath.endswith(".pickle"), "File must be a pickle file."
with open(filepath, "rb") as f:
self._data = pickle.load(f)
assert "group" in self._data, "Group parameters not found in the data."
assert "scene" in self._data, "Scene parameters not found in the data."
return self
[docs]
def apply_to_objects(self, scene: Scene, verbose: bool = False):
"""Decode the parameter file and apply it to the objects in a scene.
Args:
scene (Scene): The scene to which the parameters will be applied.
verbose (bool): Enable verbose logging
"""
assert self._data is not None, "Parameter data not set. Call set_path() first."
if verbose:
print("=== Object Parameters ===")
for params, objects in self._data["group"]:
for obj_name in objects:
if verbose:
print(f"*** name: {obj_name} ***")
obj = scene.select(obj_name)
obj.param.clear_all()
for key, val in params.items():
if verbose:
print(f" {key}: {val}")
obj.param.set(key, val)
[docs]
def apply_to_session(self, session: Session, verbose: bool = False):
"""Decode the parameter file and apply it to the session.
Args:
filepath (str): Path to the pickle file containing the session parameters.
verbose (bool): Enable verbose logging
"""
assert self._data is not None, "Parameter data not set. Call set_path() first."
if verbose:
print("=== Session Parameters ===")
session.param.clear_all()
for k, v in self._data["scene"].items():
if verbose:
print(f" {k}: {v}")
if k == "fitting" and v > 0:
fitting_time = float(v)
session.param.set("fitting")
session.param.dyn("fitting").time(fitting_time).hold().change(False)
else:
session.param.set(k, v)
[docs]
class SceneDecoder:
def __init__(
self, filepath: str, asset_manager: AssetManager, mesh_manager: MeshManager
):
assert filepath.endswith(".pickle"), "File must be a pickle file."
with open(filepath, "rb") as f:
self._data = pickle.load(f)
self._asset = asset_manager
self._mesh = mesh_manager
[docs]
def populate_objects(self, scene: Scene, verbose: bool = False) -> Scene:
"""
Populate the scene with objects from a pickle file.
Args:
filepath (str): Path to the pickle file containing the objects data.
Returns:
scene: The populated scene.
"""
for group in self._data:
objects = group.get("object", None)
assert objects is not None, "Object data not found in the group."
group_type = group.get("type")
if verbose:
print(f"--- new group: {group_type} ---")
for obj in objects:
name, vert, face, edge, uv = (
obj.get("name"),
obj.get("vert"),
obj.get("face"),
obj.get("edge"),
obj.get("uv", None),
)
if verbose:
if edge is not None:
print(
f" * name: {name}, vert: {vert.shape}, edge: {edge.shape}, uv: {len(uv) if uv is not None else 'None'}"
)
else:
print(
f" * name: {name}, vert: {vert.shape}, face: {face.shape if face is not None else 'None'}, uv: {len(uv) if uv is not None else 'None'}"
)
if group_type == "STATIC":
if verbose:
print(
f" > animation: {'YES' if 'animation' in obj else 'NO'}"
)
self._asset.add.tri(name, vert, face)
pin = scene.add(name).pin()
animation = obj.get("animation", None)
if animation:
time = animation.get("time", [])
position = animation.get("position", [])
for t, pos in zip(time, position, strict=False):
if t > 0:
pin.move_to(pos, t)
_obj = None
elif group_type == "SOLID":
V, F, T = self._mesh.create.tri(vert, face).tetrahedralize()
self._asset.add.tet(name, V, F, T)
_obj = scene.add(name)
elif group_type == "SHELL":
self._asset.add.tri(name, vert, face)
_obj = scene.add(name)
if uv is not None:
assert len(uv) == len(face), "UV length must match face length."
_obj.set_uv(uv)
elif group_type == "ROD":
assert edge is not None, (
f"Edge data not found for rod object {name}"
)
self._asset.add.rod(name, vert, edge)
_obj = scene.add(name)
else:
raise ValueError(f"Unknown group type: {group_type}")
if _obj is not None and "pin" in obj:
pin_index, pin_anim = obj["pin"]
if verbose:
print(
f" > pin: {len(pin_index)}, animation: {'YES' if pin_anim else 'NO'}"
)
if pin_anim:
pins = [_obj.pin([i]) for i in pin_index]
for i, _pin in enumerate(pins):
keyframe = pin_anim.get(pin_index[i], None)
if keyframe:
for t, pos in zip(
keyframe["time"], keyframe["position"], strict=False
):
if t > 0:
pins[i].move_to(np.array([pos]), t)
else:
_obj.pin(pin_index)
if _obj is not None and "stitch" in obj:
stitch_data = obj["stitch"]
stitch_name = f"{name}_stitch"
if verbose:
print(f" > stitch: {len(stitch_data[0])} edges")
self._asset.add.stitch(stitch_name, stitch_data)
_obj.stitch(stitch_name)
return scene