# Addon version
bl_info = {
    "name" : "Hi Phy",
    "description": "",
    "author" : "HiPhyEngine Team",
    "version" : (1, 6, 0),
    "blender" : (4, 5, 0),
    "location" : "Properties > Physics > Hi Phy",
    "warning" : "",
    "wiki_url" : "HiPhyEngine.github.io",
    "doc_url" : "HiPhyEngine.github.io",
    "category" : "Simulation"
}

ADDON_VERSION = ".".join(map(str, bl_info["version"]))

import os
import ctypes

from . import variables
import subprocess
import sys
import bpy

# Get the addon directory
addon_dir = os.path.dirname(__file__)
# sys.path.append(os.path.dirname(addon_dir))
libs_path = os.path.join(addon_dir, "libs")

# print (sys.executable)
def install_package(package):
    try:
        __import__(package)
    except ImportError:
        try:
            subprocess.check_call([sys.executable, "-m", "pip", "install", package])
        except:
            # if we don't have permission, install in user space
            subprocess.check_call([sys.executable, "-m", "pip", "install", "--user", package])

# Trial version will not be packaged with the wheel files
install_package("numpy>=2.4.2")
install_package("mkl==2025.3.1")
install_package("mkl_service==2.6.1")
install_package("mkl_random==1.3.1")
install_package("pybind11>=3.0.1")


# the user sight may not be in the import path, we need to add it
import site
user_site = site.getusersitepackages()
if user_site not in sys.path:
    sys.path.append(user_site)

def add_dll_path():
    if (os.name == "nt"):
        # on windows, the mkl library path may not by in the enviroment, we manually add it here
        executable_dll_path = os.path.join(os.path.dirname(sys.executable), "../Library/bin")
        if os.path.exists(executable_dll_path):
            os.add_dll_directory(executable_dll_path)
        site_dll_path = os.path.join(user_site, "../../Library/bin")
        if os.path.exists(site_dll_path):
            os.add_dll_directory(site_dll_path)

try:
    add_dll_path()
    import mkl
    import pybind11
except:
    from pathlib import Path
    # install the wheels
    for root, dirs, files in os.walk(os.path.join(addon_dir, "wheels")):
        for file in files:
            file_path = os.path.join(root, file)
            print (file_path)
            try:
                install_package(file_path)
            except:
                pass
    add_dll_path()
    import mkl
    import pybind11

# Add your library path
# os.environ['LD_LIBRARY_PATH'] = libs_path + ":" + os.environ.get('LD_LIBRARY_PATH', '')
libs = []
if (os.name == "posix") :
    libs = [
        "libiomp5.so",
        "libtbbmalloc.so.2.12",
        "libtbb.so.12.12",
        "libmetis.so.5",
        "libembree4.so.4",
        "libSeExpr2.so.2",
        # MKL libraries
        # We can't load mkl libraries if we import mkl, it will cause symbol conflicts
        # "libmkl_intel_lp64.so.2",
        # "libmkl_core.so.2",
        # "libmkl_intel_thread.so.2",
        # ICC libraries
        "libintlc.so.5",
        "libimf.so",
        "libsvml.so",
        "libirng.so",
        "libsodium.so.23.3.0",
        "libHiPhyEngine.so"
    ]
if (os.name == "nt"):
    libs = [
        "embree4.dll",
        "svml_dispmd.dll",
        "libmmd.dll",
        "libsodium.dll",
        "HiPhyEngine.dll",
    ]

for lib in libs:
    lib_path = os.path.join(libs_path, lib)
    # Pre-load the library
    ctypes.CDLL(lib_path, mode=ctypes.RTLD_GLOBAL)

import os

if (os.name == "nt"):
    for root, dirs, files in os.walk(os.path.join(addon_dir, "cAPI")):
        for file in files:
            try:
                file_path = os.path.join(root, file)
                ctypes.CDLL(file_path)
            except:
                pass


import mathutils

