All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.oneandone.maven.plugins.spritepacker.PackGrowing Maven / Gradle / Ivy

package net.oneandone.maven.plugins.spritepacker;

import java.awt.Dimension;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

/**
 * An algorithm for packing images into a spritesheet
 *
 * @author Robert Murphy
 */
public class PackGrowing {

    private static final Comparator IMAGE_ORDER = new Comparator() {
        @Override
        public int compare(NamedImage arg0, NamedImage arg1) {
            int max0 = Math.max(arg0.getWidth(), arg0.getHeight());
            int max1 = Math.max(arg1.getWidth(), arg1.getHeight());
            return max1 - max0;
        }
    };

    private final List images;
    private final int padding;

    private final Map positionMap;
    private Node root;

    /**
     * Creates a new PackGrowing for the given images and padding.
     *
     * @param images  the list of images to be packed
     * @param padding the amount of padding to put between sprites, in pixels
     */
    PackGrowing(List images, int padding) {
        this.images = new ArrayList<>(images);
        this.padding = padding;
        positionMap = new IdentityHashMap<>();
    }

    /**
     * Fit the images and return an ImagePacking containing the outer dimensions
     * of the spritesheet and a Map of images to positions.
     *
     * @param images    the list of images to be packed
     * @param padding   the amount of padding to put between sprites, in pixels
     * @return          the resulting ImagePacking, containing dimension and image position data
     */
    public static ImagePacking fit(List images, int padding) {
        PackGrowing packGrowing = new PackGrowing(images, padding);
        Dimension dimension = packGrowing.fit();
        return new ImagePacking(dimension, packGrowing.positionMap);
    }

    /**
     * Fits the images and return the outer dimensions of the resulting spritesheet.
     *
     * @return  the outer dimensions of the spritesheet after fitting the images
     */
    private Dimension fit() {
        if (images.isEmpty()) {
            return new Dimension(0, 0);
        }
        // sort the images, without modifying the sort order of the original image list
        sortImages(images);

        root = new Node(padding, this.padding, images.get(0).getWidth(), images.get(0).getHeight());

        for (NamedImage image : images) {
            final int width = image.getWidth();
            final int height = image.getHeight();

            final Node availableNode = findNode(root, width, height);
            final Node newNode = (availableNode == null) ? growNode(width, height, padding) : splitNode(availableNode, width, height, padding);

            positionMap.put(image, new Point(newNode.getX(), newNode.getY()));
        }

        return new Dimension(root.getWidth() + padding * 2, root.getHeight() + padding * 2);
    }

    /**
     * Sort images by max width / height descending
     *
     * @param images list of images to sort
     */
    private void sortImages(List images) {
        // Sort by max width / height descending
        Collections.sort(images, IMAGE_ORDER);
    }

    /**
     * Find an available unused node of the given width and height, starting from nodeIn
     *
     * @param nodeIn the node from which to start the find operation
     * @param width  width of the node
     * @param height height of the node
     * @return       found node, or null if no available node was found
     */
    private Node findNode(Node nodeIn, int width, int height) {
        if (nodeIn.isUsed()) {
            Node rightAvailable = findNode(nodeIn.getRight(), width, height);
            return (rightAvailable != null) ? rightAvailable : findNode(nodeIn.getDown(), width, height);
        }

        if (width <= nodeIn.getWidth() && height <= nodeIn.getHeight()) {
            return nodeIn;
        }

        return null;
    }

    /**
     * Split a node into a node of size width x height and return the remaining space to the pool
     *
     * @param nodeIn the node to split
     * @param width  width
     * @param height height
     * @return       the split node
     */
    private Node splitNode(Node nodeIn, int width, int height, int padding) {
        nodeIn.setUsed(true);
        nodeIn.setDown(new Node(nodeIn.getX(), nodeIn.getY() + height + padding, nodeIn.getWidth(), nodeIn.getHeight() - height - padding));
        nodeIn.setRight(new Node(nodeIn.getX() + width + padding, nodeIn.getY(), nodeIn.getWidth() - width - padding, height));
        return nodeIn;
    }

    /**
     * Grow the size of the available space to add another block, and return new available node
     *
     * @param width  width needed
     * @param height height needed
     * @return       new available node
     */
    Node growNode(int width, int height, int padding) {
        boolean canGrowDown = width <= root.getWidth();
        boolean canGrowRight = height <= root.getHeight();

        // attempt to keep square-ish by growing right when height is much greater than width
        boolean shouldGrowRight = canGrowRight && root.getHeight() >= root.getWidth() + width;
        // attempt to keep square-ish by growing down when width is much greater than height
        boolean shouldGrowDown = canGrowDown && root.getWidth() >= root.getHeight() + height;

        if (shouldGrowRight) {
            return growRight(width, height, padding);
        } else if (shouldGrowDown) {
            return growDown(width, height, padding);
        } else if (canGrowRight) {
            return growRight(width, height, padding);
        } else if (canGrowDown) {
            return growDown(width, height, padding);
        }

        // need to ensure sensible root starting size to avoid this happening
        return null;
    }

    /**
     * Grow the root node right, and return new available node
     *
     * @param width  width needed
     * @param height height needed
     * @return       new available node
     */
    Node growRight(int width, int height, int padding) {
        Node newRoot = new Node(root.getX(), root.getY(), root.getWidth() + width + padding, root.getHeight());
        newRoot.setUsed(true);
        newRoot.setDown(root);
        newRoot.setRight(new Node(root.getWidth() + root.getX() + padding, root.getY(), width, root.getHeight()));
        root = newRoot;

        Node availableNode = findNode(root, width, height);
        if (availableNode != null) {
        return splitNode(availableNode, width, height, padding);
    }

        return null;
    }

    /**
     * Grow the root node down and return new available node
     *
     * @param width  width needed
     * @param height height needed
     * @return       new available node
     */
    Node growDown(int width, int height, int padding) {
        Node newRoot = new Node(root.getX(), root.getY(), root.getWidth(), root.getHeight() + height + padding);
        newRoot.setUsed(true);
        newRoot.setDown(new Node(root.getX(), root.getY() + root.getHeight() + padding, root.getWidth(), height));
        newRoot.setRight(root);
        root = newRoot;

        Node availableNode = findNode(root, width, height);
        if (availableNode != null) {
            return splitNode(availableNode, width, height, padding);
        }

        return null;
    }

    /**
     * Allows setting the root to test certain edge cases which cannot occur in actual fittings.
     * @param root the root node to set
     */
    void setRoot(Node root) {
        this.root = root;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy