Exemplo n.º 1
0
class Builder(object):

    # Tool Level Paths ---
    USER_PATH = os.path.join(os.path.expanduser("~"), "NoMansSkyBaseBuilder")
    FILE_PATH = os.path.dirname(os.path.realpath(__file__))
    MODEL_PATH = os.path.join(FILE_PATH, "models")
    NICE_JSON = os.path.join(FILE_PATH, "resources", "nice_names.json")
    MODS_PATH = os.path.join(USER_PATH, "mods")
    PRESET_PATH = os.path.join(USER_PATH, "presets")

    # Load in nice name information.
    nice_name_dictionary = python_utils.load_dictionary(NICE_JSON)

    override_classes = {
        "BASE_FLAG": base_flag.BASE_FLAG,
        "MESSAGEMODULE": messagemodule.MESSAGEMODULE,
        "U_POWERLINE": u_powerline.U_POWERLINE,
        "U_PIPELINE": u_pipeline.U_PIPELINE,
        "U_PORTALLINE": u_portalline.U_PORTALLINE,
        "POWER_CONTROL": power_control.POWER_CONTROL,
        "FREIGHTER_CORE": freighter_core.FREIGHTER_CORE,
        "BRIDGECONNECTOR": bridge_connector.BRIDGECONNECTOR,
        "AIRLCKCONNECTOR": air_lock_connector.AIRLCKCONNECTOR,
        "BYTEBEAT": bytebeat.BYTEBEAT,
        "BYTEBEATSWITCH": bytebeatswitch.BYTEBEATSWITCH,
        "U_BYTEBEATLINE": u_bytebeatline.U_BYTEBEATLINE
    }

    def __init__(self):
        """Builder __init__."""

        # Part Cache.
        self.__part_cache = {}
        self.__preset_cache = {}

        # Construct category and OBJ reference.
        # Create default part pack.
        self.available_packs = [("Parts", self.MODEL_PATH)]

        # Find any mods with model packs inside.
        if os.path.exists(self.MODS_PATH):
            mod_folders = os.listdir(self.MODS_PATH)
            for mod_folder in mod_folders:
                full_mod_path = os.path.join(self.MODS_PATH, mod_folder)
                if "models" in os.listdir(full_mod_path):
                    full_model_path = os.path.join(self.MODS_PATH, mod_folder,
                                                   "models")
                    self.available_packs.append((mod_folder, full_model_path))

        # Find Parts and build a reference dictionary.
        self.part_reference = {}
        for (pack_name, pack_folder) in self.available_packs:
            for category in self.get_categories(pack=pack_name):
                parts = self.get_objs_from_category(category, pack=pack_name)
                for part in parts:
                    # Get Unique ID.
                    unique_id = os.path.splitext(part)[0]
                    # Construct full path.
                    search_path = pack_folder or self.MODEL_PATH
                    part_path = os.path.join(search_path, category, part)
                    # Place part information into reference.
                    self.part_reference[unique_id] = {
                        "category": category,
                        "full_path": part_path,
                        "pack": pack_name
                    }

    def clear_caches(self):
        """Clear all the caches we use in this class."""
        self.__part_cache.clear()

    def add_to_part_cache(self, object_id, bpy_object):
        """Add item to part cache."""
        self.__part_cache[object_id] = bpy_object.name

    def add_to_preset_cache(self, object_id, bpy_object):
        """Add item to preset cache."""
        self.__part_cache[object_id] = bpy_object.name

    def find_object_by_id(self, object_id):
        """Get the item from the part cache."""
        part_name = self.__part_cache.get(object_id, None)
        # Return None if not found.
        if not part_name:
            return None
        # If something is found, we need to check if it still exists.
        if part_name in bpy.data.objects:
            bpy_object = bpy.data.objects[part_name]
            return self.get_builder_object_from_bpy_object(bpy_object)
        # If all fails, return None.
        return None

    @classmethod
    def get_part_class(cls, object_id):
        return cls.override_classes.get(object_id, part.Part)

    def get_builder_object_from_bpy_object(self, bpy_object):
        # Handle Presets.
        if "PresetID" in bpy_object:
            return preset.Preset.deserialise_from_object(bpy_object=bpy_object,
                                                         builder_object=self)

        # Handle Parts.
        object_id = None
        if "ObjectID" in bpy_object:
            object_id = bpy_object.get("ObjectID")
        elif "SnapID" in bpy_object:
            object_id = bpy_object.get("SnapID")

        if not object_id:
            return None

        use_class = self.get_part_class(object_id)
        return use_class.deserialise_from_object(bpy_object=bpy_object,
                                                 builder_object=self)

    def find_preset_by_id(self, preset_id):
        """Get the item from the part cache."""
        preset_name = self.__part_cache.get(preset_id, None)
        # Return None if not found.
        if not preset_name:
            return None
        # If something is found, we need to check if it still exists.
        if preset_name in bpy.data.objects:
            bpy_object = bpy.data.objects[preset_name]
            return preset.Preset.deserialise_from_object(bpy_object=bpy_object,
                                                         builder_object=self)
        # If all fails, return None.
        return None

    def get_all_parts(self, exclude_presets=False, skip_object_type=None):
        """Get all NMS parts in the scene.

        Args:
            get_presets (bool): Choose to exclude parts generated via
                preset.
        """
        # Validate skip list
        skip_object_type = skip_object_type or []

        # Get all individual NMS parts.
        flat_parts = [part for part in bpy.data.objects if "ObjectID" in part]
        flat_parts = [
            part for part in flat_parts
            if part["ObjectID"] not in skip_object_type
        ]

        # If exclude presets is on, just return the top level objects.
        if exclude_presets:
            flat_parts = [
                part for part in flat_parts
                if part["belongs_to_preset"] == False
            ]
        flat_parts = sorted(flat_parts, key=Builder.by_order)
        return flat_parts

    def get_all_presets(self):
        """Get all Builder preset items in the scene."""
        return [part for part in bpy.data.objects if "PresetID" in part]

    def add_part(self, object_id, user_data=None, build_rigs=True):
        """Add an item based on it's object ID."""
        use_class = self.get_part_class(object_id)
        item = use_class(object_id=object_id,
                         builder_object=self,
                         user_data=user_data,
                         build_rigs=build_rigs)
        return item

    def add_preset(self, preset_id):
        """Add an item based on it's preset ID."""
        item = preset.Preset(preset_id=preset_id, builder_object=self)
        return item

    # Serialising ---
    def serialise(self, get_presets=False, add_timestamp=False):
        """Return NMS compatible dictionary.

        Args:
            get_presets (bool): This will generate data for presets. And 
                exclude parts generated from presets.
        Returns:
            dict: Dictionary of base information.
        """
        # Get all object part data.
        object_list = []

        for item in self.get_all_parts(exclude_presets=get_presets):
            object_id = item["ObjectID"]
            use_class = self.get_part_class(object_id)
            item_obj = use_class.deserialise_from_object(item,
                                                         builder_object=self)
            object_list.append(item_obj.serialise())

        # Create full dictionary.
        data = {"Objects": object_list}

        if add_timestamp:
            data["timestamp"] = int(time.time())

        # Add preset information if specified.
        if get_presets:
            preset_list = []
            for _preset in self.get_all_presets():
                preset_obj = preset.Preset.deserialise_from_object(
                    _preset, builder_object=self)
                preset_list.append(preset_obj.serialise())
            data["Presets"] = preset_list

        return data

    def deserialise_from_data(self, data):
        """Given NMS data, reconstruct the base.
        
        We don't need to create a new class, we can act upon this one.
        """
        # pr = cProfile.Profile()
        # pr.enable()

        # Reconstruct objects.
        for part_data in data.get("Objects", []):
            object_id = part_data.get("ObjectID").replace("^", "")
            use_class = self.get_part_class(object_id)
            use_class.deserialise_from_data(part_data, self)

        # Reconstruct presets.
        for preset_data in data.get("Presets", []):
            preset.Preset.deserialise_from_data(preset_data, self)

        # Build Rigs.
        self.build_rigs()
        # Optimise control points.
        self.optimise_control_points()
        # pr.disable()
        # pr.print_stats(sort='time')

    @staticmethod
    def by_order(bpy_object):
        """Sorting method to get objects by the order attribute.
        
        Args:
            bpy_object (bpy.ob): A blender object.

        Returns:
            int: The order of which the item is/was built.
        """
        return bpy_object.get("order", 0)

    # Category Methods ---
    def get_categories(self, pack=None):
        """Get the list of categories.
        
        Args:
            pack (str): The model pack search under for categories.
                Use this for mod support. Defaults to vanilla 'Parts'.
        Returns:
            list: List of folders underneath category path.
        """
        # Validate Pack name.
        pack = pack or "Parts"
        # Get the associated model path.
        search_path = self.get_model_path_from_pack(pack)
        return os.listdir(search_path)

    def get_objs_from_category(self, category, pack=None):
        """Get a list of parts belonging to a category.
        
        Args:
            category (str): The name of the category.
            pack (str): The model pack search under for categories.
                Use this for mod support. Defaults to vanilla 'Parts'.
        """
        # Validate Pack name.
        pack = pack or "Parts"
        # Get the associated model path.
        search_path = self.get_model_path_from_pack(pack)
        category_path = os.path.join(search_path, category)
        all_objs = [
            part for part in os.listdir(category_path) if part.endswith(".obj")
        ]
        file_names = sorted(all_objs)
        return file_names

    def get_obj_path(self, part):
        """Get the path to the OBJ file from a part."""
        part_dictionary = self.part_reference.get(part, {})
        return part_dictionary.get("full_path", None)

    def get_model_path_from_pack(self, pack_request):
        """Given a pack name, return it's associated path.
        
        Args:
            pack_request (str): The name of the pack
            
        Return:
            str: The model path of the pack.
        """
        for pack_name, pack_path in self.available_packs:
            if pack_name == pack_request:
                return pack_path

    def get_parts_from_category(self, category, pack=None):
        """Get all the parts from a specific category.
        
        Args:
            category (str): The category to search.
            pack (str): The model pack name. Defaults to vanilla 'Parts'.
        """
        # Validate pack name.
        pack = pack or "Parts"
        parts = []
        for item, value in self.part_reference.items():
            # Get pack and category values.
            part_category = value["category"]
            part_pack = value["pack"]
            # Check both are valid.
            pack_check = part_pack == pack
            category_check = part_category == category
            # Add to parts.
            if pack_check and category_check:
                parts.append(item)
        return sorted(parts)

    def get_nice_name(self, part):
        """Get a nice version of the part id."""
        part = os.path.basename(part)
        nice_name = part.title().replace("_", " ")
        return self.nice_name_dictionary.get(part, nice_name)

    def save_preset_to_file(self, preset_name):
        # Get a file path.
        file_path = os.path.join(self.PRESET_PATH, preset_name)
        # Add .json if it's not specified.
        if not file_path.endswith(".json"):
            file_path += ".json"
        # Save to file path
        with open(file_path, "w") as stream:
            json.dump(self.serialise(add_timestamp=True), stream, indent=4)

    def build_rigs(self):
        """Get all items that require a rig and build them."""
        blend_utils.scene_refresh()
        parts = self.get_all_parts(exclude_presets=True)
        for part in parts:
            builder_object = self.get_builder_object_from_bpy_object(part)
            if hasattr(builder_object, "build_rig"):
                builder_object.build_rig()

    def optimise_control_points(self):
        """Find all control points that share the same location and combine them."""
        blend_utils.scene_refresh()

        # First build a dictionary of controls that match.
        power_control_objects = [
            obj for obj in bpy.data.objects if "rig_item" in obj
        ]
        power_control_reference = defaultdict(list)
        for power_control in power_control_objects:
            # Create a key that will group the controls based on their location.
            # I am rounding the decimal point to 4 as the accuracy means items
            # in the same location have slightly different values.
            key = ",".join([
                str(round(loc, 3))
                for loc in power_control.matrix_world.decompose()[0]
            ])
            # Append the control to the key.
            power_control_reference[key].append(power_control)

        # Swap any duplicate controls with the first instance.
        for key, value in power_control_reference.items():
            unique_control = value[0]
            other_controls = value[1:]

            if not other_controls:
                continue

            for control in other_controls:
                power_line = blend_utils.get_item_by_name(
                    control["power_line"])
                power_line_obj = self.get_builder_object_from_bpy_object(
                    power_line)
                prev_start_control = bpy.data.objects[
                    power_line_obj.start_control]
                prev_end_control = bpy.data.objects[power_line_obj.end_control]

                # Assign new controls.
                if control == prev_start_control:
                    power_line_obj.build_rig(unique_control, prev_end_control)
                else:
                    power_line_obj.build_rig(prev_start_control,
                                             unique_control)

                # Hide away control.
                blend_utils.remove_object(control.name)