@bpy.app.handlers.persistent
def scene_update_post(scene):
    return;

from . import utils
from .utils import create_curve_parameterization_attribute
from .utils import evaluate_expression_attributes
from .utils import caches
from .utils import compute_curves_normal
from .utils import exporter
from .utils import motion_mapper_data_conversion
import base64

from .cAPI import SimulationAPI

SimulationAPI.CheckLicense()

# --- License / AddonPreferences ---
class HiPhyAddonPreferences(bpy.types.AddonPreferences):
    bl_idname = __package__
    def draw(self, context):
        layout = self.layout
        if (SimulationAPI.GetLicenseTier() == 0):
            days_left = SimulationAPI.GetLicenseDaysLeft()
            layout.label(text="This is a trial version of HiPhyEngine. " + str(days_left) + " day(s) left for the trial period.")
        elif (SimulationAPI.GetLicenseTier() == 1):
            layout.label(text="This is a retail version of HiPhyEngine.")

# stop all simulations
@bpy.app.handlers.persistent
def load_pre(dummy):
    for solver in utils.caches:
        if caches[solver].IsSimulationRunning():
            caches[solver].driver.Interrupt();
            # wait until the sim has been interrupted
            while True:
                if not caches[solver].IsSimulationRunning():
                    break;

# clear cache when a new file is loaded
@bpy.app.handlers.persistent
def load_post(dummy):
    utils.caches.clear()
    utils.curves_motion_mappers.clear()
    utils.mesh_motion_mappers.clear()
    # set the renderer to be locked to prevent crash
    scene = bpy.context.scene
    if scene:
        scene.render.use_lock_interface = True

# To prevent bad position been saved, have to set the scene time to the start of the simulation frame to reset the position of everything
saved_frame = 0
@bpy.app.handlers.persistent
def save_pre(filename):
    if (bpy.context.object):
        bk_mode = bpy.context.object.mode
        try:
            bpy.ops.object.mode_set(mode='OBJECT')
        except:
            pass
    scene = bpy.context.scene
    global saved_frame
    saved_frame = scene.frame_current
    solvers = utils.GetSolvers();
    start_frames = [caches[solver.name].GetStartFrame() for solver in solvers if solver.name in caches]
    start_frames.append(saved_frame)
    start_frame = min(start_frames)
    bpy.context.scene.frame_set(int(start_frame))
    if (bpy.context.object):
        try:
            bpy.ops.object.mode_set(mode=bk_mode)
        except:
            pass

@bpy.app.handlers.persistent
def save_post(filename):
    bpy.context.scene.frame_set(saved_frame)

import numpy as np
import bmesh

