Update Studio previews

- Merge the eight additional commits from PR #1254.
- Preserve closure guards while refreshing object and hidden 3D views.
- Stop hidden crossfade timers and verify frame retention.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-25 17:25:18 -04:00
commit 584e8758a7
8 files changed
+659 -12

No files matched your search

@@ -0,0 +1,119 @@
package art.arcane.iris.studio.map;
import org.junit.After;
import org.junit.Test;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public final class FrameCrossfadeTest {
private final FrameCrossfade crossfade = new FrameCrossfade(() -> {
});
@After
public void stopRepaintTimer() throws Exception {
SwingUtilities.invokeAndWait(crossfade::clear);
}
@Test
public void heldFrameStaysFullyVisibleUntilTheNextFrameArrives() {
NoiseRenderCoordinator.Result frame = frame();
crossfade.hold(frame);
assertSame(frame, crossfade.outgoing());
assertEquals(1F, crossfade.outgoingOpacity(), 0F);
}
@Test
public void heldFrameFadesOnceTheNextFrameArrives() throws Exception {
SwingUtilities.invokeAndWait(() -> {
crossfade.hold(frame());
crossfade.reveal();
});
Thread.sleep(20L);
SwingUtilities.invokeAndWait(() -> assertTrue(crossfade.outgoing() == null || crossfade.outgoingOpacity() < 1F));
}
@Test
public void fadingStopsAndReleasesTheFrameWithoutPainting() throws Exception {
Timer timer = (Timer) field("repaintTimer");
SwingUtilities.invokeAndWait(() -> {
crossfade.hold(frame());
crossfade.reveal();
assertTrue(timer.isRunning());
});
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L);
while (timer.isRunning() && System.nanoTime() < deadline) {
Thread.sleep(10L);
}
SwingUtilities.invokeAndWait(() -> { });
assertFalse("Hidden views must stop scheduling fade repaints", timer.isRunning());
assertNull(field("outgoing"));
}
@Test
public void holdingDuringFadeStopsRepaintsUntilAnotherFrameArrives() throws Exception {
Timer timer = (Timer) field("repaintTimer");
NoiseRenderCoordinator.Result frame = frame();
SwingUtilities.invokeAndWait(() -> {
crossfade.hold(frame);
crossfade.reveal();
crossfade.hold(null);
assertFalse("A held frame must not keep the fade timer running", timer.isRunning());
assertSame(frame, crossfade.outgoing());
assertEquals(1F, crossfade.outgoingOpacity(), 0F);
crossfade.reveal();
assertTrue(timer.isRunning());
});
}
@Test
public void switchingAgainBeforeAnythingDrewKeepsTheFrameHeld() {
NoiseRenderCoordinator.Result frame = frame();
crossfade.hold(frame);
crossfade.hold(null);
assertSame(frame, crossfade.outgoing());
}
@Test
public void frameWithoutAHeldFrameHasNothingToFade() {
crossfade.reveal();
assertNull(crossfade.outgoing());
}
@Test
public void clearDropsTheHeldFrame() {
crossfade.hold(frame());
crossfade.clear();
assertNull(crossfade.outgoing());
}
private static NoiseRenderCoordinator.Result frame() {
return new NoiseRenderCoordinator.Result(null, null, 1D, 1L, 0D, 1D, 0.5D, 0L, 0L, 0L);
}
private Object field(String name) throws Exception {
Field field = FrameCrossfade.class.getDeclaredField(name);
field.setAccessible(true);
return field.get(crossfade);
}
}
@@ -0,0 +1,103 @@
package art.arcane.iris.studio.map;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import org.junit.Test;
import java.awt.image.BufferedImage;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public final class ModelFrontViewTest {
private static final String SEA_PICKLE = """
[
{ "from": [6, 0, 6], "to": [10, 6, 10], "faces": {
"up": { "uv": [4, 1, 8, 5], "texture": "#all" },
"south": { "uv": [0, 5, 4, 11], "texture": "#all" } } },
{ "from": [6, 5.95, 6], "to": [10, 5.95, 10], "faces": {
"up": { "uv": [8, 1, 12, 5], "texture": "#all" } } },
{ "from": [7.5, 5.2, 8], "to": [8.5, 8.7, 8],
"rotation": { "origin": [8, 8, 8], "axis": "y", "angle": 45, "rescale": true },
"faces": { "south": { "uv": [3, 0, 1, 5], "texture": "#all" } } }
]
""";
private static final String PRESSURE_PLATE = """
[
{ "from": [1, 0, 1], "to": [15, 1, 15], "faces": {
"up": { "uv": [1, 1, 15, 15], "texture": "#all" },
"south": { "uv": [1, 15, 15, 16], "texture": "#all" } } }
]
""";
private static final Map<String, String> TEXTURES = Map.of("all", "block/test");
private static final int SIZE = 16;
private final BufferedImage texture = everyTexelItsOwnColour();
@Test
public void bodyShowsItsOwnPartOfTheTexture() throws Exception {
ModelFrontView front = draw(SEA_PICKLE);
assertEquals(litAsFront(0, 5), front.image().getRGB(6, 7));
assertEquals(litAsFront(3, 10), front.image().getRGB(9, 12));
}
@Test
public void topShowsAboveTheFrontOverThePlaneJustUnderIt() throws Exception {
ModelFrontView front = draw(SEA_PICKLE);
assertEquals(texture.getRGB(5, 2), front.image().getRGB(7, 5));
}
@Test
public void crossedPlaneIsMirroredWhereItsUvRunsBackwards() throws Exception {
ModelFrontView front = draw(SEA_PICKLE);
assertEquals(litAsFront(1, 1), front.image().getRGB(8, 4));
}
@Test
public void spotsNoBoxCoversStayClear() throws Exception {
ModelFrontView front = draw(SEA_PICKLE);
assertEquals(0, front.image().getRGB(2, 12) >>> 24);
assertEquals(0, front.image().getRGB(7, 2) >>> 24);
}
@Test
public void smallShapeLeavesMostOfItsSpotOpen() {
assertFalse(ModelFrontView.fillsMostOfItsSpotFromAbove(elements(SEA_PICKLE)));
}
@Test
public void flatShapeFillsMostOfItsSpot() {
assertTrue(ModelFrontView.fillsMostOfItsSpotFromAbove(elements(PRESSURE_PLATE)));
}
private int litAsFront(int textureX, int textureY) {
return 0xFF000000 | BlockPreviewColors.shade(texture.getRGB(textureX, textureY), CubeFace.SOUTH.shade());
}
private ModelFrontView draw(String elements) throws Exception {
ModelFrontView front = ModelFrontView.draw(elements(elements), TEXTURES, name -> texture);
assertNotNull(front);
return front;
}
private static JsonArray elements(String json) {
return JsonParser.parseString(json).getAsJsonArray();
}
private static BufferedImage everyTexelItsOwnColour() {
BufferedImage image = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
image.setRGB(x, y, 0xFF000000 | x << 16 | y << 8 | 0x40);
}
}
return image;
}
}