Exemplo n.º 2
0
"""Convenient material related methods."""
import os

import bpy
import no_mans_sky_base_builder.utils.python as python_utils

# Get Colour Information.
FILE_PATH = os.path.dirname(os.path.realpath(__file__))
COLOURS_JSON = os.path.join(FILE_PATH, "..", "resources", "colours.json")
material_reference = python_utils.load_dictionary(COLOURS_JSON)

GHOSTED_JSON = os.path.join(FILE_PATH, "..", "resources", "ghosted.json")
ghosted_reference = python_utils.load_dictionary(GHOSTED_JSON)
GHOSTED_ITEMS = ghosted_reference["GHOSTED"]


def validate_material(colour_name, colour_value):
    """Creates or returns a material based on its name.
    
    Args:
        colour_name (str): The name of the material.
        colour_value (list): RGBA values representing the colour.
        
    Returns:
        bpy.Material: The Blender material.
    """
    # Retrieve material if it already exists.
    colour_material = bpy.data.materials.get(colour_name, None)
    # Create material.
    if not colour_material:
        colour_material = bpy.data.materials.new(name=colour_name)
Exemplo n.º 3
0
class Part(object):

    DEFAULT_USER_DATA = 0
    DEFAULT_BELONGS_TO_PRESET = False
    FILE_PATH = os.path.dirname(os.path.realpath(__file__))
    SNAP_MATRIX_JSON = os.path.join(FILE_PATH, "resources",
                                    "snapping_info.json")
    SNAP_PAIR_JSON = os.path.join(FILE_PATH, "resources",
                                  "snapping_pairs.json")

    SNAP_MATRIX_DICTIONARY = python_utils.load_dictionary(SNAP_MATRIX_JSON)
    SNAP_PAIR_DICTIONARY = python_utils.load_dictionary(SNAP_PAIR_JSON)

    SNAP_CACHE = {}

    def __init__(self,
                 object_id=None,
                 bpy_object=None,
                 builder_object=None,
                 user_data=None,
                 build_rigs=False):
        """Part __init__
        
        Args:
            object_id (str): The object ID we wish to create.
            bpy_object (bpy.data.object): An existing part we can reconstruct
                the class from.
            builder_object (Builder): The "parent" class for managing the NMS
                scene.
        """
        # Assign private variables.
        self.__builder_object = builder_object
        self.build_rigs = build_rigs
        user_data = user_data or self.DEFAULT_USER_DATA

        # Decide whether or not to retrieve from the scene
        # or create a new one.
        if bpy_object:
            self.__object = bpy_object
            object_id = bpy_object["ObjectID"]
        else:
            # Create new object.
            self.__object = self.retrieve_object_from_id(object_id)
            # Set properties.
            self.__object.hide_select = False
            self.object_id = object_id
            self.parent = None
            self.time_stamp = str(int(time.time()))
            self.belongs_to_preset = self.DEFAULT_BELONGS_TO_PRESET
            self.order = len(bpy.data.objects)
            # Assign material.
            material.assign_material(self.__object, user_data)
            # Set to origin.
            self.reset_transforms()

        # Place the blender item in the builder cache.
        builder_object.add_to_part_cache(object_id, self.__object)

        self.snap_id = object_id

    # Properties ---
    @property
    def name(self):
        return self.__object.name

    @name.setter
    def name(self, value):
        self.__object.name = value

    @property
    def object(self):
        return self.__object

    @object.setter
    def object(self, value):
        self.__object = value

    @property
    def location(self):
        return self.__object.location

    @location.setter
    def location(self, value):
        self.__object.location = value

    @property
    def rotation(self):
        return self.__object.euler_rotation

    @rotation.setter
    def rotation(self, value):
        self.__object.rotation_euler = value

    @property
    def scale(self):
        return self.__object.scale

    @scale.setter
    def scale(self, value):
        self.__object.scale = value

    @property
    def builder(self):
        return self.__builder_object

    @property
    def matrix_world(self):
        return self.__object.matrix_world

    @matrix_world.setter
    def matrix_world(self, value):
        self.__object.matrix_world = value

    @property
    def order(self):
        return self.__object["order"]

    @order.setter
    def order(self, value):
        self.__object["order"] = value

    @property
    def object_id(self):
        return self.__object["ObjectID"]

    @object_id.setter
    def object_id(self, value):
        self.__object["ObjectID"] = value.replace("^", "")

    @property
    def snap_id(self):
        return self.__object["SnapID"]

    @snap_id.setter
    def snap_id(self, value):
        self.__object["SnapID"] = value.replace("^", "")

    @property
    def object_id_format(self):
        return "^{0}".format(self.object_id)

    @property
    def time_stamp(self):
        return self.__object["Timestamp"]

    @time_stamp.setter
    def time_stamp(self, value):
        self.__object["Timestamp"] = value

    @property
    def user_data(self):
        return self.__object["UserData"]

    @user_data.setter
    def user_data(self, value):
        self.__object["UserData"] = str(value)

    @property
    def belongs_to_preset(self):
        return self.__object["belongs_to_preset"]

    @belongs_to_preset.setter
    def belongs_to_preset(self, value):
        self.__object["belongs_to_preset"] = value

    @property
    def hide_select(self):
        return self.__object.hide_select

    @hide_select.setter
    def hide_select(self, value):
        self.__object.hide_select = value

    @property
    def snapped_to(self):
        return self.__object["snapped_to"]

    @snapped_to.setter
    def snapped_to(self, value):
        self.__object["snapped_to"] = value

    # Methods ---
    def reset_transforms(self):
        """Reset transformations to default."""
        # Lock all translations and rotations.
        self.__object.lock_location = [False, False, False]
        self.__object.lock_rotation = [False, False, False]
        self.__object.lock_scale = [False, False, False]

        self.location = [0.0, 0.0, 0.0]
        self.rotation = [1.5708, 0.0, 0.0]
        self.scale = [1.0, 1.0, 1.0]

    def remove_constraints(self):
        # Remove any constraints from the duplication.
        for c in self.__object.constraints:
            self.__object.constraints.remove(c)

        # Remove any drivers from the duplication.
        anim_data = self.__object.animation_data
        if anim_data:
            drivers_data = anim_data.drivers
            for dr in drivers_data:
                self.__object.driver_remove(dr.data_path, -1)

    def duplicate(self):
        """Duplicate the part and return it."""
        # Create new object as whole.
        new_object = self.__object.copy()
        # Transfer a copy of the mesh and material.
        new_object.data = self.__object.data.copy()

        if self.__object.active_material:
            new_object.active_material = self.__object.active_material.copy()

        # Clear Parent
        if new_object.parent:
            new_object.parent = None
            new_object.matrix_parent_inverse = mathutils.Matrix.Identity(4)

        # Convert to Base Builder Object.
        print("BUILDER", self.builder)
        new_object = self.builder.get_builder_object_from_bpy_object(
            new_object)
        new_object.remove_constraints()
        return new_object

    def select(self, value=True, add=False):
        blend_utils.select(self.object)

    @property
    def parent(self):
        return self.__object.parent

    @parent.setter
    def parent(self, bpy_object):
        """Parent this to another blender object.
        
        Whilst maintaining the offset.
        """
        if bpy_object:
            self.__object.parent = bpy_object
            self.__object.matrix_parent_inverse = bpy_object.matrix_world.inverted(
            )

    def add_to_scene(self):
        blend_utils.add_to_scene(self.object)

    def retrieve_object_from_id(self, object_id):
        """Given an ID. Find the best way to create a new one.
        
        As loading OBJ can be resource and time intensive. We can figure ways
        of caching and duplicating existing items via th Builder class.

        Method Priority.
        - If the object already exists in the builder cache, we can just
            dupliciate it.
        - If it doesn't exist in the cache, find the obj path.
        - If the obj path doesn't exist, just create a cube.
        """
        # Duplicate existing.
        existing_object = self.builder.find_object_by_id(object_id)
        if existing_object:
            duped = existing_object.duplicate()
            duped = duped.object
            blend_utils.add_to_scene(duped)
            return duped

        # Locate OBJ.
        obj_path = self.builder.get_obj_path(object_id)
        # If it exists, import the obj.
        if obj_path and os.path.isfile(obj_path):
            bpy.ops.import_scene.obj(filepath=obj_path, split_mode="OFF")
            item = bpy.data.objects[bpy.context.selected_objects[0].name]
            item.select_set(False)
            blend_utils.add_to_scene(item)
            return item

        # Create cube.
        bpy.ops.mesh.primitive_cube_add()
        item = bpy.data.objects[bpy.context.object.name]
        blend_utils.add_to_scene(item)
        return item

    # Serialisation ---
    def serialise(self):
        """Return NMS compatible dictionary.

        Returns:
            dict: Dictionary of part information.
        """
        # Get Matrix Data
        world_matrix = self.matrix_world
        # Bring the matrix from Blender Z-Up soace into standard Y-up space.
        z_compensate = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X')
        world_matrix_offset = z_compensate @ world_matrix
        # Retrieve Position, Up and At vectors.
        pos = world_matrix_offset.decompose()[0]
        up = [
            world_matrix_offset[0][1], world_matrix_offset[1][1],
            world_matrix_offset[2][1]
        ]
        at = [
            world_matrix_offset[0][2], world_matrix_offset[1][2],
            world_matrix_offset[2][2]
        ]

        return {
            "ObjectID": self.object_id_format,
            "Position": [pos[0], pos[1], pos[2]],
            "Up": [up[0], up[1], up[2]],
            "At": [at[0], at[1], at[2]],
            "Timestamp": int(self.time_stamp),
            "UserData": int(self.user_data)
        }

    # Class Methods ---
    @classmethod
    def deserialise_from_object(cls, bpy_object, builder_object):
        """Reconstruct the class using an existing Blender object."""
        part = cls(bpy_object=bpy_object, builder_object=builder_object)
        return part

    @classmethod
    def deserialise_from_data(cls, data, builder_object, build_rigs=True):
        """Reconstruct the class using an a data.
        
        Data usually comes from NMS or the serialise method.
        """
        # Create object based on the ID.
        object_id = data["ObjectID"].replace("^", "")
        user_data = data.get("UserData", 0)
        part = cls(object_id=object_id,
                   builder_object=builder_object,
                   build_rigs=build_rigs,
                   user_data=user_data)
        # Get location data.
        pos = data.get("Position", [0.0, 0.0, 0.0])
        up = data.get("Up", [0.0, 0.0, 0.0])
        at = data.get("At", [0.0, 0.0, 0.0])
        # Set part position.
        world_matrix = cls.create_matrix_from_vectors(pos, up, at)
        part.matrix_world = world_matrix
        part.rotation = world_matrix.to_euler()
        # Apply metadata
        part.time_stamp = str(data.get("Timestamp", int(time.time())))
        part.user_data = data.get("UserData", 0)
        return part

    # Static Methods ---
    @staticmethod
    def create_matrix_from_vectors(pos, up, at):
        """Create a world space matrix given by an Up and At vector.
        
        Args:
            pos (list): 3 element list/vector representing the x,y,z position.
            up (list): 3 element list/vector representing the up vector.
            at (list): 3 element list/vector representing the aim vector.
        """
        # Create vectors that will construct the matrix.
        up_vector = mathutils.Vector(up)
        at_vector = mathutils.Vector(at)
        right_vector = at_vector.cross(up_vector)

        # Make sure the right vector magnitude is an average of the other two.
        right_vector.normalize()
        right_vector *= -1

        average = ((up_vector.length + at_vector.length) / 2)
        right_vector.length = right_vector.length * average

        # Construct a world matrix for the item.
        mat = mathutils.Matrix(
            [[right_vector[0], up_vector[0], at_vector[0], pos[0]],
             [right_vector[1], up_vector[1], at_vector[1], pos[1]],
             [right_vector[2], up_vector[2], at_vector[2], pos[2]],
             [0.0, 0.0, 0.0, 1.0]])
        # Create a rotation matrix that turns the whole thing 90 degrees at the origin.
        # This is to compensate blender's Z up axis.
        mat_rot = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'X')
        mat = mat_rot @ mat
        return mat

    # Snapping Methods ---
    def get_snap_points(self):
        """Get the snap points related to this part."""
        # First find the snap group the part belonds to.
        use_group = self.get_snap_group()
        # Get nothing if it doesn't belong anywhere.
        if not use_group:
            return

        # Further validation.
        if "snap_points" not in self.SNAP_MATRIX_DICTIONARY[use_group]:
            return

        # Get the snap points from the dictionary.
        return self.SNAP_MATRIX_DICTIONARY[use_group]["snap_points"]

    def get_snap_group(self):
        """Search through the grouping dictionary and return the snap group.
        
        Args:
            part_id (str): The ID of the building part.
        """
        for group, value in self.SNAP_MATRIX_DICTIONARY.items():
            parts = value["parts"]
            if self.object_id in parts:
                return group

    def get_snap_pair_options(self, target_item):
        """Get the compatible snap points

        Args:
            target_item (part.Part): 
        """
        # Get Groups.
        target_group = target_item.get_snap_group()
        source_group = self.get_snap_group()

        # If no target and no source then we don't do anything.
        if not target_group and not source_group:
            return None

        # Get Pairing.
        if target_group in self.SNAP_PAIR_DICTIONARY:
            snapping_dictionary = self.SNAP_PAIR_DICTIONARY[target_group]
            if source_group in snapping_dictionary:
                return snapping_dictionary[source_group]

    def snap_to(self,
                target,
                next_target=False,
                prev_target=False,
                next_source=False,
                prev_source=False):
        """Snap this item to the specified builder object.

        Args:
            target (part.Part): The item to snap to.
            next_target (bool): Cycle to the next target snap point.
            prev_target (bool): Cycle to the prev target snap point.
            next_source (bool): Cycle to the next source snap point.
            prev_source (bool): Cycle to the prev source snap point.
        """
        # For preset targets, just snap them together.
        if hasattr(target, "control"):
            mat_rot = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'X')
            use_matrix = copy(target.matrix_world) @ mat_rot
            self.matrix_world = use_matrix
            self.select()
            return

        # Check for Snap IDs
        target_snap_check = "SnapID" in target.object
        source_snap_check = "SnapID" in self.object
        if not target_snap_check or not source_snap_check:
            return False

        # First and foremost, just move the source on top of the target.
        self.matrix_world = copy(target.matrix_world)
        # Set the snapped to property so it remembers it without selection.
        self.snapped_to = target.name
        self.select()

        # Get Pairing options.
        snap_pairing_options = self.get_snap_pair_options(target)
        # If no snap details are avaialbe then don't bother.
        if not snap_pairing_options:
            return False

        # Get pair options.
        target_pairing_options = [
            part.strip() for part in snap_pairing_options[0].split(",")
        ]
        source_pairing_options = [
            part.strip() for part in snap_pairing_options[1].split(",")
        ]

        # Get the per item reference.
        target_item_snap_reference = self.SNAP_CACHE.get(target.name, {})
        # Find corresponding dict in snap reference.
        target_local_matrix_datas = target.get_snap_points()
        if target_local_matrix_datas:
            # Get the default target.
            default_target_key = target_pairing_options[0]
            target_key = target_item_snap_reference.get(
                "target", default_target_key)

            # If the previous key is not in the available options
            # revert to default.
            if target_key not in target_pairing_options:
                target_key = default_target_key

            if next_target:
                target_key = python_utils.get_adjacent_dict_key(
                    target_pairing_options, target_key, step="next")

            if prev_target:
                target_key = python_utils.get_adjacent_dict_key(
                    target_pairing_options, target_key, step="prev")

        # Get the per item reference.
        source_item_snap_reference = self.SNAP_CACHE.get(self.name, {})

        # Find corresponding dict.
        source_local_matrix_datas = self.get_snap_points()
        if source_local_matrix_datas:
            default_source_key = source_pairing_options[0]

            # If the source and target are the same, the source key can be
            # the opposite of target.
            if self.snap_id == target.snap_id:
                default_source_key = target_local_matrix_datas[target_key].get(
                    "opposite", default_source_key)

            # Get the source key from the item reference, or use the default.
            if (self.snap_id == target.snap_id) and (prev_target
                                                     or next_target):
                source_key = target_local_matrix_datas[target_key].get(
                    "opposite", default_source_key)
            else:
                source_key = source_item_snap_reference.get(
                    "source", default_source_key)

            # If the previous key is not in the available options
            # revert to default.
            if source_key not in source_pairing_options:
                source_key = default_source_key

            if next_source:
                source_key = python_utils.get_adjacent_dict_key(
                    source_pairing_options, source_key, step="next")

            if prev_source:
                source_key = python_utils.get_adjacent_dict_key(
                    source_pairing_options, source_key, step="prev")

        # If no keys were found, don't snap.
        if not source_key and not target_key:
            return False

        # Snap-point to snap-point matrix maths.
        # As I've defined X to be always outward facing, we snap the rotated
        # matrix to the point.
        # s = source, t = target, o = local snap matrix.
        # [(s.so)^-1 * (t.to)] * [(s.so) * 180 rot-matrix * (s.so)^-1]

        # First Create a Flipped Y Matrix based on local offset.
        start_matrix = copy(self.matrix_world)
        start_matrix_inv = copy(self.matrix_world)
        start_matrix_inv.invert()
        offset_matrix = mathutils.Matrix(
            source_local_matrix_datas[source_key]["matrix"])

        # Target Matrix
        target_matrix = copy(target.matrix_world)
        target_offset_matrix = mathutils.Matrix(
            target_local_matrix_datas[target_key]["matrix"])

        # Calculate the location of the target matrix.
        target_snap_matrix = target_matrix @ target_offset_matrix

        # Calculate snap position.
        snap_matrix = start_matrix @ offset_matrix
        snap_matrix_inv = copy(snap_matrix)
        snap_matrix_inv.invert()

        # Rotate by 180 around Y at the origin.
        origin_matrix = snap_matrix_inv @ snap_matrix
        rotation_matrix = mathutils.Matrix.Rotation(math.radians(180.0), 4,
                                                    "Y")
        origin_flipped_matrix = rotation_matrix @ origin_matrix
        flipped_snap_matrix = snap_matrix @ origin_flipped_matrix

        flipped_local_offset = start_matrix_inv @ flipped_snap_matrix

        # Diff between the two.
        flipped_local_offset.invert()
        target_location = target_snap_matrix @ flipped_local_offset

        # Set matrix, and then re-apply radian rotation for better accuracy.
        self.matrix_world = target_location
        self.rotation = target_location.to_euler()

        # Find the opposite source key and set it.
        next_target_key = target_key

        # If we are working with the same objects.
        next_target_key = source_local_matrix_datas[source_key].get(
            "opposite", None)

        # Update source item refernece.
        source_item_snap_reference["source"] = source_key
        source_item_snap_reference["target"] = next_target_key

        # Update target item reference.
        target_item_snap_reference["target"] = target_key

        # Update per item reference.
        self.SNAP_CACHE[self.name] = source_item_snap_reference
        self.SNAP_CACHE[target.name] = target_item_snap_reference

        return True

    def get_closest_snap_points(self,
                                target,
                                source_filter=None,
                                target_filter=None):
        """Get the closest snap points of two objects.
        
        Args:
            target (part.Part): The object we are snapping on to.
            source_filter (str): A filter for the snap points being used.
            target_filter (str): A filter for the snap points being used.
        """
        source_matrices = self.get_snap_points()
        target_matrices = target.get_snap_points()

        lowest_source_key = None
        lowest_target_key = None
        lowest_distance = 9999999
        for source_key, source_info in source_matrices.items():
            # Check source filter.
            if source_filter and source_filter not in source_key:
                continue

            for target_key, target_info in target_matrices.items():
                # Check target filter.
                if target_filter and target_filter not in target_key:
                    continue

                # Find the distance and check if its lower then the one
                # stored.
                local_source_matrix = mathutils.Matrix(source_info["matrix"])
                local_target_matrix = mathutils.Matrix(target_info["matrix"])

                source_snap_matrix = self.matrix_world @ local_source_matrix
                target_snap_matrix = target.matrix_world @ local_target_matrix

                distance = blend_utils.get_distance_between(
                    source_snap_matrix, target_snap_matrix)
                if distance < lowest_distance:
                    lowest_distance = distance
                    lowest_source_key = source_key
                    lowest_target_key = target_key

        return lowest_source_key, lowest_target_key

    def get_matrix_from_key(self, key):
        """Get the matrix for a given item and the snap key."""
        # Get relavant snap information from item.
        snap_matrices = self.get_snap_points()
        # Validate key entry.
        if key not in snap_matrices:
            return None
        # Return matrix.
        return snap_matrices[key]["matrix"]