previous_frame = 0;
@bpy.app.handlers.persistent
def frame_change_post(scene, depsgraph=None):
    frame = scene.frame_current + scene.frame_subframe
    global previous_frame
    if (frame == previous_frame):
        return;
    previous_frame = frame
    solvers = utils.GetSolvers();
    # Use the cache to set object position
    for solver in solvers:
        if (not solver.name in utils.caches) :
            continue
        # If the solver is exporting, we don't need to set the position
        if (solver.hi_phy.solver_is_exporting):
            continue;
        cache = utils.caches[solver.name]
        if (not cache or not cache.scene):
            continue;
        if cache.driver and cache.driver.IsInteractive():
            frame = cache.scene.simulation_end_time;
        # only export in OBJECT mode
        if (cache.driver and cache.driver.IsInteractive() and bpy.context.mode == "OBJECT"):
            # Disable all constraints, constraints can be dynamic, those that still exist will be enabled again during exported.
            cache.driver.LockScene();
            cache.scene.DisableNodes(SimulationAPI.Node.Type.Constraint)
            # Write to the start time. We only have one frame of data anyway
            # exporter.ExportConstraintData(utils.interactive_constraints, cache.scene, cache.scene.start_time)
            try:
                exporter.ExportLagrangianFrameData(bpy.context, solver, utils.interactive_affine_bodies, utils.interactive_curves, utils.interactive_cloths, utils.interactive_deformable_bodies, utils.interactive_soft_colliders, utils.interactive_colliders, utils.interactive_constraints, utils.interactive_force_fields, int(cache.scene.start_time + 1), int(cache.scene.start_time + 1), False, None, cache.scene, None, False)
            except Exception as err:
                print(f"Unexpected {err=}, {type(err)=}")
            cache.driver.UnlockScene();
        for node in cache.objs:
            obj = bpy.data.objects.get(node.name)
            # use the simulation transform from the cache, as it could have been modified
            sim_transform = node.GetWorldMatrix(frame);
            if not obj or not obj.hi_phy.is_active:
                print ("Simulation/ object " + node.name + " no longer exists/active")
                continue;
            if obj.hi_phy.driven_shape:
                obj = obj.hi_phy.driven_shape
            # Set affine body transformations
            # driven shape is the one we have already modified transform (if it is affine body)
            if obj.hi_phy.object_type == "AFFINE_BODY":
                # Euler angle will cause gimbal lock
                obj.rotation_mode = 'QUATERNION'
                driven_transform = SimulationAPI.Utils.AffineDeformationToObjectTransform(cache.scene, node, frame)
                obj.matrix_world = utils.toBlenderTransform(driven_transform)
                # we do not need to set position for affine bodies, because we are setting the transform
                if solver.hi_phy.solver_affine_body_auto_key_transform:
                    obj.keyframe_insert(data_path="location", frame=frame)
                    obj.keyframe_insert(data_path="rotation_quaternion", frame=frame)
                    obj.keyframe_insert(data_path="scale", frame=frame)
                continue;
            # for non-affine body objects
            driven_transform = utils.createTransform(obj.matrix_world)
            sim_to_driven = driven_transform.Inverse() * sim_transform;
            position_accessor = cache.scene.GetVector3DataAccessor(node, "position", frame)
            # NOTE: We are assuming driven shape is in the same space as the simulation shape
            obj_data = obj.data
            if not 'position' in obj_data.attributes:
                # if we can't get position we are in the wrong mode
                continue;
            position = obj_data.attributes['position']
            n_verts = obj_data.attributes.domain_size('POINT')
            if (position_accessor.GetDataArraySize() != n_verts):
                print ("simulation vertex data no longer matches the object " + node.name)
                continue;

            position_values = np.zeros((n_verts, 3), dtype=np.float32)
            position_accessor.CopyVector3ArrayToPython(position_values, sim_to_driven, SimulationAPI.VectorType.Position)
            position.data.foreach_set("vector", np.ravel(position_values))
            if (obj.type == 'MESH'):
                bm = bmesh.new()
                bm.from_mesh(obj.data)       # read mesh into BMesh
                bm.to_mesh(obj.data)         # write back to mesh
                bm.free()
            # TODO: Also set the frame information for the rods
            if (obj.type == 'CURVES'):
                # Set the normal
                normal_accessor = cache.scene.GetVector3DataAccessor(node, "frameNormal", frame)
                if 'hi_phy_frame_normal' in obj_data.attributes:
                    normal = obj_data.attributes['hi_phy_frame_normal']
                else:
                    normal = obj_data.attributes.new("hi_phy_frame_normal", 'FLOAT_VECTOR', 'POINT')
                normal_values = np.zeros((n_verts, 3), dtype=np.float32)
                normal_accessor.CopyVector3ArrayToPython(normal_values, sim_to_driven, SimulationAPI.VectorType.Normal)
                normal.data.foreach_set("vector", np.ravel(normal_values))
    depsgraph = bpy.context.evaluated_depsgraph_get()
    for obj in bpy.data.objects:
        # update expressions
        if (obj.hi_phy.is_active and (obj.hi_phy.expression_attribute_live_update or variables.expression_attribute_live_update)) :
            evaluate_expression_attributes.evaluate_expressions(obj)
        # Update constraints
        # Mesh constraint and Root constraint
        # We should be ok doing root constraints directly in the local coordinate
        # reason: 1. length does not matter, so scaling with the local triangle size change should be ok
        #         2. the normal frame will always be perpendicular to the triangle and the basenormal will always on the triangle plane, so they will always be orthogonal
        #         3. we reorthogonalize and normalize when loading the constraint into the simulator anyway
        if (obj.hi_phy.is_active and obj.hi_phy.object_type == "CONSTRAINT" and
            (obj.hi_phy.constraint_type == SimulationAPI.ConstraintType.MeshConstraint or
             obj.hi_phy.constraint_type == SimulationAPI.ConstraintType.RootConstraint)):
            # First get the mesh
            # TODO: this is expensive, figuring out is there a way not to copy the whole mesh data each frame
            mesh = obj.hi_phy.constraint_object1.evaluated_get(depsgraph)
            faces = mesh.data.polygons
            n_faces = len(faces);
            tris = SimulationAPI.CreateIntArray(n_faces * 3)
            for f in range(0, n_faces):
                tris[f * 3 + 0] = faces[f].vertices[0];
                tris[f * 3 + 1] = faces[f].vertices[1];
                tris[f * 3 + 2] = faces[f].vertices[2];

            positions_attr = mesh.data.attributes['position']
            n_verts = mesh.data.attributes.domain_size('POINT')
            positions_data = np.zeros((n_verts, 3), dtype=np.float32)
            positions_attr.data.foreach_get('vector', np.ravel(positions_data))
            verts = SimulationAPI.CreateVector3Array(n_verts)
            SimulationAPI.CopyVector3Array(verts, positions_data)

            # Get the binding info
            n_points = obj.data.attributes.domain_size('POINT')

            bond_face_ids_np = np.zeros(n_points, dtype=np.int32)
            obj.data.attributes["hi_phy_constraint_point_indices1_0"].data.foreach_get('value', bond_face_ids_np);
            bond_face_ids = SimulationAPI.CreateIntArray(n_points)
            SimulationAPI.CopyIntArray(bond_face_ids, bond_face_ids_np)

            bond_barycentric_weights_on_mesh_np = np.zeros((n_points, 3), dtype=np.float32)
            obj.data.attributes["hi_phy_constraint_weight1"].data.foreach_get('vector', np.ravel(bond_barycentric_weights_on_mesh_np));
            bond_barycentric_weights_on_mesh = SimulationAPI.CreateVector3Array(n_points)
            SimulationAPI.CopyVector3Array(bond_barycentric_weights_on_mesh, bond_barycentric_weights_on_mesh_np)

            bond_local_coord_np = np.zeros((n_points, 3), dtype=np.float32)
            obj.data.attributes["hi_phy_constraint_local_coord"].data.foreach_get('vector', np.ravel(bond_local_coord_np));
            bond_local_coord = SimulationAPI.CreateVector3Array(n_points)
            SimulationAPI.CopyVector3Array(bond_local_coord, bond_local_coord_np)

            points = SimulationAPI.CreateVector3Array(n_points)
            mesh_transform = utils.createTransform(mesh.matrix_world)
            points_transform = utils.createTransform(obj.matrix_world)
            if not SimulationAPI.Utils.DeformPointsOnMesh(points, tris, verts, bond_face_ids, bond_barycentric_weights_on_mesh,
                                                          bond_local_coord, points_transform, mesh_transform):
                print ("Unable to deform points on mesh")
                continue

            # Now set the position
            position_values = np.zeros((n_points, 3), dtype=np.float32)
            SimulationAPI.CopyVector3ArrayToPython(position_values, points)
            obj.data.attributes['position'].data.foreach_set("vector", np.ravel(position_values))
            # use bmesh to force blender update
            bm = bmesh.new()
            bm.from_mesh(obj.data)       # read mesh into BMesh
            bm.to_mesh(obj.data)         # write back to mesh
            bm.free()

        # Binding constraint
        if (obj.hi_phy.is_active and obj.hi_phy.object_type == "CONSTRAINT" and
            obj.hi_phy.constraint_type == SimulationAPI.ConstraintType.BindingConstraint
            and obj.hi_phy.constraint_live_update):
            mesh_0 = obj.hi_phy.constraint_object0.evaluated_get(depsgraph)
            positions_attr = mesh_0.data.attributes['position']
            n_verts = mesh_0.data.attributes.domain_size('POINT')
            positions_data_0 = np.zeros((n_verts, 3), dtype=np.float32)
            positions_attr.data.foreach_get('vector', np.ravel(positions_data_0))

            mesh_1 = obj.hi_phy.constraint_object1.evaluated_get(depsgraph)
            positions_attr = mesh_1.data.attributes['position']
            n_verts = mesh_1.data.attributes.domain_size('POINT')
            positions_data_1 = np.zeros((n_verts, 3), dtype=np.float32)
            positions_attr.data.foreach_get('vector', np.ravel(positions_data_1))

            # Get the binding info
            n_points = obj.data.attributes.domain_size('EDGE')

            indices_0_0 = np.zeros(n_points, dtype=np.int32)
            obj.data.attributes["hi_phy_constraint_point_indices0_0"].data.foreach_get('value', indices_0_0);
            indices_0_1 = np.zeros(n_points, dtype=np.int32)
            obj.data.attributes["hi_phy_constraint_point_indices0_1"].data.foreach_get('value', indices_0_1);
            indices_0_2 = np.zeros(n_points, dtype=np.int32)
            obj.data.attributes["hi_phy_constraint_point_indices0_2"].data.foreach_get('value', indices_0_2);

            indices_1_0 = np.zeros(n_points, dtype=np.int32)
            obj.data.attributes["hi_phy_constraint_point_indices1_0"].data.foreach_get('value', indices_1_0);
            indices_1_1 = np.zeros(n_points, dtype=np.int32)
            obj.data.attributes["hi_phy_constraint_point_indices1_1"].data.foreach_get('value', indices_1_1);
            indices_1_2 = np.zeros(n_points, dtype=np.int32)
            obj.data.attributes["hi_phy_constraint_point_indices1_2"].data.foreach_get('value', indices_1_2);

            weights_0 = np.zeros((n_points, 3), dtype=np.float32)
            obj.data.attributes["hi_phy_constraint_weight0"].data.foreach_get('vector', np.ravel(weights_0));
            weights_1 = np.zeros((n_points, 3), dtype=np.float32)
            obj.data.attributes["hi_phy_constraint_weight1"].data.foreach_get('vector', np.ravel(weights_1));

            mesh0_transform = mesh_0.matrix_world
            mesh1_transform = mesh_1.matrix_world
            obj_transform = obj.matrix_world
            obj_transform.invert()

            mesh0_to_obj = obj_transform @ mesh0_transform
            mesh1_to_obj = obj_transform @ mesh1_transform

            points_0 = [positions_data_0[indices_0_0[p]] * weights_0[p][0] +
                        positions_data_0[indices_0_1[p]] * weights_0[p][1] +
                        positions_data_0[indices_0_2[p]] * weights_0[p][2] for p in range(n_points)];
            points_1 = [positions_data_1[indices_1_0[p]] * weights_1[p][0] +
                        positions_data_1[indices_1_1[p]] * weights_1[p][1] +
                        positions_data_1[indices_1_2[p]] * weights_1[p][2] for p in range(n_points)];

            positions_data = np.zeros((n_points * 2, 3), dtype=np.float32)
            for i in range(0, n_points):
                positions_data[i           ] = mesh0_to_obj @ mathutils.Vector((points_0[i][0], points_0[i][1], points_0[i][2]))
                positions_data[i + n_points] = mesh1_to_obj @ mathutils.Vector((points_1[i][0], points_1[i][1], points_1[i][2]))
            positions_attr = obj.data.attributes['position']
            positions_attr.data.foreach_set('vector', np.ravel(positions_data))

            # use bmesh to force blender update
            bm = bmesh.new()
            bm.from_mesh(obj.data)       # read mesh into BMesh
            bm.to_mesh(obj.data)         # write back to mesh
            bm.free()
            continue;

        # Update forcefield
        if (obj.hi_phy.is_active and obj.hi_phy.object_type == "FORCE_FIELD" and obj.hi_phy.force_field_visualization_live_update and obj.hi_phy.force_field_visualization_initialized):
            positions_attr = obj.data.attributes['position']
            n_verts = obj.data.attributes.domain_size('POINT')
            points = np.zeros((n_verts, 3), dtype=np.float32)
            positions_attr.data.foreach_get('vector', np.ravel(points))

            n_points = int(n_verts / 2)
            p = points[0:n_points, :]
            f = np.zeros((n_points, 3), dtype=np.float32)

            points_final = utils.force_field.ComputeVisualizationPoints(obj.hi_phy, p, f)
            if (points_final is None):
                print ("Failed to evaluate the force")
                continue;
            positions_attr.data.foreach_set('vector', np.ravel(points_final))
            # use bmesh to force blender update
            bm = bmesh.new()
            bm.from_mesh(obj.data)       # read mesh into BMesh
            bm.to_mesh(obj.data)         # write back to mesh
            bm.free()

        # Update Motion Mappers
        if (obj.hi_phy.is_active and obj.hi_phy.object_type == "CURVES_MOTION_MAPPER" and obj.hi_phy.motion_mapper_live_update):
            if (not obj.hi_phy.motion_mapper_is_bond):
                continue;
            motion_mapper_rebuid = False;
            if (obj.name in utils.curves_motion_mappers):
                motion_mapper = utils.curves_motion_mappers[obj.name]
                if (not motion_mapper or not motion_mapper.IsBond()):
                    motion_mapper_rebuid = True
            else:
                motion_mapper_rebuid = True;
            # Get guide and driven objects
            # Guide is evaluated
            guide = obj.hi_phy.guide_shape.evaluated_get(depsgraph)
            if (not guide or guide.type != 'CURVES'):
                continue;
            # Driven is not evaluated
            driven = obj.hi_phy.driven_shape
            if (not driven or driven.type != 'CURVES'):
                continue;
            if motion_mapper_rebuid:
                # Rebuild motion mapper with the stored data
                motion_mapper = motion_mapper_data_conversion.constructCurvesMotionMapperFromBlender(obj, guide, driven)
                if (motion_mapper):
                    utils.curves_motion_mappers[obj.name] = motion_mapper
                else:
                    obj.hi_phy.motion_mapper_is_bond = False
                    continue;
            # get transforms
            guide_transform = utils.createTransform(guide.matrix_world)
            driven_transform = utils.createTransform(driven.matrix_world)
            guide_to_driven = driven_transform.Inverse() * guide_transform
            identity_transform = SimulationAPI.TransformMatrix();
            # Set guide data
            guide_positions = guide.data.attributes['position']
            n_guide_cvs = guide.data.attributes.domain_size('POINT')
            if (n_guide_cvs != motion_mapper.GuideNumCVs()):
                print ("the guide cv size no longer matches the bond data for " + guide.name)
                continue;
            guide_position_data = np.zeros((n_guide_cvs, 3), dtype=np.float32)
            guide_positions.data.foreach_get('vector', np.ravel(guide_position_data))
            n_driven_cvs = driven.data.attributes.domain_size('POINT')
            if (n_driven_cvs != motion_mapper.DrivenNumCVs()):
                print ("the driven cv size no longer matches the bond data for " + driven.name)
                continue;
            driven_positions = driven.data.attributes['position']
            driven_position_data = np.zeros((n_driven_cvs, 3), dtype=np.float32)
            if (not motion_mapper.SetGuidePosition(guide_position_data, identity_transform)):
                print ("failed to set guide position data for " + obj.name)
                continue;
            if (motion_mapper.UseFrame()):
                # Set frame
                if (obj.hi_phy.motion_mapper_frame_mode == "BASE_NORMAL_TWIST"):
                    guide_normals = compute_curves_normal.Compute(guide, obj.hi_phy.motion_mapper_base_normal_attribute, obj.hi_phy.motion_mapper_twist_attribute)
                    if (not motion_mapper.SetGuideNormalRaw(guide_normals, identity_transform)):
                        print ("failed to set guide normal data for " + obj.name)
                        continue
                elif (obj.hi_phy.motion_mapper_frame_mode == "FRAME_NORMAL"):
                    if (not obj.hi_phy.motion_mapper_frame_attribute in guide.data.attributes):
                        print ("no frame attribute exists for " + guide.name)
                        continue;
                    guide_normals = guide.data.attributes[obj.hi_phy.motion_mapper_frame_attribute]
                    if (n_guide_cvs != guide.data.attributes.domain_size(guide_normals.domain)):
                        print ("no frame attribute size no longer matches the bond data for " + guide.name)
                        continue;
                    guide_normal_data = np.zeros((n_guide_cvs, 3), dtype=np.float32)
                    guide_normals.data.foreach_get('vector', np.ravel(guide_normal_data))
                    if (not motion_mapper.SetGuideNormal(guide_normal_data, identity_transform)):
                        print ("failed to set guide normal data for " + obj.name)
                        continue
            motion_mapper.SetDynamicParting(obj.hi_phy.motion_mapper_dynamic_parting)
            motion_mapper.SetDynamicPartingPower(obj.hi_phy.motion_mapper_dynamic_parting_power)
            motion_mapper.Drive()
            if (not motion_mapper.GetDrivenPosition(driven_position_data, guide_to_driven)):
                print ("failed to get driven position data for " + obj.name)
                continue
            driven_positions.data.foreach_set("vector", np.ravel(driven_position_data))

        if (obj.hi_phy.is_active and obj.hi_phy.object_type == "MESH_MOTION_MAPPER" and obj.hi_phy.motion_mapper_live_update):
            if (not obj.hi_phy.motion_mapper_is_bond):
                continue;
            motion_mapper_rebuid = False;
            if (obj.name in utils.mesh_motion_mappers):
                motion_mapper = utils.mesh_motion_mappers[obj.name]
                if (not motion_mapper or not motion_mapper.IsBond()):
                    motion_mapper_rebuid = True
            else:
                motion_mapper_rebuid = True;
            guide = obj.hi_phy.guide_shape.evaluated_get(depsgraph)
            if (not guide or guide.type != 'MESH'):
                continue
            # Driven is not evaluated
            driven = obj.hi_phy.driven_shape
            if (not driven or driven.type != 'MESH'):
                continue
            if motion_mapper_rebuid:
                # Rebuild motion mapper with the stored data
                motion_mapper = motion_mapper_data_conversion.constructMeshMotionMapperFromBlender(obj, guide)
                if (motion_mapper):
                    utils.mesh_motion_mappers[obj.name] = motion_mapper
                else:
                    obj.hi_phy.motion_mapper_is_bond = False
                    continue;

            target = None
            if (obj.hi_phy.target_shape):
                target = obj.hi_phy.target_shape.evaluated_get(depsgraph)
            # Get guide, target, and driven objects
            # Guide, and Target are evaluated
            # get transforms
            guide_transform = utils.createTransform(guide.matrix_world)
            if target:
                target_positions = target.data.attributes['position']
                n_target_verts = target.data.attributes.domain_size('POINT')
                if (n_target_verts != motion_mapper.DrivenNumVertices()):
                    print ("the target mesh vertices does not match the driven mesh vertices " + target.name)
                    continue;
                target_transform = utils.createTransform(target.matrix_world)
                target_to_guide = guide_transform.Inverse() * target_transform
                target_position_data = np.zeros((n_target_verts, 3), dtype=np.float32)
                target_positions.data.foreach_get('vector', np.ravel(target_position_data))
                if (not motion_mapper.SetDrivenPosition(target_position_data, target_to_guide)):
                    print ("failed to set target position data for " + obj.name)
                    continue;

            driven_transform = utils.createTransform(driven.matrix_world)
            guide_to_driven = driven_transform.Inverse() * guide_transform
            identity_transform = SimulationAPI.TransformMatrix();
            # Set guide data
            guide_positions = guide.data.attributes['position']
            n_guide_cvs = guide.data.attributes.domain_size('POINT')
            if (n_guide_cvs != motion_mapper.GuideNumVertices()):
                print ("the guide mesh vertices no longer matches the bond data for " + guide.name)
                continue;
            guide_position_data = np.zeros((n_guide_cvs, 3), dtype=np.float32)
            guide_positions.data.foreach_get('vector', np.ravel(guide_position_data))
            n_driven_cvs = driven.data.attributes.domain_size('POINT')
            if (n_driven_cvs != motion_mapper.DrivenNumVertices()):
                print ("the driven mesh vertices no longer matches the bond data for " + driven.name)
                continue;
            driven_positions = driven.data.attributes['position']
            driven_position_data = np.zeros((n_driven_cvs, 3), dtype=np.float32)
            # binding is in guide space
            if (not motion_mapper.SetGuidePosition(guide_position_data, identity_transform)):
                print ("failed to set guide position data for " + obj.name)
                continue;
            motion_mapper.Drive()
            if (not motion_mapper.GetDrivenPosition(driven_position_data, guide_to_driven)):
                print ("failed to get driven position data for " + obj.name)
                continue
            driven_positions.data.foreach_set("vector", np.ravel(driven_position_data))

