From fb4bddd163d8f0078a0b408ccc929d6744d2bc8c Mon Sep 17 00:00:00 2001 From: Anuken Date: Sun, 13 Sep 2026 20:23:30 -0400 Subject: [PATCH] Support for dynamically-generated rounded/angled corners in UI --- .../mindustry/graphics/CornerGenerator.java | 190 ++++++++++++++++++ .../graphics/OutlinedCornerDrawable.java | 169 ++++++++++++++++ core/src/mindustry/io/SaveVersion.java | 2 +- gradle.properties | 2 +- 4 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 core/src/mindustry/graphics/CornerGenerator.java create mode 100644 core/src/mindustry/graphics/OutlinedCornerDrawable.java diff --git a/core/src/mindustry/graphics/CornerGenerator.java b/core/src/mindustry/graphics/CornerGenerator.java new file mode 100644 index 0000000000..620fa38c83 --- /dev/null +++ b/core/src/mindustry/graphics/CornerGenerator.java @@ -0,0 +1,190 @@ +package mindustry.graphics; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.graphics.g2d.PixmapPacker.*; +import arc.math.*; +import arc.math.geom.*; +import arc.scene.style.*; +import arc.scene.ui.layout.*; +import mindustry.core.*; + +public class CornerGenerator{ + /** Sub-samples per axis, per pixel, used to antialias the diagonal edge of the sharp/beveled corners. 8 -> 64 samples/pixel. */ + private static final int supersample = 8; + + private CornerGenerator(){} + + /** + * Generates a NinePatchDrawable with antialiased angled corners. + * @param unscaledSize size of the corner in unscaled pixels; this is scaled by Scl.scl. + * */ + public static NinePatchDrawable generateAngled(int unscaledSize){ + int size = Math.max((int)Scl.scl(unscaledSize), 1); + TextureRegion[] tiles = renderCorner("corner-" + size, size, (x, y) -> coverage(x, y, size)); + return ninePatch(tiles); + } + + /** + * Generates an {@link OutlinedCornerDrawable}: an antialiased, beveled-corner outline in the same visual style as + * {@link #generateAngled(int)}, but drawing only the outline (stroke) of the shape rather than filling it. + * @param unscaledSize size of the corner in unscaled pixels; this is scaled by Scl.scl, same as {@link #generateAngled(int)}. + * @param unscaledStroke width of the outline, perpendicular to the line, in unscaled pixels; this is scaled by Scl.scl. + */ + public static OutlinedCornerDrawable generateAngledOutline(int unscaledSize, float unscaledStroke){ + int size = Math.max((int)Scl.scl(unscaledSize), 1); + float stroke = Scl.scl(unscaledStroke); + String name = "corner-outline-" + size + "-" + Float.floatToIntBits(stroke); + TextureRegion[] tiles = renderCorner(name, size, (x, y) -> outlineCoverage(x, y, stroke)); + return outlinedDrawable(tiles, size, stroke); + } + + /** + * Generates a NinePatchDrawable with antialiased rounded corners. + * @param unscaledSize size of the corner in unscaled pixels; this is scaled by Scl.scl, same as {@link #generateAngled(int)}. + */ + public static NinePatchDrawable generateRound(int unscaledSize){ + int size = Math.max((int)Scl.scl(unscaledSize), 1); + String name = "corner-round-" + size; + TextureRegion[] tiles = renderCorner(name, size, (x, y) -> roundCoverage(x, y, size)); + return ninePatch(tiles); + } + + /** + * Generates an outlined drawable with antialiased rounded corners. + *

+ * @param unscaledSize size of the corner in unscaled pixels; this is scaled by Scl.scl. + * @param unscaledStroke width of the outline, in unscaled pixels; this is scaled by Scl.scl. + */ + public static OutlinedCornerDrawable generateRoundOutline(int unscaledSize, float unscaledStroke){ + int size = Math.max((int)Scl.scl(unscaledSize), 1); + float stroke = Scl.scl(unscaledStroke); + String name = "corner-round-outline-" + size + "-" + Float.floatToIntBits(stroke); + TextureRegion[] tiles = renderCorner(name, size, (x, y) -> outlineRoundCoverage(x, y, size, stroke)); + return outlinedDrawable(tiles, size, stroke); + } + + /** + * Packs a {@code size} x {@code size} tile into the UI packer, filling it pixel-by-pixel via {@code fn}, and + * returns the four flipped corner regions derived from it. + * @return {topLeft, topRight, bottomLeft, bottomRight}, matching the flips used by both {@link #generateAngled(int)} + * and {@link #generateAngledOutline(int, float)}. + */ + private static TextureRegion[] renderCorner(String name, int size, CoverageFn fn){ + PixmapPacker packer = UI.packer; + Rect rect = packer.pack(name, size, size); + Page page = packer.getPage(name); + Pixmap pix = page.image; + + int baseX = (int)rect.x, baseY = (int)rect.y; + + //draw the antialiased shape directly into the packer page's pixmap + for(int y = 0; y < size; y++){ + for(int x = 0; x < size; x++){ + float coverage = fn.get(x, y); + pix.setRaw(baseX + x, baseY + y, Color.rgba8888(1f, 1f, 1f, coverage)); + } + } + + page.setDirty(true); + + Texture texture = page.texture; + + //top-right corner tile, exactly as generated + TextureRegion topRight = new TextureRegion(texture, baseX, baseY, size, size); + TextureRegion topLeft = new TextureRegion(topRight); + topLeft.flip(true, false); + TextureRegion bottomRight = new TextureRegion(topRight); + bottomRight.flip(false, true); + TextureRegion bottomLeft = new TextureRegion(topRight); + bottomLeft.flip(true, true); + + return new TextureRegion[]{topLeft, topRight, bottomLeft, bottomRight}; + } + + /** Builds a filled nine-patch drawable out of the four corner tiles returned by {@link #renderCorner}. */ + private static NinePatchDrawable ninePatch(TextureRegion[] corners){ + TextureRegion topLeft = corners[0], topRight = corners[1], bottomLeft = corners[2], bottomRight = corners[3]; + int size = topRight.height; + + //note: Core.atlas uses an array texture, and in this case it is guaranteed to be the same, assuming the packer is set up to target it + TextureRegion white = Core.atlas.white(); + + TextureRegion top = new TextureRegion(white); + top.height = size; + TextureRegion bottom = new TextureRegion(white); + bottom.height = size; + TextureRegion left = new TextureRegion(white); + left.width = size; + TextureRegion right = new TextureRegion(white); + right.width = size; + TextureRegion center = new TextureRegion(white); + + NinePatch patch = new NinePatch( + topLeft, top, topRight, + left, center, right, + bottomLeft, bottom, bottomRight + ); + + return new NinePatchDrawable(patch); + } + + /** Builds an {@link OutlinedCornerDrawable} out of the four corner tiles returned by {@link #renderCorner}. */ + private static OutlinedCornerDrawable outlinedDrawable(TextureRegion[] corners, int size, float stroke){ + TextureRegion topLeft = corners[0], topRight = corners[1], bottomLeft = corners[2], bottomRight = corners[3]; + + //note: Core.atlas uses an array texture, and in this case it is guaranteed to be the same, assuming the packer is set up to target it + return new OutlinedCornerDrawable(topLeft, topRight, bottomLeft, bottomRight, Core.atlas.white(), size, stroke); + } + + /** @return the antialiased coverage (0-1) of pixel (x, y) with an angled edge. */ + private static float coverage(int x, int y, int size){ + int inside = 0; + for(int sy = 0; sy < supersample; sy++){ + float fy = y + (sy + 0.5f) / supersample; + for(int sx = 0; sx < supersample; sx++){ + float fx = x + (sx + 0.5f) / supersample; + if(fy > fx) inside++; + } + } + return inside / (float)(supersample * supersample); + } + + /** @return the antialiased coverage (0-1) of pixel (x, y) with an angled outlined edge. */ + private static float outlineCoverage(int x, int y, float stroke){ + int inside = 0; + float band = stroke; + float extrude = stroke * ((float)Math.sqrt(2.0) - 1f); + for(int sy = 0; sy < supersample; sy++){ + float fy = y + (sy + 0.5f) / supersample; + for(int sx = 0; sx < supersample; sx++){ + float fx = x + (sx + 0.5f) / supersample; + float diff = fy - fx; + //on the filled side of the diagonal, extruded a bit toward the top-right, and within the band + if(diff >= -extrude && diff < band) inside++; + } + } + return inside / (float)(supersample * supersample); + } + + private static float cornerDistance(float fx, float fy, int size){ + float qy = Math.max(size - fy, 0f); + return (float)Math.sqrt(fx * fx + qy * qy) - size; + } + + private static float roundCoverage(int x, int y, int size){ + float d = cornerDistance(x + 0.5f, y + 0.5f, size); + return Mathf.clamp(0.5f - d); + } + + private static float outlineRoundCoverage(int x, int y, int size, float stroke){ + float d = cornerDistance(x + 0.5f, y + 0.5f, size); + return Mathf.clamp(0.5f - d) - Mathf.clamp(0.5f - d - stroke); + } + + /** (x, y) pixel coordinate -> coverage (0-1). */ + private interface CoverageFn{ + float get(int x, int y); + } +} \ No newline at end of file diff --git a/core/src/mindustry/graphics/OutlinedCornerDrawable.java b/core/src/mindustry/graphics/OutlinedCornerDrawable.java new file mode 100644 index 0000000000..e768702a08 --- /dev/null +++ b/core/src/mindustry/graphics/OutlinedCornerDrawable.java @@ -0,0 +1,169 @@ +package mindustry.graphics; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.scene.style.*; + +/** + * Drawable for an antialiased, beveled-corner outline, as generated by {@link CornerGenerator#generateAngledOutline(int, float)}. + *

+ * This looks similar to a {@link NinePatchDrawable} but it isn't a true nine-patch; the 4 corners are generated, the center is not drawn, and the 4 sides are drawn as lines. + */ +public class OutlinedCornerDrawable extends BaseDrawable implements TransformDrawable{ + private static final Color tmpColor = new Color(); + + //quad slots within the vertex array; there is deliberately no "center" slot, as it is never drawn + private static final int topLeft = 0, topRight = 1, bottomLeft = 2, bottomRight = 3, + top = 4, bottom = 5, left = 6, right = 7; + private static final int quads = 8; + + private final Color color = new Color(Color.white); + private final float[] vertices = new float[quads * SpriteBatch.spriteSize]; + private Texture texture; + + private float cornerSize, strokeWidth; + + /** + * @param topLeftRegion generated corner tile for the top-left corner (flipped horizontally from the base tile) + * @param topRightRegion generated corner tile for the top-right corner (the base tile, unflipped) + * @param bottomLeftRegion generated corner tile for the bottom-left corner (flipped both ways) + * @param bottomRightRegion generated corner tile for the bottom-right corner (flipped vertically) + * @param white a plain white region, used to draw the four straight sides; must share a texture with the corners + * @param cornerSize size of each (square) corner region, in draw-time pixels + * @param strokeWidth thickness of the outline, in draw-time pixels + */ + public OutlinedCornerDrawable(TextureRegion topLeftRegion, TextureRegion topRightRegion, TextureRegion bottomLeftRegion, TextureRegion bottomRightRegion, TextureRegion white, float cornerSize, float strokeWidth){ + + this.texture = white.texture; + this.cornerSize = cornerSize; + this.strokeWidth = strokeWidth; + + setUv(topLeft, topLeftRegion); + setUv(topRight, topRightRegion); + setUv(bottomLeft, bottomLeftRegion); + setUv(bottomRight, bottomRightRegion); + setUv(top, white); + setUv(bottom, white); + setUv(left, white); + setUv(right, white); + + setMinWidth(cornerSize * 2f); + setMinHeight(cornerSize * 2f); + setTopHeight(cornerSize); + setBottomHeight(cornerSize); + setLeftWidth(cornerSize); + setRightWidth(cornerSize); + } + + private void setUv(int quad, TextureRegion region){ + int idx = quad * SpriteBatch.spriteSize; + float u = region.u, v = region.v2, u2 = region.u2, v2 = region.v; + float depth = region.getDepth(); + float[] vs = vertices; + + vs[idx + 2] = u; vs[idx + 3] = v; vs[idx + 4] = depth; + vs[idx + 9] = u; vs[idx + 10] = v2; vs[idx + 11] = depth; + vs[idx + 16] = u2; vs[idx + 17] = v2; vs[idx + 18] = depth; + vs[idx + 23] = u2; vs[idx + 24] = v; vs[idx + 25] = depth; + } + + /** Sets the position and color of one of the eight quads. */ + private void setQuad(int quad, float x, float y, float w, float h, float c){ + int idx = quad * SpriteBatch.spriteSize; + float x2 = x + w, y2 = y + h; + float[] vs = vertices; + + vs[idx] = x; vs[idx + 1] = y; vs[idx + 5] = c; + vs[idx + 7] = x; vs[idx + 8] = y2; vs[idx + 12] = c; + vs[idx + 14] = x2; vs[idx + 15] = y2; vs[idx + 19] = c; + vs[idx + 21] = x2; vs[idx + 22] = y; vs[idx + 26] = c; + } + + private void prepareVertices(float x, float y, float width, float height){ + float c = tmpColor.set(color).mul(Draw.getColor()).toFloatBits(); + + float innerX = x + cornerSize, innerY = y + cornerSize; + float rightX = x + width - cornerSize, topY = y + height - cornerSize; + + //corners: full generated tiles, unscaled, sitting exactly in the four corners + setQuad(bottomLeft, x, y, cornerSize, cornerSize, c); + setQuad(bottomRight, rightX, y, cornerSize, cornerSize, c); + setQuad(topLeft, x, topY, cornerSize, cornerSize, c); + setQuad(topRight, rightX, topY, cornerSize, cornerSize, c); + + //sides: plain white, only as thick as the stroke, spanning the gap between corners - not the corner size + float sideWidth = Math.max(0f, width - cornerSize * 2f); + float sideHeight = Math.max(0f, height - cornerSize * 2f); + + setQuad(bottom, innerX, y, sideWidth, strokeWidth, c); + setQuad(top, innerX, y + height - strokeWidth, sideWidth, strokeWidth, c); + setQuad(left, x, innerY, strokeWidth, sideHeight, c); + setQuad(right, x + width - strokeWidth, innerY, strokeWidth, sideHeight, c); + } + + @Override + public void draw(float x, float y, float width, float height){ + prepareVertices(x, y, width, height); + Draw.vert(texture, vertices, 0, vertices.length); + } + + @Override + public void draw(float x, float y, float originX, float originY, float width, float height, float scaleX, float scaleY, float rotation){ + prepareVertices(x, y, width, height); + + float worldOriginX = x + originX, worldOriginY = y + originY; + float[] vs = vertices; + int n = vs.length; + + if(rotation != 0){ + float cos = Mathf.cosDeg(rotation), sin = Mathf.sinDeg(rotation); + for(int i = 0; i < n; i += SpriteBatch.vertexSize){ + float vx = (vs[i] - worldOriginX) * scaleX, vy = (vs[i + 1] - worldOriginY) * scaleY; + vs[i] = cos * vx - sin * vy + worldOriginX; + vs[i + 1] = sin * vx + cos * vy + worldOriginY; + } + }else if(scaleX != 1f || scaleY != 1f){ + for(int i = 0; i < n; i += SpriteBatch.vertexSize){ + vs[i] = (vs[i] - worldOriginX) * scaleX + worldOriginX; + vs[i + 1] = (vs[i + 1] - worldOriginY) * scaleY + worldOriginY; + } + } + + Draw.vert(texture, vs, 0, n); + } + + public float getCornerSize(){ + return cornerSize; + } + + public float getStrokeWidth(){ + return strokeWidth; + } + + public Color getColor(){ + return color; + } + + /** Copy given color. Blended with the batch color, then combined with the texture colors at draw time. */ + public void setColor(Color color){ + this.color.set(color); + } + + /** Creates a new drawable that renders the same as this drawable tinted the specified color. */ + public OutlinedCornerDrawable tint(Color tint){ + OutlinedCornerDrawable drawable = new OutlinedCornerDrawable(this); + drawable.color.set(tint); + return drawable; + } + + /** Copy constructor; shares the same texture/UV data (via {@link #vertices}) as the source drawable. */ + private OutlinedCornerDrawable(OutlinedCornerDrawable other){ + super(other); + this.texture = other.texture; + this.cornerSize = other.cornerSize; + this.strokeWidth = other.strokeWidth; + this.color.set(other.color); + System.arraycopy(other.vertices, 0, this.vertices, 0, other.vertices.length); + } +} \ No newline at end of file diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 805f6f78e3..735204190b 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -558,7 +558,7 @@ public abstract class SaveVersion extends SaveFileReader{ //the requiredPlanets filter needs this, since the rules aren't read yet if(headless){ try{ - Planet planet = content.planet(new JsonReader().parse(saveState.ruleString).getString("planet", Planets.serpulo.name)); + Planet planet = content.planet(Jval.read(saveState.ruleString).getString("planet", Planets.serpulo.name)); state.rules.planet = planet == null ? Planets.serpulo : planet; }catch(Exception ignored){} } diff --git a/gradle.properties b/gradle.properties index f08987180c..f40ab3f9d4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,4 +26,4 @@ org.gradle.caching=true org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 android.enableR8.fullMode=false -archash=b63432df12 +archash=c082cc548b