Refactor Stitcher class to use TurboStitcher for improved performance

This commit is contained in:
HoosierTransfer 2024-06-19 15:49:05 -04:00
parent b06faea26e
commit a8347a5126
14 changed files with 499 additions and 38 deletions

View File

@ -0,0 +1,27 @@
package net.hoosiertransfer.Argon.stitcher;
import java.util.Collections;
import java.util.List;
import net.minecraft.client.renderer.texture.Stitcher;
public class HolderSlot extends SpriteSlot {
private final Stitcher.Holder holder;
public HolderSlot(Stitcher.Holder holder) {
this.holder = holder;
width = holder.getWidth();
height = holder.getHeight();
}
public Stitcher.Holder getHolder() {
return holder;
}
@Override
public List<Stitcher.Slot> getSlots(Rect2D parent) {
Stitcher.Slot slot = new Stitcher.Slot(x + parent.x, y + parent.y, width, height);
slot.insertHolder(holder);
return Collections.singletonList(slot);
}
}

View File

@ -0,0 +1,26 @@
package net.hoosiertransfer.Argon.stitcher;
public class Rect2D implements Comparable<Rect2D> {
public int x;
public int y;
public int width;
public int height;
public Rect2D() {
}
public Rect2D(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
@Override
public int compareTo(Rect2D o) {
int ourArea = width * height;
int theirArea = o.width * o.height;
// will make sure that larger areas go first
return theirArea - ourArea;
}
}

View File

@ -0,0 +1,9 @@
package net.hoosiertransfer.Argon.stitcher;
import net.minecraft.client.renderer.texture.Stitcher;
import java.util.List;
public abstract class SpriteSlot extends Rect2D {
public abstract List<Stitcher.Slot> getSlots(Rect2D parent);
}

View File

@ -0,0 +1,6 @@
package net.hoosiertransfer.Argon.stitcher;
public enum StitcherState {
SETUP,
STITCHED,
}

View File

@ -0,0 +1,4 @@
package net.hoosiertransfer.Argon.stitcher;
public class TooBigException extends Exception {
}

View File

@ -0,0 +1,177 @@
package net.hoosiertransfer.Argon.stitcher;
import java.util.*;
import com.google.common.collect.ImmutableSet;
import net.hoosiertransfer.Argon.stitcher.packing2d.Algorithm;
import net.hoosiertransfer.Argon.stitcher.packing2d.Packer;
import net.minecraft.client.renderer.texture.Stitcher;
import net.minecraft.util.ResourceLocation;
public class TurboStitcher extends SpriteSlot {
private final int maxWidth;
private final int maxHeight;
private final boolean forcePowerOf2;
private List<SpriteSlot> slots = new LinkedList<>();
private List<SpriteSlot> finalizedSlots = null;
private boolean needsSorting = false;
private int trackedArea = 0;
private StitcherState state = StitcherState.SETUP;
private static final boolean OPTIMAL_PACKING = true;
public TurboStitcher(int maxWidth, int maxHeight, boolean forcePowerOf2) {
this.maxHeight = maxHeight;
this.maxWidth = maxWidth;
this.forcePowerOf2 = forcePowerOf2;
}
private static int nextPowerOfTwo(int number) {
number--;
number |= number >>> 1;
number |= number >>> 2;
number |= number >>> 4;
number |= number >>> 8;
number |= number >>> 16;
number++;
return number;
}
public void addSprite(Stitcher.Holder holder) {
addSprite(new HolderSlot(holder));
}
public void addSprite(SpriteSlot rect) {
verifyState(StitcherState.SETUP);
slots.add(rect);
trackedArea += rect.width * rect.height;
needsSorting = true;
}
public void reset() {
state = StitcherState.SETUP;
}
public void dropFirst() {
verifyState(StitcherState.SETUP);
if (slots.size() > 0) {
SpriteSlot slot = slots.remove(0);
String name;
if (slot instanceof HolderSlot) {
name = ((HolderSlot) slot).getHolder().getAtlasSprite().getIconName();
} else {
name = "unknown";
}
trackedArea -= slot.width * slot.height;
} else
throw new IllegalStateException();
}
public void retainAllSprites(Set<ResourceLocation> theLocations) {
verifyState(StitcherState.SETUP);
Set<String> locationStrings = new HashSet<>();
for (ResourceLocation rl : theLocations)
locationStrings.add(rl.toString());
slots.removeIf(slot -> {
if (slot instanceof HolderSlot) {
String spriteName = ((HolderSlot) slot).getHolder().getAtlasSprite().getIconName();
// drop textures that are:
// - not in the desired list of locations
if (!locationStrings.contains(spriteName)) {
trackedArea -= slot.width * slot.height;
return true;
}
}
return false;
});
}
public void stitch() throws TooBigException {
verifyState(StitcherState.SETUP);
width = 0;
height = 0;
if (slots.size() == 0) {
state = StitcherState.STITCHED;
return;
}
// ensure we have largest sprites first
if (needsSorting) {
Collections.sort(slots);
needsSorting = false;
}
if (trackedArea > (maxWidth * maxHeight)) {
throw new TooBigException();
}
// start with a really simple check, if the total area is larger than we could
// handle, we know this will fail
for (SpriteSlot slot : slots) {
width = Math.max(width, slot.width);
}
if (forcePowerOf2 || !OPTIMAL_PACKING) {
width = nextPowerOfTwo(width);
}
if (width > maxWidth) {
throw new TooBigException();
}
width = Math.max(width >>> 1, 1);
List<SpriteSlot> packedSlots;
List<SpriteSlot> toPack = new ArrayList<>(slots);
do {
if (width == maxWidth) {
throw new TooBigException();
}
if (forcePowerOf2 || !OPTIMAL_PACKING) {
width *= 2;
} else {
width += Math.min(width, 16);
}
if (width > maxWidth) {
width = maxWidth;
}
packedSlots = Packer.pack(toPack, Algorithm.FIRST_FIT_DECREASING_HEIGHT, width);
height = 0;
for (SpriteSlot sprite : packedSlots) {
height = Math.max(height, sprite.y + sprite.height);
}
if (forcePowerOf2) {
height = nextPowerOfTwo(height);
}
} while (height > maxHeight || height > width);
finalizedSlots = packedSlots;
state = StitcherState.STITCHED;
// TextureCollector.weaklyCollectedTextures = ImmutableSet.of();
}
public List<Stitcher.Slot> getSlots() {
return getSlots(new Rect2D());
}
public List<Stitcher.Slot> getSlots(Rect2D parent) {
verifyState(StitcherState.STITCHED);
ArrayList<Stitcher.Slot> mineSlots = new ArrayList<Stitcher.Slot>();
Rect2D offset = new Rect2D(x + parent.x, y + parent.y, width, height);
for (SpriteSlot slot : finalizedSlots) {
mineSlots.addAll(slot.getSlots(offset));
}
/*
* for(Stitcher.Slot slot : mineSlots) {
* System.out.println(slot.getStitchHolder().getAtlasSprite().getIconName());
* }
*
*/
return mineSlots;
}
private void verifyState(StitcherState... allowedStates) {
boolean ok = false;
for (StitcherState state : allowedStates) {
if (state == this.state) {
ok = true;
break;
}
}
if (!ok) {
throw new IllegalStateException("Cold not execute operation: invalid state");
}
}
}

View File

@ -0,0 +1,5 @@
package net.hoosiertransfer.Argon.stitcher;
public class WrongDimensionException extends RuntimeException {
public static WrongDimensionException INSTANCE = new WrongDimensionException();
}

View File

@ -0,0 +1,6 @@
package net.hoosiertransfer.Argon.stitcher.packing2d;
public enum Algorithm {
FIRST_FIT_DECREASING_HEIGHT,
BEST_FIT_DECREASING_HEIGHT
}

View File

@ -0,0 +1,38 @@
package net.hoosiertransfer.Argon.stitcher.packing2d;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import net.hoosiertransfer.Argon.stitcher.Rect2D;
public abstract class Packer<T extends Rect2D> {
final int stripWidth;
final List<T> rectangles;
Packer(int stripWidth, List<T> rectangles) {
this.stripWidth = stripWidth;
this.rectangles = rectangles;
}
public static <U extends Rect2D> List<U> pack(List<U> rectangles, Algorithm algorithm, int stripWidth) {
Packer<U> packer;
switch (algorithm) {
case FIRST_FIT_DECREASING_HEIGHT:
packer = new PackerFFDH<>(stripWidth, rectangles);
return packer.pack();
case BEST_FIT_DECREASING_HEIGHT:
packer = new PackerBFDH<>(stripWidth, rectangles);
break;
default:
return new ArrayList<>();
}
return packer.pack();
}
public abstract List<T> pack();
void sortByNonIncreasingHeight(List<T> rectangles) {
rectangles.sort(Comparator.<T>comparingInt((rect) -> rect.height).reversed());
}
}

View File

@ -0,0 +1,48 @@
package net.hoosiertransfer.Argon.stitcher.packing2d;
import java.util.ArrayList;
import java.util.List;
import net.hoosiertransfer.Argon.stitcher.Rect2D;
public class PackerBFDH<T extends Rect2D> extends Packer<T> {
private final List<StripLevel> levels;
public PackerBFDH(int stripWidth, List<T> rectangles) {
super(stripWidth, rectangles);
levels = new ArrayList<>();
}
@Override
public List<T> pack() {
int top = 0;
sortByNonIncreasingHeight(rectangles);
for (T r : rectangles) {
StripLevel levelWithSmallestResidual = null;
for (StripLevel level : levels) {
if (!level.canFit(r)) {
continue;
}
if (levelWithSmallestResidual != null &&
levelWithSmallestResidual.availableWidth() > level.availableWidth()) {
levelWithSmallestResidual = level;
} else if (levelWithSmallestResidual == null) {
levelWithSmallestResidual = level;
}
}
if (levelWithSmallestResidual == null) {
StripLevel level = new StripLevel(stripWidth, top);
level.fitRectangle(r);
levels.add(level);
top += r.height;
} else {
StripLevel newLevel = levelWithSmallestResidual.fitRectangle(r);
if (newLevel != null) {
levels.add(levels.indexOf(levelWithSmallestResidual) + 1, newLevel);
}
}
}
return rectangles;
}
}

View File

@ -0,0 +1,43 @@
package net.hoosiertransfer.Argon.stitcher.packing2d;
import java.util.ArrayList;
import java.util.List;
import net.hoosiertransfer.Argon.stitcher.Rect2D;
class PackerFFDH<T extends Rect2D> extends Packer<T> {
private final List<StripLevel> levels = new ArrayList<>(1);
private int top = 0;
public PackerFFDH(int stripWidth, List<T> rectangles) {
super(stripWidth, rectangles);
}
@Override
public List<T> pack() {
sortByNonIncreasingHeight(rectangles);
for (T r : rectangles) {
boolean fitsOnALevel = false;
for (int i = 0; i < levels.size(); i++) {
StripLevel level = levels.get(i);
fitsOnALevel = level.checkFitRectangle(r);
if (!fitsOnALevel) {
continue;
}
StripLevel newStrip = level.fitRectangle(r);
if (newStrip != null) {
levels.add(0, newStrip);
}
break;
}
if (fitsOnALevel) {
continue;
}
StripLevel level = new StripLevel(stripWidth, top);
level.fitRectangle(r);
levels.add(level);
top += r.height;
}
return rectangles;
}
}

View File

@ -0,0 +1,51 @@
package net.hoosiertransfer.Argon.stitcher.packing2d;
import net.hoosiertransfer.Argon.stitcher.Rect2D;
class StripLevel {
private final int width;
private final int top;
private int availableWidth;
private int tallest = -1;
StripLevel(int width, int top) {
this.width = width;
this.availableWidth = width;
this.top = top;
}
boolean checkFitRectangle(Rect2D r) {
return (tallest < 0 || r.height <= tallest) && r.width <= availableWidth;
}
StripLevel fitRectangle(Rect2D r) {
if (tallest >= 0 && r.height > tallest) {
return null;
}
StripLevel newStrip = null;
int leftOver = availableWidth - r.width;
if (leftOver >= 0) {
r.x = width - availableWidth;
r.y = top;
if (tallest == -1) {
tallest = r.height;
}
if (r.height < tallest) {
newStrip = new StripLevel(width, top + r.height);
newStrip.availableWidth = availableWidth;
newStrip.tallest = tallest - r.height;
tallest = r.height;
}
availableWidth = leftOver;
}
return newStrip;
}
int availableWidth() {
return availableWidth;
}
boolean canFit(Rect2D r) {
return availableWidth - r.width >= 0 && (tallest < 0 || r.height <= tallest);
}
}

View File

@ -26,14 +26,21 @@ import net.minecraft.util.ResourceLocation;
/** /**
* Copyright (c) 2022 lax1dude. All Rights Reserved. * Copyright (c) 2022 lax1dude. All Rights Reserved.
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * DISCLAIMED.
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * DIRECT,
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. * POSSIBILITY OF SUCH DAMAGE.
* *
@ -151,7 +158,7 @@ public class EaglerTextureAtlasSprite {
} }
public void updateAnimation(IFramebufferGL[] copyColorFramebuffer) { public void updateAnimation(IFramebufferGL[] copyColorFramebuffer) {
if(animationCache == null) { if (animationCache == null) {
throw new IllegalStateException("Animation cache for '" + this.iconName + "' was never baked!"); throw new IllegalStateException("Animation cache for '" + this.iconName + "' was never baked!");
} }
++this.tickCounter; ++this.tickCounter;
@ -163,16 +170,19 @@ public class EaglerTextureAtlasSprite {
this.tickCounter = 0; this.tickCounter = 0;
int k = this.animationMetadata.getFrameIndex(this.frameCounter); int k = this.animationMetadata.getFrameIndex(this.frameCounter);
if (i != k && k >= 0 && k < this.framesTextureData.size()) { if (i != k && k >= 0 && k < this.framesTextureData.size()) {
animationCache.copyFrameLevelsToTex2D(k, this.originX, this.originY, this.width, this.height, copyColorFramebuffer); animationCache.copyFrameLevelsToTex2D(k, this.originX, this.originY, this.width, this.height,
copyColorFramebuffer);
} }
} else if (this.animationMetadata.isInterpolate()) { } else if (this.animationMetadata.isInterpolate()) {
float f = 1.0f - (float) this.tickCounter / (float) this.animationMetadata.getFrameTimeSingle(this.frameCounter); float f = 1.0f
- (float) this.tickCounter / (float) this.animationMetadata.getFrameTimeSingle(this.frameCounter);
int i = this.animationMetadata.getFrameIndex(this.frameCounter); int i = this.animationMetadata.getFrameIndex(this.frameCounter);
int j = this.animationMetadata.getFrameCount() == 0 ? this.framesTextureData.size() int j = this.animationMetadata.getFrameCount() == 0 ? this.framesTextureData.size()
: this.animationMetadata.getFrameCount(); : this.animationMetadata.getFrameCount();
int k = this.animationMetadata.getFrameIndex((this.frameCounter + 1) % j); int k = this.animationMetadata.getFrameIndex((this.frameCounter + 1) % j);
if (i != k && k >= 0 && k < this.framesTextureData.size()) { if (i != k && k >= 0 && k < this.framesTextureData.size()) {
animationCache.copyInterpolatedFrameLevelsToTex2D(i, k, f, this.originX, this.originY, this.width, this.height, copyColorFramebuffer); animationCache.copyInterpolatedFrameLevelsToTex2D(i, k, f, this.originX, this.originY, this.width,
this.height, copyColorFramebuffer);
} }
} }
} }
@ -295,9 +305,9 @@ public class EaglerTextureAtlasSprite {
} }
public void bakeAnimationCache() { public void bakeAnimationCache() {
if(animationMetadata != null) { if (animationMetadata != null) {
int mipLevels = framesTextureData.get(0).length; int mipLevels = framesTextureData.get(0).length;
if(animationCache == null) { if (animationCache == null) {
animationCache = new TextureAnimationCache(width, height, mipLevels); animationCache = new TextureAnimationCache(width, height, mipLevels);
} }
animationCache.initialize(framesTextureData); animationCache.initialize(framesTextureData);
@ -328,7 +338,7 @@ public class EaglerTextureAtlasSprite {
public void clearFramesTextureData() { public void clearFramesTextureData() {
this.framesTextureData.clear(); this.framesTextureData.clear();
if(this.animationCache != null) { if (this.animationCache != null) {
this.animationCache.free(); this.animationCache.free();
this.animationCache = null; this.animationCache = null;
} }
@ -347,7 +357,7 @@ public class EaglerTextureAtlasSprite {
this.setFramesTextureData(Lists.newArrayList()); this.setFramesTextureData(Lists.newArrayList());
this.frameCounter = 0; this.frameCounter = 0;
this.tickCounter = 0; this.tickCounter = 0;
if(this.animationCache != null) { if (this.animationCache != null) {
this.animationCache.free(); this.animationCache.free();
this.animationCache = null; this.animationCache = null;
} }
@ -365,16 +375,17 @@ public class EaglerTextureAtlasSprite {
Throwable t = new UnsupportedOperationException("PBR is not enabled"); Throwable t = new UnsupportedOperationException("PBR is not enabled");
try { try {
throw t; throw t;
}catch(Throwable tt) { } catch (Throwable tt) {
logger.error(t); logger.error(t);
} }
} }
public void updateAnimationPBR(IFramebufferGL[] copyColorFramebuffer, IFramebufferGL[] copyMaterialFramebuffer, int materialTexOffset) { public void updateAnimationPBR(IFramebufferGL[] copyColorFramebuffer, IFramebufferGL[] copyMaterialFramebuffer,
int materialTexOffset) {
Throwable t = new UnsupportedOperationException("PBR is not enabled"); Throwable t = new UnsupportedOperationException("PBR is not enabled");
try { try {
throw t; throw t;
}catch(Throwable tt) { } catch (Throwable tt) {
logger.error(t); logger.error(t);
} }
} }

View File

@ -1,11 +1,11 @@
package net.minecraft.client.renderer.texture; package net.minecraft.client.renderer.texture;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import net.lax1dude.eaglercraft.v1_8.HString; import net.hoosiertransfer.Argon.stitcher.TooBigException;
import net.hoosiertransfer.Argon.stitcher.TurboStitcher;
import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerTextureAtlasSprite; import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerTextureAtlasSprite;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@ -13,6 +13,7 @@ import com.google.common.collect.Sets;
import net.minecraft.client.renderer.StitcherException; import net.minecraft.client.renderer.StitcherException;
import net.minecraft.util.MathHelper; import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
/** /**
* + * +
@ -54,12 +55,16 @@ public class Stitcher {
private final boolean forcePowerOf2; private final boolean forcePowerOf2;
private final int maxTileDimension; private final int maxTileDimension;
private TurboStitcher masterStitcher;
private List<Stitcher.Holder> holdersToReadd = new ArrayList<>();
public Stitcher(int maxTextureWidth, int maxTextureHeight, boolean parFlag, int parInt1, int mipmapLevel) { public Stitcher(int maxTextureWidth, int maxTextureHeight, boolean parFlag, int parInt1, int mipmapLevel) {
this.mipmapLevelStitcher = mipmapLevel; this.mipmapLevelStitcher = mipmapLevel;
this.maxWidth = maxTextureWidth; this.maxWidth = maxTextureWidth;
this.maxHeight = maxTextureHeight; this.maxHeight = maxTextureHeight;
this.forcePowerOf2 = parFlag; this.forcePowerOf2 = parFlag;
this.maxTileDimension = parInt1; this.maxTileDimension = parInt1;
masterStitcher = new TurboStitcher(maxTextureWidth, maxTextureHeight, true);
} }
public int getCurrentWidth() { public int getCurrentWidth() {
@ -75,31 +80,32 @@ public class Stitcher {
if (this.maxTileDimension > 0) { if (this.maxTileDimension > 0) {
stitcher$holder.setNewDimension(this.maxTileDimension); stitcher$holder.setNewDimension(this.maxTileDimension);
} }
masterStitcher.addSprite(stitcher$holder);
holdersToReadd.add(stitcher$holder);
this.setStitchHolders.add(stitcher$holder); this.setStitchHolders.add(stitcher$holder);
} }
public void doStitch() { public void doStitch() {
Stitcher.Holder[] astitcher$holder = (Stitcher.Holder[]) this.setStitchHolders try {
.toArray(new Stitcher.Holder[this.setStitchHolders.size()]); masterStitcher.stitch();
Arrays.sort(astitcher$holder); currentWidth = masterStitcher.width;
currentHeight = masterStitcher.height;
for (int i = 0; i < astitcher$holder.length; ++i) { stitchSlots.clear();
Stitcher.Holder stitcher$holder = astitcher$holder[i]; stitchSlots.addAll(masterStitcher.getSlots());
if (!this.allocateSlot(stitcher$holder)) { } catch (TooBigException ignored) {
String s = HString.format("Unable to fit: %s - size: %dx%d - Maybe try a lowerresolution resourcepack?", throw new StitcherException(null,
new Object[] { stitcher$holder.getAtlasSprite().getIconName(), "Unable to fit all textures into atlas. Maybe try a lower resolution resourcepack?");
Integer.valueOf(stitcher$holder.getAtlasSprite().getIconWidth()), } finally {
Integer.valueOf(stitcher$holder.getAtlasSprite().getIconHeight()) }); masterStitcher.reset();
throw new StitcherException(stitcher$holder, s);
} }
} }
if (this.forcePowerOf2) { public void dropLargestSprite() {
this.currentWidth = MathHelper.roundUpToPowerOfTwo(this.currentWidth); masterStitcher.dropFirst();
this.currentHeight = MathHelper.roundUpToPowerOfTwo(this.currentHeight);
} }
public void retainAllSprites(Set<ResourceLocation> spriteLocations) {
masterStitcher.retainAllSprites(spriteLocations);
} }
public List<EaglerTextureAtlasSprite> getStichSlots() { public List<EaglerTextureAtlasSprite> getStichSlots() {
@ -306,6 +312,10 @@ public class Stitcher {
return this.holder; return this.holder;
} }
public void insertHolder(Stitcher.Holder holder) {
this.holder = holder;
}
public int getOriginX() { public int getOriginX() {
return this.originX; return this.originX;
} }