from . import properties
from . import operators
from . import ui

modules = [
    properties,
    operators,
    ui
]

# TODO call upgrade script here for the future
@bpy.app.handlers.persistent
def check_addon_version(dummy):
    scene = bpy.context.scene
    if (scene.hiphy_version != ADDON_VERSION):
        print ("HiPhy version of the scene " + scene.hiphy_version + " does not match the current hi phy version " + ADDON_VERSION)
    scene.hiphy_version = ADDON_VERSION

from .properties import object_properties
@bpy.app.handlers.persistent
def check_object_type(dummy):
    for obj in bpy.context.scene.objects:
        object_properties.updt_bool_obj_type(obj.hi_phy)

def register():
    bpy.utils.register_class(HiPhyAddonPreferences)
    for module in modules:
        module.register()
    bpy.types.Scene.hiphy_version = bpy.props.StringProperty(
        name="HiPhy Version",
        description="The version of the hiphy used to create the scene",
        default=ADDON_VERSION)
    bpy.app.handlers.depsgraph_update_post.append(scene_update_post)
    bpy.app.handlers.frame_change_post.append(frame_change_post)
    bpy.app.handlers.save_pre.append(save_pre)
    bpy.app.handlers.save_post.append(save_post)
    bpy.app.handlers.load_post.append(load_post)
    bpy.app.handlers.load_pre.append(load_pre)
    bpy.app.handlers.load_post.append(check_addon_version)
    bpy.app.handlers.load_post.append(check_object_type)

def unregister():
    for module in reversed(modules):
        module.unregister()
    bpy.utils.unregister_class(HiPhyAddonPreferences)
    bpy.app.handlers.depsgraph_update_post.remove(scene_update_post)
    bpy.app.handlers.frame_change_post.remove(frame_change_post)
    bpy.app.handlers.save_pre.remove(save_pre)
    bpy.app.handlers.save_post.remove(save_post)
    bpy.app.handlers.load_pre.remove(load_pre)
    bpy.app.handlers.load_post.remove(check_object_type)
    bpy.app.handlers.load_post.remove(check_addon_version)
    del bpy.types.Scene.hiphy_version
