(64);
+ public String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
+ public String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
+ public int repetitions = 0; // 0: infinite loop, N: number of loops
+ private BufferedImage img = null; // Currently drawn frame
+ private int[] prevPx = null; // Previous frame's pixels
+ private final BitReader bits = new BitReader();
+ private final CodeTable codes = new CodeTable();
+ private Graphics2D g;
+
+ private final int[] decode(final GifFrame fr, final int[] activeColTbl) {
+ codes.init(fr, activeColTbl, bits);
+ bits.init(fr.data); // Incoming codes
+ final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode;
+ final int[] out = new int[wh]; // Target image pixel array
+ final int[][] tbl = codes.tbl; // Code table
+ int outPos = 0; // Next pixel position in the output image array
+ codes.clear(); // Init code table
+ bits.read(); // Skip leading clear code
+ int code = bits.read(); // Read first code
+ int[] pixels = tbl[code]; // Output pixel for first code
+ arraycopy(pixels, 0, out, outPos, pixels.length);
+ outPos += pixels.length;
+ try {
+ while (true) {
+ final int prevCode = code;
+ code = bits.read(); // Get next code in stream
+ if (code == clearCode) { // After a CLEAR table, there is
+ codes.clear(); // no previous code, we need to read
+ code = bits.read(); // a new one
+ pixels = tbl[code]; // Output pixels
+ arraycopy(pixels, 0, out, outPos, pixels.length);
+ outPos += pixels.length;
+ continue; // Back to the loop with a valid previous code
+ } else if (code == endCode) {
+ break;
+ }
+ final int[] prevVals = tbl[prevCode];
+ final int[] prevValsAndK = new int[prevVals.length + 1];
+ arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length);
+ if (code < codes.nextCode) { // Code table contains code
+ pixels = tbl[code]; // Output pixels
+ arraycopy(pixels, 0, out, outPos, pixels.length);
+ outPos += pixels.length;
+ prevValsAndK[prevVals.length] = tbl[code][0]; // K
+ } else {
+ prevValsAndK[prevVals.length] = prevVals[0]; // K
+ arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length);
+ outPos += prevValsAndK.length;
+ }
+ codes.add(prevValsAndK); // Previous indices + K
+ }
+ } catch (final ArrayIndexOutOfBoundsException e) {
+ }
+ return out;
+ }
+
+ private final int[] deinterlace(final int[] src, final GifFrame fr) {
+ final int w = fr.w, h = fr.h, wh = fr.wh;
+ final int[] dest = new int[src.length];
+ // Interlaced images are organized in 4 sets of pixel lines
+ final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0)
+ final int set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0)
+ final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0)
+ // Sets' start indices in source array
+ final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y;
+ // Line skips in destination array
+ final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1;
+ // Group 1 contains every 8th line starting from 0
+ int from = 0, to = 0;
+ for (; from < set2; from += w, to += w8) {
+ arraycopy(src, from, dest, to, w);
+ } // Group 2 contains every 8th line starting from 4
+ for (to = w4; from < set3; from += w, to += w8) {
+ arraycopy(src, from, dest, to, w);
+ } // Group 3 contains every 4th line starting from 2
+ for (to = w2; from < set4; from += w, to += w4) {
+ arraycopy(src, from, dest, to, w);
+ } // Group 4 contains every 2nd line starting from 1 (biggest group)
+ for (to = w; from < wh; from += w, to += w2) {
+ arraycopy(src, from, dest, to, w);
+ }
+ return dest; // All pixel lines have now been rearranged
+ }
+
+ private final void drawFrame(final GifFrame fr) {
+ // Determine the color table that will be active for this frame
+ final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl;
+ // Get pixels from data stream
+ int[] pixels = decode(fr, activeColTbl);
+ if (fr.interlaceFlag) {
+ pixels = deinterlace(pixels, fr); // Rearrange pixel lines
+ }
+ // Create image of type 2=ARGB for frame area
+ final BufferedImage frame = new BufferedImage(fr.w, fr.h, 2);
+ arraycopy(pixels, 0, ((DataBufferInt) frame.getRaster().getDataBuffer()).getData(), 0, fr.wh);
+ // Draw frame area on top of working image
+ g.drawImage(frame, fr.x, fr.y, null);
+
+ // Visualize frame boundaries during testing
+ // if (DEBUG_MODE) {
+ // if (prev != null) {
+ // g.setColor(Color.RED); // Previous frame color
+ // g.drawRect(prev.x, prev.y, prev.w - 1, prev.h - 1);
+ // }
+ // g.setColor(Color.GREEN); // New frame color
+ // g.drawRect(fr.x, fr.y, fr.w - 1, fr.h - 1);
+ // }
+
+ // Keep one copy as "previous frame" in case we need to restore it
+ prevPx = new int[wh];
+ arraycopy(((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, prevPx, 0, wh);
+
+ // Create another copy for the end user to not expose internal state
+ fr.img = new BufferedImage(w, h, 2); // 2 = ARGB
+ arraycopy(prevPx, 0, ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData(), 0, wh);
+
+ // Handle disposal of current frame
+ if (fr.disposalMethod == 2) {
+ // Restore to background color (clear frame area only)
+ g.clearRect(fr.x, fr.y, fr.w, fr.h);
+ } else if (fr.disposalMethod == 3 && prevPx != null) {
+ // Restore previous frame
+ arraycopy(prevPx, 0, ((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, wh);
+ }
+ }
+
+ /**
+ * Returns the background color of the first frame in this GIF image. If the frame has a local
+ * color table, the returned color will be from that table. If not, the color will be from the
+ * global color table. Returns 0 if there is neither a local nor a global color table.
+ *
+ * Index of the current frame, 0 to N-1
+ *
+ * @return 32 bit ARGB color in the form 0xAARRGGBB
+ */
+ public final int getBackgroundColor() {
+ final GifFrame frame = frames.get(0);
+ if (frame.hasLocColTbl) {
+ return frame.localColTbl[bgColIndex];
+ } else if (hasGlobColTbl) {
+ return globalColTbl[bgColIndex];
+ }
+ return 0;
+ }
+
+ /**
+ * If not 0, the delay specifies how many hundredths (1/100) of a second to wait before
+ * displaying the frame after the current frame.
+ *
+ * @param index Index of the current frame, 0 to N-1
+ * @return Delay as number of hundredths (1/100) of a second
+ */
+ public final int getDelay(final int index) {
+ return frames.get(index).delay;
+ }
+
+ /**
+ * @param index Index of the frame to return as image, starting from 0. For incremental calls
+ * such as [0, 1, 2, ...] the method's run time is O(1) as only one frame is drawn per call.
+ * For random access calls such as [7, 12, ...] the run time is O(N+1) with N being the
+ * number of previous frames that need to be drawn before N+1 can be drawn on top. Once a
+ * frame has been drawn it is being cached and the run time is more or less O(0) to retrieve
+ * it from the list.
+ * @return A BufferedImage for the specified frame.
+ */
+ public final BufferedImage getFrame(final int index) {
+ if (img == null) { // Init
+ img = new BufferedImage(w, h, 2); // 2 = ARGB
+ g = img.createGraphics();
+ g.setBackground(new Color(0, true)); // Transparent color
+ }
+ GifFrame fr = frames.get(index);
+ if (fr.img == null) {
+ // Draw all frames until and including the requested frame
+ for (int i = 0; i <= index; i++) {
+ fr = frames.get(i);
+ if (fr.img == null) {
+ drawFrame(fr);
+ }
+ }
+ }
+ return fr.img;
+ }
+
+ /** @return The number of frames contained in this GIF image */
+ public final int getFrameCount() {
+ return frames.size();
+ }
+
+ /** @return The height of the GIF image */
+ public final int getHeight() {
+ return h;
+ }
+
+ /** @return The width of the GIF image */
+ public final int getWidth() {
+ return w;
+ }
+ }
+
+ static final boolean DEBUG_MODE = false;
+
+ /**
+ * @param in Raw image data as a byte[] array
+ * @return A GifImage object exposing the properties of the GIF image.
+ * @throws IOException If the image violates the GIF specification or is truncated.
+ */
+ public static final GifImage read(final byte[] in) throws IOException {
+ final GifDecoder decoder = new GifDecoder();
+ final GifImage img = decoder.new GifImage();
+ GifFrame frame = null; // Currently open frame
+ int pos = readHeader(in, img); // Read header, get next byte position
+ pos = readLogicalScreenDescriptor(img, in, pos);
+ if (img.hasGlobColTbl) {
+ img.globalColTbl = new int[img.sizeOfGlobColTbl];
+ pos = readColTbl(in, img.globalColTbl, pos);
+ }
+ while (pos < in.length) {
+ final int block = in[pos] & 0xFF;
+ switch (block) {
+ case 0x21: // Extension introducer
+ if (pos + 1 >= in.length) {
+ throw new IOException("Unexpected end of file.");
+ }
+ switch (in[pos + 1] & 0xFF) {
+ case 0xFE: // Comment extension
+ pos = readTextExtension(in, pos);
+ break;
+ case 0xFF: // Application extension
+ pos = readAppExt(img, in, pos);
+ break;
+ case 0x01: // Plain text extension
+ frame = null; // End of current frame
+ pos = readTextExtension(in, pos);
+ break;
+ case 0xF9: // Graphic control extension
+ if (frame == null) {
+ frame = decoder.new GifFrame();
+ img.frames.add(frame);
+ }
+ pos = readGraphicControlExt(frame, in, pos);
+ break;
+ default:
+ throw new IOException("Unknown extension at " + pos);
+ }
+ break;
+ case 0x2C: // Image descriptor
+ if (frame == null) {
+ frame = decoder.new GifFrame();
+ img.frames.add(frame);
+ }
+ pos = readImgDescr(frame, in, pos);
+ if (frame.hasLocColTbl) {
+ frame.localColTbl = new int[frame.sizeOfLocColTbl];
+ pos = readColTbl(in, frame.localColTbl, pos);
+ }
+ pos = readImgData(frame, in, pos);
+ frame = null; // End of current frame
+ break;
+ case 0x3B: // GIF Trailer
+ return img; // Found trailer, finished reading.
+ default:
+ // Unknown block. The image is corrupted. Strategies: a) Skip
+ // and wait for a valid block. Experience: It'll get worse. b)
+ // Throw exception. c) Return gracefully if we are almost done
+ // processing. The frames we have so far should be error-free.
+ final double progress = 1.0 * pos / in.length;
+ if (progress < 0.9) {
+ throw new IOException("Unknown block at: " + pos);
+ }
+ pos = in.length; // Exit loop
+ }
+ }
+ return img;
+ }
+
+ /**
+ * @param is Image data as input stream. This method will read from the input stream's current
+ * position. It will not reset the position before reading and won't reset or close the stream
+ * afterwards. Call these methods before and after calling this method as needed.
+ * @return A GifImage object exposing the properties of the GIF image.
+ * @throws IOException If an I/O error occurs, the image violates the GIF specification or the GIF
+ * is truncated.
+ */
+ public static final GifImage read(final InputStream is) throws IOException {
+ final byte[] data = new byte[is.available()];
+ is.read(data, 0, data.length);
+ return read(data);
+ }
+
+ /**
+ * Empty application extension object
+ *
+ * @param in Raw data
+ * @param i Index of the first byte of the application extension
+ * @return Index of the first byte after this extension
+ */
+ static final int readAppExt(final GifImage img, final byte[] in, int i) {
+ img.appId = new String(in, i + 3, 8); // should be "NETSCAPE"
+ img.appAuthCode = new String(in, i + 11, 3); // should be "2.0"
+ i += 14; // Go to sub-block size, it's value should be 3
+ final int subBlockSize = in[i] & 0xFF;
+ // The only app extension widely used is NETSCAPE, it's got 3 data bytes
+ if (subBlockSize == 3) {
+ // in[i+1] should have value 01, in[i+5] should be block terminator
+ img.repetitions = in[i + 2] & 0xFF | in[i + 3] & 0xFF << 8; // Short
+ return i + 5;
+ } // Skip unknown application extensions
+ while ((in[i] & 0xFF) != 0) { // While sub-block size != 0
+ i += (in[i] & 0xFF) + 1; // Skip to next sub-block
+ }
+ return i + 1;
+ }
+
+ /**
+ * @param in Raw data
+ * @param colors Pre-initialized target array to store ARGB colors
+ * @param i Index of the color table's first byte
+ * @return Index of the first byte after the color table
+ */
+ static final int readColTbl(final byte[] in, final int[] colors, int i) {
+ final int numColors = colors.length;
+ for (int c = 0; c < numColors; c++) {
+ final int a = 0xFF; // Alpha 255 (opaque)
+ final int r = in[i++] & 0xFF; // 1st byte is red
+ final int g = in[i++] & 0xFF; // 2nd byte is green
+ final int b = in[i++] & 0xFF; // 3rd byte is blue
+ colors[c] = ((a << 8 | r) << 8 | g) << 8 | b;
+ }
+ return i;
+ }
+
+ /**
+ * Graphic control extension object
+ *
+ * @param in Raw data
+ * @param i Index of the extension introducer
+ * @return Index of the first byte after this block
+ */
+ static final int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) {
+ fr.disposalMethod = (in[i + 3] & 0b00011100) >>> 2; // Bits 4-2
+ fr.transpColFlag = (in[i + 3] & 1) == 1; // Bit 0
+ fr.delay = in[i + 4] & 0xFF | (in[i + 5] & 0xFF) << 8; // 16 bit LSB
+ fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6
+ return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00
+ }
+
+ /**
+ * @param in Raw data
+ * @param img The GifImage object that is currently read
+ * @return Index of the first byte after this block
+ * @throws IOException If the GIF header/trailer is missing, incomplete or unknown
+ */
+ static final int readHeader(final byte[] in, final GifImage img) throws IOException {
+ if (in.length < 6) { // Check first 6 bytes
+ throw new IOException("Image is truncated.");
+ }
+ img.header = new String(in, 0, 6);
+ if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) {
+ throw new IOException("Invalid GIF header.");
+ }
+ return 6;
+ }
+
+ /**
+ * @param fr The GIF frame to whom this image descriptor belongs
+ * @param in Raw data
+ * @param i Index of the first byte of this block, i.e. the minCodeSize
+ * @return
+ */
+ static final int readImgData(final GifFrame fr, final byte[] in, int i) {
+ final int fileSize = in.length;
+ final int minCodeSize = in[i++] & 0xFF; // Read code size, go to block
+ final int clearCode = 1 << minCodeSize; // CLEAR = 2^minCodeSize
+ fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI
+ fr.clearCode = clearCode;
+ fr.endOfInfoCode = clearCode + 1;
+ final int imgDataSize = readImgDataSize(in, i);
+ final byte[] imgData = new byte[imgDataSize + 2];
+ int imgDataPos = 0;
+ int subBlockSize = in[i] & 0xFF;
+ while (subBlockSize > 0) { // While block has data
+ try { // Next line may throw exception if sub-block size is fake
+ final int nextSubBlockSizePos = i + subBlockSize + 1;
+ final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
+ arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
+ imgDataPos += subBlockSize; // Move output data position
+ i = nextSubBlockSizePos; // Move to next sub-block size
+ subBlockSize = nextSubBlockSize;
+ } catch (final Exception e) {
+ // Sub-block exceeds file end, only use remaining bytes
+ subBlockSize = fileSize - i - 1; // Remaining bytes
+ arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
+ imgDataPos += subBlockSize; // Move output data position
+ i += subBlockSize + 1; // Move to next sub-block size
+ break;
+ }
+ }
+ fr.data = imgData; // Holds LZW encoded data
+ i++; // Skip last sub-block size, should be 0
+ return i;
+ }
+
+ static final int readImgDataSize(final byte[] in, int i) {
+ final int fileSize = in.length;
+ int imgDataPos = 0;
+ int subBlockSize = in[i] & 0xFF;
+ while (subBlockSize > 0) { // While block has data
+ try { // Next line may throw exception if sub-block size is fake
+ final int nextSubBlockSizePos = i + subBlockSize + 1;
+ final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
+ imgDataPos += subBlockSize; // Move output data position
+ i = nextSubBlockSizePos; // Move to next sub-block size
+ subBlockSize = nextSubBlockSize;
+ } catch (final Exception e) {
+ // Sub-block exceeds file end, only use remaining bytes
+ subBlockSize = fileSize - i - 1; // Remaining bytes
+ imgDataPos += subBlockSize; // Move output data position
+ break;
+ }
+ }
+ return imgDataPos;
+ }
+
+ /**
+ * @param fr The GIF frame to whom this image descriptor belongs
+ * @param in Raw data
+ * @param i Index of the image separator, i.e. the first block byte
+ * @return Index of the first byte after this block
+ */
+ static final int readImgDescr(final GifFrame fr, final byte[] in, int i) {
+ fr.x = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 1-2: left
+ fr.y = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 3-4: top
+ fr.w = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 5-6: width
+ fr.h = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 7-8: height
+ fr.wh = fr.w * fr.h;
+ final byte b = in[++i]; // Byte 9 is a packed byte
+ fr.hasLocColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7
+ fr.interlaceFlag = (b & 0b01000000) >>> 6 == 1; // Bit 6
+ fr.sortFlag = (b & 0b00100000) >>> 5 == 1; // Bit 5
+ final int colTblSizePower = (b & 7) + 1; // Bits 2-0
+ fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec
+ return ++i;
+ }
+
+ /**
+ * @param img
+ * @param i Start index of this block.
+ * @return Index of the first byte after this block.
+ */
+ static final int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) {
+ img.w = in[i] & 0xFF | (in[i + 1] & 0xFF) << 8; // 16 bit, LSB 1st
+ img.h = in[i + 2] & 0xFF | (in[i + 3] & 0xFF) << 8; // 16 bit
+ img.wh = img.w * img.h;
+ final byte b = in[i + 4]; // Byte 4 is a packed byte
+ img.hasGlobColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7
+ final int colResPower = ((b & 0b01110000) >>> 4) + 1; // Bits 6-4
+ img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec
+ img.sortFlag = (b & 0b00001000) >>> 3 == 1; // Bit 3
+ final int globColTblSizePower = (b & 7) + 1; // Bits 0-2
+ img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec
+ img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte
+ img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte
+ return i + 7;
+ }
+
+ /**
+ * @param in Raw data
+ * @param pos Index of the extension introducer
+ * @return Index of the first byte after this block
+ */
+ static final int readTextExtension(final byte[] in, final int pos) {
+ int i = pos + 2; // Skip extension introducer and label
+ int subBlockSize = in[i++] & 0xFF;
+ while (subBlockSize != 0 && i < in.length) {
+ i += subBlockSize;
+ subBlockSize = in[i++] & 0xFF;
+ }
+ return i;
+ }
+
+ public Animation getAnimation(Animation.PlayMode playMode) throws IOException {
+ int nrFrames = getFrameCount();
+ Pixmap frame = getFrame(0);
+ int width = frame.getWidth();
+ int height = frame.getHeight();
+ int vzones = (int) Math.sqrt((double) nrFrames);
+ int hzones = vzones;
+
+ while (vzones * hzones < nrFrames) vzones++;
+
+ int v, h;
+
+ Pixmap target = new Pixmap(width * hzones, height * vzones, Pixmap.Format.RGBA8888);
+
+ for (h = 0; h < hzones; h++) {
+ for (v = 0; v < vzones; v++) {
+ int frameID = v + h * vzones;
+ if (frameID < nrFrames) {
+ frame = getFrame(frameID);
+ target.drawPixmap(frame, h * width, v * height);
+ }
+ }
+ }
+
+ Texture texture = new Texture(target);
+ Array texReg = new Array();
+
+ for (h = 0; h < hzones; h++) {
+ for (v = 0; v < vzones; v++) {
+ int frameID = v + h * vzones;
+ if (frameID < nrFrames) {
+ TextureRegion tr = new TextureRegion(texture, h * width, v * height, width, height);
+ texReg.add(tr);
+ }
+ }
+ }
+ float frameDuration = (float) getDelay(0);
+ frameDuration /= 100; // convert milliseconds into seconds
+ Animation result = new Animation(frameDuration, texReg, playMode);
+
+ return result;
+ }
+
+ private int getDelay(int i) {
+ return image.getDelay(0);
+ }
+
+ private Pixmap getFrame(int i) throws IOException {
+ var bytes = ImageUtil.imageToBytes(image.getFrame(i), "png");
+ // without imageutil there seem to be some issues with tranparency for some images.
+ // (black background instead of tranparent)
+ // var bytes = AssetManager.getAsset(key).getImage();
+ return new Pixmap(bytes, 0, bytes.length);
+ }
+
+ private int getFrameCount() {
+ return image.getFrameCount();
+ }
+
+ public static Animation loadGIFAnimation(
+ Animation.PlayMode playMode, InputStream is) {
+ try {
+ var dec = new GifDecoder(is);
+ return dec.getAnimation(playMode);
+
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/net/rptools/lib/gdx/Joiner.java b/src/main/java/net/rptools/lib/gdx/Joiner.java
new file mode 100644
index 0000000000..3bda9123f0
--- /dev/null
+++ b/src/main/java/net/rptools/lib/gdx/Joiner.java
@@ -0,0 +1,138 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.lib.gdx;
+
+import com.badlogic.gdx.math.Vector;
+import com.badlogic.gdx.math.Vector2;
+import space.earlygrey.shapedrawer.ShapeUtils;
+
+// taken from shapedrawer because its protected there
+public class Joiner {
+
+ static final Vector2 AB = new Vector2(), BC = new Vector2(), v = new Vector2();
+
+ // All methods here set D and E based on A,B,C.
+ // D is always on left E is on right, relative to AB.
+ // Treat straight line as special case as in this case mitres have undefined length.
+ // "Inside point" refers to whichever of D or E is on the smaller angle side, vice versa for
+ // "outside point".
+
+ // see
+ // https://math.stackexchange.com/questions/1849784/calculate-miter-points-of-stroked-vectors-in-cartesian-plane
+ public static float preparePointyJoin(
+ Vector2 A, Vector2 B, Vector2 C, Vector2 D, Vector2 E, float halfLineWidth) {
+ AB.set(B).sub(A);
+ BC.set(C).sub(B);
+ float angle = ShapeUtils.angleRad(AB, BC);
+ if (ShapeUtils.epsilonEquals(angle, 0) || ShapeUtils.epsilonEquals(angle, ShapeUtils.PI2)) {
+ prepareStraightJoin(B, D, E, halfLineWidth);
+ return angle;
+ }
+ float len = (float) (halfLineWidth / Math.sin(angle));
+ boolean bendsLeft = angle < 0;
+ AB.setLength(len);
+ BC.setLength(len);
+ Vector insidePoint = bendsLeft ? D : E;
+ Vector outsidePoint = bendsLeft ? E : D;
+ insidePoint.set(B).sub(AB).add(BC);
+ outsidePoint.set(B).add(AB).sub(BC);
+ return angle;
+ }
+
+ public static boolean prepareSmoothJoin(
+ Vector2 A,
+ Vector2 B,
+ Vector2 C,
+ Vector2 D,
+ Vector2 E,
+ float halfLineWidth,
+ boolean startOfEdge) {
+ AB.set(B).sub(A);
+ BC.set(C).sub(B);
+ float angle = ShapeUtils.angleRad(AB, BC);
+ if (ShapeUtils.epsilonEquals(angle, 0) || ShapeUtils.epsilonEquals(angle, ShapeUtils.PI2)) {
+ prepareStraightJoin(B, D, E, halfLineWidth);
+ return true;
+ }
+ float len = (float) (halfLineWidth / Math.sin(angle));
+ AB.setLength(len);
+ BC.setLength(len);
+ boolean bendsLeft = angle < 0;
+ Vector insidePoint = bendsLeft ? D : E;
+ Vector outsidePoint = bendsLeft ? E : D;
+ insidePoint.set(B).sub(AB).add(BC);
+ // edgeDirection points towards the relevant edge - is this being calculated for the start of BC
+ // or the end of AB?
+ Vector2 edgeDirection = startOfEdge ? BC : AB;
+ // rotate edgeDirection PI/2 towards outsidePoint
+ if (bendsLeft) {
+ v.set(edgeDirection.y, -edgeDirection.x); // rotate PI/2 to the right (clockwise)
+ } else {
+ v.set(-edgeDirection.y, edgeDirection.x); // rotate PI/2 to the left (anticlockwise)
+ }
+ v.setLength(halfLineWidth);
+ outsidePoint.set(B).add(v);
+ return bendsLeft;
+ }
+
+ public static void prepareStraightJoin(Vector2 B, Vector2 D, Vector2 E, float halfLineWidth) {
+ AB.setLength(halfLineWidth);
+ D.set(-AB.y, AB.x).add(B);
+ E.set(AB.y, -AB.x).add(B);
+ }
+
+ public static Vector2 prepareFlatEndpoint(
+ float pathPointX,
+ float pathPointY,
+ float endPointX,
+ float endPointY,
+ Vector2 D,
+ Vector2 E,
+ float halfLineWidth) {
+ v.set(endPointX, endPointY).sub(pathPointX, pathPointY).setLength(halfLineWidth);
+ D.set(v.y, -v.x).add(endPointX, endPointY);
+ E.set(-v.y, v.x).add(endPointX, endPointY);
+ return v;
+ }
+
+ public static void prepareFlatEndpoint(
+ Vector2 pathPoint, Vector2 endPoint, Vector2 D, Vector2 E, float halfLineWidth) {
+ prepareFlatEndpoint(pathPoint.x, pathPoint.y, endPoint.x, endPoint.y, D, E, halfLineWidth);
+ }
+
+ public static void prepareSquareEndpoint(
+ float pathPointX,
+ float pathPointY,
+ float endPointX,
+ float endPointY,
+ Vector2 D,
+ Vector2 E,
+ float halfLineWidth) {
+ v.set(endPointX, endPointY).sub(pathPointX, pathPointY).setLength(halfLineWidth);
+ D.set(v.y, -v.x).add(endPointX, endPointY).add(v);
+ E.set(-v.y, v.x).add(endPointX, endPointY).add(v);
+ }
+
+ public static void prepareSquareEndpoint(
+ Vector2 pathPoint, Vector2 endPoint, Vector2 D, Vector2 E, float halfLineWidth) {
+ prepareSquareEndpoint(pathPoint.x, pathPoint.y, endPoint.x, endPoint.y, D, E, halfLineWidth);
+ }
+
+ public static void prepareRadialEndpoint(Vector2 A, Vector2 D, Vector2 E, float halfLineWidth) {
+ v.set(A).setLength(halfLineWidth);
+ D.set(A).sub(v);
+ E.set(A).add(v);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/swing/ImageBorder.java b/src/main/java/net/rptools/maptool/client/swing/ImageBorder.java
index 966b0408b1..b3d609d3ba 100644
--- a/src/main/java/net/rptools/maptool/client/swing/ImageBorder.java
+++ b/src/main/java/net/rptools/maptool/client/swing/ImageBorder.java
@@ -41,9 +41,11 @@ public class ImageBorder implements Border {
private int bottomMargin;
private int leftMargin;
private int rightMargin;
+ private String imagePath;
public ImageBorder(String imagePath) {
try {
+ this.imagePath = imagePath;
topRight = ImageUtil.getCompatibleImage(imagePath + "/tr.png");
top = ImageUtil.getCompatibleImage(imagePath + "/top.png");
topLeft = ImageUtil.getCompatibleImage(imagePath + "/tl.png");
@@ -62,6 +64,10 @@ public ImageBorder(String imagePath) {
}
}
+ public String getImagePath() {
+ return imagePath;
+ }
+
public int getTopMargin() {
return topMargin;
}
diff --git a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java
index 2d63214805..c6bd050228 100644
--- a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java
+++ b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java
@@ -14,10 +14,13 @@
*/
package net.rptools.maptool.client.ui;
+import com.badlogic.gdx.backends.jogamp.JoglAwtApplicationConfiguration;
+import com.badlogic.gdx.backends.jogamp.JoglSwingCanvas;
import com.google.common.eventbus.Subscribe;
import com.jidesoft.docking.DefaultDockableHolder;
import com.jidesoft.docking.DockableFrame;
import com.jidesoft.docking.DockingManager;
+import com.jogamp.opengl.awt.GLJPanel;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
@@ -94,6 +97,7 @@
import net.rptools.maptool.client.ui.zone.PointerOverlay;
import net.rptools.maptool.client.ui.zone.PointerToolOverlay;
import net.rptools.maptool.client.ui.zone.ZoneMiniMapPanel;
+import net.rptools.maptool.client.ui.zone.gdx.GdxRenderer;
import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer;
import net.rptools.maptool.events.MapToolEventBus;
import net.rptools.maptool.language.I18N;
@@ -157,6 +161,10 @@ public class MapToolFrame extends DefaultDockableHolder implements WindowListene
/** Contains the zoneRenderer, as well as all overlays. */
private final JPanel zoneRendererPanel;
+ private GLJPanel gdxPanel;
+
+ private JPanel currentRenderPanel;
+
/** Contains the overlays that should be displayed in front of everything else. */
private final PointerToolOverlay pointerToolOverlay;
@@ -411,8 +419,12 @@ public MapToolFrame(JMenuBar menuBar) {
zoneRendererPanel = new JPanel(new PositionalLayout(5));
zoneRendererPanel.setBackground(Color.black);
+ currentRenderPanel = zoneRendererPanel;
+ initGdx();
+
zoneRendererPanel.add(getChatTypingPanel(), PositionalLayout.Position.NW);
zoneRendererPanel.add(getChatActionLabel(), PositionalLayout.Position.SW);
+ zoneRendererPanel.add(gdxPanel, PositionalLayout.Position.CENTER);
commandPanel = new CommandPanel();
@@ -464,6 +476,36 @@ public MapToolFrame(JMenuBar menuBar) {
setChatTypingLabelColor(AppPreferences.chatNotificationColor.get());
}
+ private void initGdx() {
+ var config = new JoglAwtApplicationConfiguration();
+ // config.foregroundFPS = 300;
+ // config.backgroundFPS = 10;
+ // config.title = "maptool";
+ // config.width = 640;
+ // config.height = 480;
+ // config.samples = 1;
+ // var config = new LwjglApplicationConfiguration();
+ config.foregroundFPS = 10000;
+ config.vSyncEnabled = false;
+
+ var joglSwingCanvas = new JoglSwingCanvas(GdxRenderer.getInstance(), config);
+ // var joglSwingCanvas = new LwjglAWTCanvas(GdxRenderer.getInstance(), config);
+
+ gdxPanel = joglSwingCanvas.getGLCanvas();
+ gdxPanel.setVisible(false);
+ // gdxPanel.setLayout(new PositionalLayout(5));
+ }
+
+ public void switchRenderers() {
+ var isVisible = gdxPanel.isVisible();
+ gdxPanel.setVisible(!isVisible);
+ // currentRenderer.setVisible(isVisible);
+ }
+
+ public GLJPanel getGdxPanel() {
+ return gdxPanel;
+ }
+
public ChatNotificationTimers getChatNotificationTimers() {
return chatTyperTimers;
}
@@ -1042,10 +1084,10 @@ public void showControlPanel(JPanel... panels) {
}
layoutPanel.setSize(layoutPanel.getPreferredSize());
- zoneRendererPanel.add(layoutPanel, PositionalLayout.Position.NE);
- zoneRendererPanel.setComponentZOrder(layoutPanel, 0);
- zoneRendererPanel.revalidate();
- zoneRendererPanel.repaint();
+ currentRenderPanel.add(layoutPanel, PositionalLayout.Position.NE);
+ currentRenderPanel.setComponentZOrder(layoutPanel, 0);
+ currentRenderPanel.revalidate();
+ currentRenderPanel.repaint();
visibleControlPanel = layoutPanel;
}
@@ -1100,8 +1142,8 @@ public CoordinateStatusBar getCoordinateStatusBar() {
*/
public void removeControlPanel() {
if (visibleControlPanel != null) {
- if (zoneRendererPanel != null) {
- zoneRendererPanel.remove(visibleControlPanel);
+ if (currentRenderPanel != null) {
+ currentRenderPanel.remove(visibleControlPanel);
}
visibleControlPanel = null;
refresh();
@@ -1683,7 +1725,7 @@ public void setCurrentZoneRenderer(ZoneRenderer renderer) {
}
if (renderer != null) {
zoneRendererPanel.add(
- renderer, PositionalLayout.Position.CENTER, zoneRendererPanel.getComponentCount() - 1);
+ renderer, PositionalLayout.Position.CENTER, zoneRendererPanel.getComponentCount() - 2);
zoneRendererPanel.doLayout();
}
currentRenderer = renderer;
@@ -1909,6 +1951,7 @@ public void showFullScreenTools() {
zoneRendererPanel.setComponentZOrder(initiativePanel, 0);
zoneRendererPanel.revalidate();
+ zoneRendererPanel.doLayout();
zoneRendererPanel.repaint();
fullScreenToolsShown = true;
diff --git a/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java b/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java
index 76d6874b42..3f0da7da00 100644
--- a/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java
+++ b/src/main/java/net/rptools/maptool/client/ui/ToolbarPanel.java
@@ -166,6 +166,8 @@ public ToolbarPanel(Toolbox tbox) {
tokenSelectionButtonAll.setSelected(true);
// Jamz: End panel
+ add(createGdxButton(Icons.TOOLBAR_LIBGDX));
+
// the "Select Map" button
mapselect = createZoneSelectionButton();
add(mapselect);
@@ -538,6 +540,17 @@ private JToggleButton createMuteButton(
return button;
}
+ private JToggleButton createGdxButton(final Icons icon) {
+ final JToggleButton button = new JToggleButton();
+ button.addActionListener(
+ e -> {
+ MapTool.getFrame().switchRenderers();
+ });
+
+ button.setIcon(RessourceManager.getBigIcon(icon));
+ return button;
+ }
+
private JToggleButton createTokenSelectionButton(
final Icons icon, final Icons offIcon, String tooltip, TokenSelection tokenSelection) {
final JToggleButton button = new JToggleButton();
diff --git a/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java b/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java
index d8434e052c..3dd8099359 100644
--- a/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java
+++ b/src/main/java/net/rptools/maptool/client/ui/theme/Icons.java
@@ -132,6 +132,7 @@ public enum Icons {
TOOLBAR_FOG_ON,
TOOLBAR_HIDE_OFF,
TOOLBAR_HIDE_ON,
+ TOOLBAR_LIBGDX,
TOOLBAR_POINTERTOOL_AI_OFF,
TOOLBAR_POINTERTOOL_AI_ON,
TOOLBAR_POINTERTOOL_MEASURE,
diff --git a/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java b/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java
index 35c7873688..a8b75d7c30 100644
--- a/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java
+++ b/src/main/java/net/rptools/maptool/client/ui/theme/RessourceManager.java
@@ -167,6 +167,7 @@ public class RessourceManager {
put(Icons.TOOLBAR_FOG_ON, IMAGE_DIR + "tool/fog-blue.png");
put(Icons.TOOLBAR_HIDE_OFF, IMAGE_DIR + "tool/upArrow.png");
put(Icons.TOOLBAR_HIDE_ON, IMAGE_DIR + "tool/downArrow.png");
+ put(Icons.TOOLBAR_LIBGDX, IMAGE_DIR + "libgdx.png");
put(Icons.TOOLBAR_POINTERTOOL_AI_OFF, IMAGE_DIR + "tool/ai-blue-off.png");
put(Icons.TOOLBAR_POINTERTOOL_AI_ON, IMAGE_DIR + "tool/ai-blue-green.png");
put(Icons.TOOLBAR_POINTERTOOL_MEASURE, IMAGE_DIR + "tool/ruler-blue.png");
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/CornerImageTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/CornerImageTokenOverlay.java
index 0e350a0193..63be4ea95e 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/CornerImageTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/CornerImageTokenOverlay.java
@@ -68,7 +68,7 @@ public Object clone() {
* @see ImageTokenOverlay#getImageBounds(java.awt.Rectangle, Token)
*/
@Override
- protected Rectangle getImageBounds(Rectangle bounds, Token token) {
+ public Rectangle getImageBounds(Rectangle bounds, Token token) {
int x = (bounds.width + 1) / 2;
int y = (bounds.height + 1) / 2;
switch (corner) {
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/FlowColorDotTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/FlowColorDotTokenOverlay.java
index a503b737bd..c4558d5255 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/FlowColorDotTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/FlowColorDotTokenOverlay.java
@@ -114,7 +114,7 @@ public void paintOverlay(Graphics2D g, Token aToken, Rectangle bounds) {
* @param token Token being rendered.
* @return An ellipse that fits inside of the bounding box returned by the flow.
*/
- protected Shape getShape(Rectangle bounds, Token token) {
+ public Shape getShape(Rectangle bounds, Token token) {
Rectangle2D r = getFlow().getStateBounds2D(bounds, token, getName());
return new Ellipse2D.Double(r.getX(), r.getY(), r.getWidth(), r.getHeight());
}
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/FlowColorSquareTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/FlowColorSquareTokenOverlay.java
index 4b0419f2e4..04de9247e5 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/FlowColorSquareTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/FlowColorSquareTokenOverlay.java
@@ -64,7 +64,7 @@ public Object clone() {
* @see FlowColorDotTokenOverlay#getShape(java.awt.Rectangle, net.rptools.maptool.model.Token)
*/
@Override
- protected Shape getShape(Rectangle bounds, Token token) {
+ public Shape getShape(Rectangle bounds, Token token) {
return getFlow().getStateBounds2D(bounds, token, getName());
}
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/FlowDiamondTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/FlowDiamondTokenOverlay.java
index 5622189d98..ef16ce0ed2 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/FlowDiamondTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/FlowDiamondTokenOverlay.java
@@ -66,7 +66,7 @@ public Object clone() {
* @see FlowColorDotTokenOverlay#getShape(java.awt.Rectangle, net.rptools.maptool.model.Token)
*/
@Override
- protected Shape getShape(Rectangle bounds, Token token) {
+ public Shape getShape(Rectangle bounds, Token token) {
Rectangle2D r = getFlow().getStateBounds2D(bounds, token, getName());
GeneralPath p = new GeneralPath();
p.moveTo((float) r.getCenterX(), (float) r.getY());
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/FlowImageTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/FlowImageTokenOverlay.java
index 33533d560e..7396fc69fe 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/FlowImageTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/FlowImageTokenOverlay.java
@@ -65,7 +65,7 @@ protected TokenOverlayFlow getFlow() {
* @see ImageTokenOverlay#getImageBounds(java.awt.Rectangle, Token)
*/
@Override
- protected Rectangle getImageBounds(Rectangle bounds, Token token) {
+ public Rectangle getImageBounds(Rectangle bounds, Token token) {
return getFlow().getStateBounds(bounds, token, getName());
}
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/FlowTriangleTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/FlowTriangleTokenOverlay.java
index 4f26005b60..370cacd474 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/FlowTriangleTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/FlowTriangleTokenOverlay.java
@@ -65,7 +65,7 @@ public Object clone() {
* @see FlowColorDotTokenOverlay#getShape(java.awt.Rectangle, net.rptools.maptool.model.Token)
*/
@Override
- protected Shape getShape(Rectangle bounds, Token token) {
+ public Shape getShape(Rectangle bounds, Token token) {
Rectangle2D r = getFlow().getStateBounds2D(bounds, token, getName());
GeneralPath p = new GeneralPath();
p.moveTo((float) r.getCenterX(), (float) r.getY());
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/FlowYieldTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/FlowYieldTokenOverlay.java
index 727283ba46..b6d0edc592 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/FlowYieldTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/FlowYieldTokenOverlay.java
@@ -66,7 +66,7 @@ public Object clone() {
* @see FlowColorDotTokenOverlay#getShape(java.awt.Rectangle, net.rptools.maptool.model.Token)
*/
@Override
- protected Shape getShape(Rectangle bounds, Token token) {
+ public Shape getShape(Rectangle bounds, Token token) {
Rectangle2D r = getFlow().getStateBounds2D(bounds, token, getName());
GeneralPath p = new GeneralPath();
p.moveTo((float) r.getX(), (float) r.getY());
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/ImageTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/ImageTokenOverlay.java
index e3e9dc5cf6..da38d0bdee 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/ImageTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/ImageTokenOverlay.java
@@ -111,7 +111,7 @@ public MD5Key getAssetId() {
* @param token Token being decorated.
* @return The bounds w/in the token where the image is painted.
*/
- protected Rectangle getImageBounds(Rectangle bounds, Token token) {
+ public Rectangle getImageBounds(Rectangle bounds, Token token) {
return bounds;
}
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/XTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/XTokenOverlay.java
index 59db35089b..dd490186db 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/XTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/XTokenOverlay.java
@@ -115,7 +115,7 @@ public Color getColor() {
*
* @return Returns the current value of stroke.
*/
- protected BasicStroke getStroke() {
+ public BasicStroke getStroke() {
return stroke;
}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/IlluminationModel.java b/src/main/java/net/rptools/maptool/client/ui/zone/IlluminationModel.java
index b84c58d76a..a482dbc901 100644
--- a/src/main/java/net/rptools/maptool/client/ui/zone/IlluminationModel.java
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/IlluminationModel.java
@@ -25,6 +25,7 @@
import net.rptools.maptool.model.GUID;
import net.rptools.maptool.model.Light;
import net.rptools.maptool.model.LightSource;
+import net.rptools.maptool.model.Token;
/**
* Manages the light sources and illuminations of a zone, for a given set of illuminator parameters.
@@ -53,7 +54,7 @@ public record ContributedLight(Illuminator.LitArea litArea, LightInfo lightInfo)
* @param lightSource
* @param light
*/
- public record LightInfo(LightSource lightSource, Light light) {}
+ public record LightInfo(LightSource lightSource, Light light, Token lightSourceToken) {}
/**
* The data structure for calculating lit areas according to lumens. Lit areas can be added and
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java
index ba395114d0..ef11e69302 100644
--- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java
@@ -325,7 +325,8 @@ private List calculateLitAreaForLightSource(
litAreas.add(
new ContributedLight(
- new LitArea(light.getLumens(), area), new LightInfo(lightSource, light)));
+ new LitArea(light.getLumens(), area),
+ new LightInfo(lightSource, light, lightSourceToken)));
}
// Magnification can cause different ranges for a single light source to overlap. This is not
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/AreaRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/AreaRenderer.java
new file mode 100644
index 0000000000..4932958b82
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/AreaRenderer.java
@@ -0,0 +1,654 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.*;
+import com.badlogic.gdx.math.*;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.IntArray;
+import com.badlogic.gdx.utils.ShortArray;
+import java.awt.geom.Area;
+import java.awt.geom.PathIterator;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import net.rptools.lib.gdx.Earcut;
+import net.rptools.lib.gdx.Joiner;
+import space.earlygrey.shapedrawer.DefaultSideEstimator;
+import space.earlygrey.shapedrawer.ShapeDrawer;
+import space.earlygrey.shapedrawer.ShapeUtils;
+import space.earlygrey.shapedrawer.SideEstimator;
+
+public class AreaRenderer {
+ public record TriangledPolygon(float[] vertices, short[] indices) {}
+
+ private final ShapeDrawer drawer;
+ private final TextureRegion whitePixel;
+
+ private final FloatArray tmpFloat = new FloatArray();
+
+ private final IntArray segmentIndicies = new IntArray();
+
+ private final Color color = Color.WHITE.cpy();
+
+ public AreaRenderer(ShapeDrawer drawer) {
+ this.drawer = drawer;
+ this.whitePixel = drawer.getRegion();
+ }
+
+ public ShapeDrawer getShapeDrawer() {
+ return drawer;
+ }
+
+ public void setColor(Color value) {
+ color.set(Objects.requireNonNullElse(value, Color.WHITE));
+ textureRegion = whitePixel;
+ }
+
+ private final float[] floatsFromArea = new float[6];
+ private final Vector2 tmpVector = new Vector2();
+ private final Vector2 tmpVector0 = new Vector2();
+ private final Vector2 tmpVector1 = new Vector2();
+ private final Vector2 tmpVector2 = new Vector2();
+ private final Vector2 tmpVector3 = new Vector2();
+ private final Vector2 tmpVectorOut = new Vector2();
+
+ private TextureRegion textureRegion = null;
+
+ public TextureRegion getTextureRegion() {
+ return textureRegion;
+ }
+
+ public void setTextureRegion(TextureRegion textureRegion) {
+ this.textureRegion = textureRegion;
+ }
+
+ public List triangulate(Area area) {
+ if (area == null || area.isEmpty()) return new ArrayList<>();
+
+ var result = new ArrayList();
+
+ pathToFloatArray(area.getPathIterator(null));
+
+ if (segmentIndicies.size == 1) {
+ removeStartFromEnd();
+ var vertices = tmpFloat.toArray();
+ var indices = Earcut.earcut(vertices).toArray();
+ result.add(new TriangledPolygon(vertices, indices));
+ return result;
+ }
+
+ var lastSegmentIndex = 0;
+ var polygons = new ArrayList();
+ // Polygons in a PathIterator are ordered. If polygon p contains q, q comes first.
+ // So we draw polygons that contains others, those others are the holes.
+ var floats = tmpFloat.toArray();
+ for (int i = 1; i <= segmentIndicies.size; i++) {
+ var idx = i == segmentIndicies.size ? floats.length / 2 : segmentIndicies.get(i);
+ var vertexCount = idx - lastSegmentIndex;
+ var currentPolyVertices = new float[2 * vertexCount];
+ System.arraycopy(floats, 2 * lastSegmentIndex, currentPolyVertices, 0, 2 * vertexCount);
+ lastSegmentIndex = idx;
+
+ var poly = new Polygon(currentPolyVertices);
+ var holes = new ArrayList();
+
+ for (int j = 0; j < polygons.size(); j++) {
+ var prevPoly = polygons.get(j);
+ var prevVertices = prevPoly.getVertices();
+ if (poly.contains(prevVertices[0], prevVertices[1])) {
+ holes.add(prevPoly);
+ }
+ }
+ if (holes.isEmpty()) {
+ polygons.add(poly);
+ continue;
+ }
+ tmpFloat.clear();
+ tmpFloat.addAll(poly.getVertices());
+
+ short[] holeIndices = new short[holes.size()];
+ var lastPoly = poly;
+ var lastHoleIndex = 0;
+ for (int j = 0; j < holes.size(); j++) {
+ lastHoleIndex += lastPoly.getVertices().length;
+ holeIndices[j] = (short) (lastHoleIndex / 2);
+ lastPoly = holes.get(j);
+ polygons.remove(lastPoly);
+ tmpFloat.addAll(lastPoly.getVertices());
+ }
+
+ var indices = Earcut.earcut(tmpFloat.toArray(), holeIndices, (short) 2);
+ result.add(new TriangledPolygon(tmpFloat.toArray(), indices.toArray()));
+ }
+
+ for (var poly : polygons) {
+ var indices = Earcut.earcut(poly.getVertices());
+ result.add(new TriangledPolygon(poly.getVertices(), indices.toArray()));
+ }
+ return result;
+ }
+
+ public void fillArea(PolygonSpriteBatch batch, Area area) {
+ if (area == null || area.isEmpty()) return;
+ for (var poly : triangulate(area)) {
+ var polyRegion = new PolygonRegion(textureRegion, poly.vertices, poly.indices);
+ paintRegion(batch, polyRegion);
+ }
+ }
+
+ public void drawArea(PolygonSpriteBatch batch, Area area, boolean rounded, float thickness) {
+ if (area == null || area.isEmpty()) return;
+
+ pathToFloatArray(area.getPathIterator(null));
+
+ if (segmentIndicies.size == 1) {
+ removeStartFromEnd(); // start and end vertices are equal. we don't want this
+ var polygon =
+ drawPathWithJoin(tmpFloat, thickness, rounded ? JoinType.Round : JoinType.Pointy, false);
+ paintPolygon(batch, polygon);
+ } else {
+ var floats = tmpFloat.toArray();
+ var lastSegmentIndex = 0;
+ for (int i = 1; i <= segmentIndicies.size; i++) {
+ var idx = i == segmentIndicies.size ? floats.length / 2 : segmentIndicies.get(i);
+ var vertexCount = (idx - lastSegmentIndex);
+
+ tmpFloat.ensureCapacity(2 * vertexCount);
+ System.arraycopy(floats, 2 * lastSegmentIndex, tmpFloat.items, 0, 2 * vertexCount);
+ tmpFloat.setSize(2 * vertexCount);
+ removeStartFromEnd();
+ var polygon =
+ drawPathWithJoin(
+ tmpFloat, thickness, rounded ? JoinType.Round : JoinType.Pointy, false);
+ paintPolygon(batch, polygon);
+ lastSegmentIndex = idx;
+ }
+ }
+ }
+
+ private void removeStartFromEnd() {
+ while (tmpFloat.get(0) == tmpFloat.get(tmpFloat.size - 2)
+ && tmpFloat.get(1) == tmpFloat.get(tmpFloat.size - 1)) {
+ // make sure we don't have last and first point the same
+ tmpFloat.pop();
+ tmpFloat.pop();
+ }
+ }
+
+ public void paintPolygon(PolygonSpriteBatch batch, TriangledPolygon polygon) {
+ var polyReg = new PolygonRegion(textureRegion, polygon.vertices, polygon.indices);
+ paintRegion(batch, polyReg);
+ }
+
+ public void paintVertices(PolygonSpriteBatch batch, float[] vertices, short[] holeIndices) {
+ var indices = Earcut.earcut(vertices, holeIndices, (short) 2).toArray();
+ var polyReg = new PolygonRegion(textureRegion, vertices, indices);
+ paintRegion(batch, polyReg);
+ }
+
+ protected void paintRegion(PolygonSpriteBatch batch, PolygonRegion polygonRegion) {
+ var oldColor = batch.getColor();
+ batch.setColor(color);
+ batch.draw(polygonRegion, 0, 0);
+ batch.setColor(oldColor);
+ }
+
+ public FloatArray pathToFloatArray(PathIterator it) {
+ tmpFloat.clear();
+ segmentIndicies.clear();
+
+ var index = 0;
+ for (; !it.isDone(); it.next()) {
+ int type = it.currentSegment(floatsFromArea);
+
+ switch (type) {
+ case PathIterator.SEG_MOVETO:
+ // System.out.println("Move to: ( " + floatsFromArea[0] + ", " +
+ // floatsFromArea[1] + ")");
+ tmpFloat.add(floatsFromArea[0], -floatsFromArea[1]);
+ segmentIndicies.add(index);
+ index += 1;
+ break;
+ case PathIterator.SEG_CLOSE:
+ // System.out.println("Close");
+
+ break;
+ // return tmpFloat;
+ case PathIterator.SEG_LINETO:
+ // System.out.println("Line to: ( " + floatsFromArea[0] + ", " +
+ // floatsFromArea[1] + ")");
+
+ if (tmpFloat.get(tmpFloat.size - 2) != floatsFromArea[0]
+ || tmpFloat.get(tmpFloat.size - 1) != -floatsFromArea[1]) {
+ tmpFloat.add(floatsFromArea[0], -floatsFromArea[1]);
+ index += 1;
+ }
+ break;
+ case PathIterator.SEG_QUADTO:
+ // System.out.println("quadratic bezier with: ( " + floatsFromArea[0] +
+ // ", " + floatsFromArea[1] +
+ // "), (" + floatsFromArea[2] + ", " + floatsFromArea[3] + ")");
+
+ tmpVector0.set(tmpFloat.get(tmpFloat.size - 2), tmpFloat.get(tmpFloat.size - 1));
+ tmpVector1.set(floatsFromArea[0], -floatsFromArea[1]);
+ tmpVector2.set(floatsFromArea[2], -floatsFromArea[3]);
+ for (var i = 1; i <= GdxRenderer.POINTS_PER_BEZIER; i++) {
+ Bezier.quadratic(
+ tmpVectorOut,
+ i / GdxRenderer.POINTS_PER_BEZIER,
+ tmpVector0,
+ tmpVector1,
+ tmpVector2,
+ tmpVector);
+ tmpFloat.add(tmpVectorOut.x, tmpVectorOut.y);
+ index += 1;
+ }
+ break;
+ case PathIterator.SEG_CUBICTO:
+ // System.out.println("cubic bezier with: ( " + floatsFromArea[0] + ",
+ // " + floatsFromArea[1] +
+ // "), (" + floatsFromArea[2] + ", " + floatsFromArea[3] +
+ // "), (" + floatsFromArea[4] + ", " + floatsFromArea[5] +
+ // ")");
+
+ tmpVector0.set(tmpFloat.get(tmpFloat.size - 2), tmpFloat.get(tmpFloat.size - 1));
+ tmpVector1.set(floatsFromArea[0], -floatsFromArea[1]);
+ tmpVector2.set(floatsFromArea[2], -floatsFromArea[3]);
+ tmpVector3.set(floatsFromArea[4], -floatsFromArea[5]);
+ for (var i = 1; i <= GdxRenderer.POINTS_PER_BEZIER; i++) {
+ Bezier.cubic(
+ tmpVectorOut,
+ i / GdxRenderer.POINTS_PER_BEZIER,
+ tmpVector0,
+ tmpVector1,
+ tmpVector2,
+ tmpVector3,
+ tmpVector);
+ tmpFloat.add(tmpVectorOut.x, tmpVectorOut.y);
+ index += 1;
+ }
+ break;
+ default:
+ System.out.println("Type: " + type);
+ }
+ }
+
+ return tmpFloat;
+ }
+
+ private final Vector2 A = new Vector2();
+ private final Vector2 B = new Vector2();
+ private final Vector2 C = new Vector2();
+ private final Vector2 D = new Vector2();
+ private final Vector2 E = new Vector2();
+ private final Vector2 E0 = new Vector2();
+ private final Vector2 D0 = new Vector2();
+ private final Vector2 AB = new Vector2();
+ private final Vector2 BC = new Vector2();
+ private final Vector2 vec1 = new Vector2();
+
+ public enum JoinType {
+ Pointy,
+ Smooth,
+ Round
+ }
+
+ private final Vector2 vert1 = new Vector2();
+ private final Vector2 vert2 = new Vector2();
+ private final Vector2 vert3 = new Vector2();
+ private final Vector2 vert4 = new Vector2();
+
+ private void pushQuad(FloatArray vertices, ShortArray indices) {
+ var index = vertices.size / 2;
+ vertices.add(vert1.x);
+ vertices.add(vert1.y);
+ vertices.add(vert2.x);
+ vertices.add(vert2.y);
+ vertices.add(vert3.x);
+ vertices.add(vert3.y);
+ vertices.add(vert4.x);
+ vertices.add(vert4.y);
+ indices.add(index);
+ indices.add(index + 1);
+ indices.add(index + 2);
+ indices.add(index);
+ indices.add(index + 2);
+ indices.add(index + 3);
+ }
+
+ private void pushTriangle(FloatArray vertices, ShortArray indices) {
+ var index = vertices.size / 2;
+ vertices.add(vert1.x);
+ vertices.add(vert1.y);
+ vertices.add(vert2.x);
+ vertices.add(vert2.y);
+ vertices.add(vert3.x);
+ vertices.add(vert3.y);
+ indices.add(index);
+ indices.add(index + 1);
+ indices.add(index + 2);
+ }
+
+ public TriangledPolygon drawPathWithJoin(
+ FloatArray path, float lineWidth, JoinType joinType, boolean open) {
+ // this code was adapted from shapedrawer
+ float halfWidth = lineWidth / 2f;
+ boolean pointyJoin = joinType == JoinType.Pointy;
+
+ var vertices = new FloatArray();
+ var indices = new ShortArray();
+
+ if (path.size == 2) {
+ var x = path.get(0);
+ var y = path.get(1);
+ if (joinType == JoinType.Round) {
+ vertices.add(x + halfWidth, y);
+ addArc(vertices, indices, x, y, halfWidth, 0, MathUtils.PI2 - 0.1f, false);
+ vertices.add(x + halfWidth, y);
+ } else {
+ vert1.set(x - halfWidth, y - halfWidth);
+ vert2.set(x - halfWidth, y + halfWidth);
+ vert3.set(x + halfWidth, y + halfWidth);
+ vert4.set(x + halfWidth, y - halfWidth);
+ pushQuad(vertices, indices);
+ }
+ return new TriangledPolygon(vertices.toArray(), indices.toArray());
+ }
+
+ if (path.size == 4) {
+ A.set(path.get(0), path.get(1));
+ B.set(path.get(2), path.get(3));
+
+ if (joinType == JoinType.Round) {
+ Joiner.prepareFlatEndpoint(B, A, D, E, halfWidth);
+
+ vertices.add(D.x);
+ vertices.add(D.y);
+ vec1.set(D).add(-A.x, -A.y);
+ var angle = vec1.angleRad();
+ addArc(vertices, indices, A.x, A.y, halfWidth, angle, angle + MathUtils.PI, false);
+ vertices.add(E.x);
+ vertices.add(E.y);
+
+ vert1.set(D);
+ vert2.set(E);
+
+ Joiner.prepareFlatEndpoint(A, B, D, E, halfWidth);
+ vertices.add(D.x);
+ vertices.add(D.y);
+ vec1.set(D).add(-B.x, -B.y);
+ angle = vec1.angleRad();
+ addArc(vertices, indices, B.x, B.y, halfWidth, angle, angle + MathUtils.PI, false);
+
+ vertices.add(E.x);
+ vertices.add(E.y);
+
+ vert3.set(D);
+ vert4.set(E);
+ pushQuad(vertices, indices);
+
+ } else {
+ Joiner.prepareSquareEndpoint(B, A, D, E, halfWidth);
+ E0.set(D);
+ vert1.set(D);
+ vert2.set(E);
+
+ Joiner.prepareSquareEndpoint(A, B, D, E, halfWidth);
+ vert3.set(D);
+ vert4.set(E);
+ pushQuad(vertices, indices);
+ }
+ return new TriangledPolygon(vertices.toArray(), indices.toArray());
+ }
+
+ for (int i = 2; i < path.size - 2; i += 2) {
+ A.set(path.get(i - 2), path.get(i - 1));
+ B.set(path.get(i), path.get(i + 1));
+ C.set(path.get(i + 2), path.get(i + 3));
+
+ if (pointyJoin) {
+ Joiner.preparePointyJoin(A, B, C, D, E, halfWidth);
+ } else {
+ Joiner.prepareSmoothJoin(A, B, C, D, E, halfWidth, false);
+ }
+ vert3.set(D);
+ vert4.set(E);
+
+ if (i == 2) {
+ if (open) {
+ Joiner.prepareSquareEndpoint(B, A, D, E, halfWidth);
+ if (joinType == JoinType.Round) {
+ vec1.set(B).sub(A).setLength(halfWidth);
+ D.add(vec1);
+ E.add(vec1);
+ vertices.add(D.x);
+ vertices.add(D.y);
+ vec1.set(D).add(-A.x, -A.y);
+ var angle = vec1.angleRad();
+ addArc(vertices, indices, A.x, A.y, halfWidth, angle, angle + MathUtils.PI, false);
+ vertices.add(E.x);
+ vertices.add(E.y);
+ }
+
+ vert1.set(E);
+ vert2.set(D);
+
+ } else {
+ vec1.set(path.get(path.size - 2), path.get(path.size - 1));
+ if (pointyJoin) {
+ Joiner.preparePointyJoin(vec1, A, B, D0, E0, halfWidth);
+ } else {
+ Joiner.prepareSmoothJoin(vec1, A, B, D0, E0, halfWidth, true);
+ }
+ vert1.set(E0);
+ vert2.set(D0);
+ }
+ }
+
+ float x3, y3, x4, y4;
+ if (pointyJoin) {
+ x3 = vert3.x;
+ y3 = vert3.y;
+ x4 = vert4.x;
+ y4 = vert4.y;
+ } else {
+ Joiner.prepareSmoothJoin(A, B, C, D, E, halfWidth, true);
+ x3 = D.x;
+ y3 = D.y;
+ x4 = E.x;
+ y4 = E.y;
+ }
+
+ pushQuad(vertices, indices);
+ if (!pointyJoin) drawSmoothJoinFill(vertices, indices, A, B, C, D, E, halfWidth, joinType);
+ vert1.set(x4, y4);
+ vert2.set(x3, y3);
+ }
+
+ if (open) {
+ // draw last link on path
+ Joiner.prepareFlatEndpoint(B, C, D, E, halfWidth);
+ if (joinType == JoinType.Round) {
+
+ vertices.add(D.x);
+ vertices.add(D.y);
+ vec1.set(D).add(-C.x, -C.y);
+ var angle = vec1.angleRad();
+ addArc(vertices, indices, C.x, C.y, halfWidth, angle, angle + MathUtils.PI, false);
+ vertices.add(E.x);
+ vertices.add(E.y);
+ } else {
+ vec1.set(C).sub(B).setLength(halfWidth);
+ D.add(vec1);
+ E.add(vec1);
+ }
+
+ vert3.set(E);
+ vert4.set(D);
+ pushQuad(vertices, indices);
+ } else {
+ if (pointyJoin) {
+ // draw last link on path
+ A.set(path.get(0), path.get(1));
+ Joiner.preparePointyJoin(B, C, A, D, E, halfWidth);
+ vert3.set(D);
+ vert4.set(E);
+ pushQuad(vertices, indices);
+
+ // draw connection back to first vertex
+ vert1.set(D);
+ vert2.set(E);
+ vert3.set(E0);
+ vert4.set(D0);
+ pushQuad(vertices, indices);
+ } else {
+ // draw last link on path
+ A.set(B);
+ B.set(C);
+ C.set(path.get(0), path.get(1));
+ Joiner.prepareSmoothJoin(A, B, C, D, E, halfWidth, false);
+ vert3.set(D);
+ vert4.set(E);
+ pushQuad(vertices, indices);
+ drawSmoothJoinFill(vertices, indices, A, B, C, D, E, halfWidth, joinType);
+
+ // draw connection back to first vertex
+ Joiner.prepareSmoothJoin(A, B, C, D, E, halfWidth, true);
+ vert3.set(E);
+ vert4.set(D);
+ A.set(path.get(2), path.get(3));
+ Joiner.prepareSmoothJoin(B, C, A, D, E, halfWidth, false);
+ vert1.set(D);
+ vert2.set(E);
+ pushQuad(vertices, indices);
+ drawSmoothJoinFill(vertices, indices, B, C, A, D, E, halfWidth, joinType);
+ }
+ }
+ return new TriangledPolygon(vertices.toArray(), indices.toArray());
+ }
+
+ private void drawSmoothJoinFill(
+ FloatArray vertices,
+ ShortArray indices,
+ Vector2 A,
+ Vector2 B,
+ Vector2 C,
+ Vector2 D,
+ Vector2 E,
+ float halfLineWidth,
+ JoinType joinType) {
+ boolean bendsLeft = Joiner.prepareSmoothJoin(A, B, C, D, E, halfLineWidth, false);
+ vert1.set(bendsLeft ? E : D);
+ vert2.set(bendsLeft ? D : E);
+ if (bendsLeft) {
+ vec1.set(E);
+ } else {
+ vec1.set(D);
+ }
+
+ bendsLeft = Joiner.prepareSmoothJoin(A, B, C, D, E, halfLineWidth, true);
+ vert3.set(bendsLeft ? E : D);
+ pushTriangle(vertices, indices);
+
+ if (joinType == JoinType.Round) {
+ if (bendsLeft) {
+ AB.set(B).sub(A);
+ BC.set(C).sub(B);
+ vec1.add(-B.x, -B.y);
+ var angle = vec1.angleRad();
+ var angleDiff = MathUtils.PI2 - ShapeUtils.angleRad(AB, BC);
+ vertices.add(vert1.x);
+ vertices.add(vert1.y);
+ addArc(vertices, indices, B.x, B.y, halfLineWidth, angle, angle + angleDiff, false);
+ vertices.add(vert3.x);
+ vertices.add(vert3.y);
+ } else {
+ AB.set(B).sub(A);
+ BC.set(C).sub(B);
+ vec1.add(-B.x, -B.y);
+ var angle = vec1.angleRad();
+ var angleDiff = MathUtils.PI2 - ShapeUtils.angleRad(AB, BC);
+ vertices.add(vert1.x);
+ vertices.add(vert1.y);
+ addArc(vertices, indices, B.x, B.y, halfLineWidth, angle, angle + angleDiff, true);
+ vertices.add(vert3.x);
+ vertices.add(vert3.y);
+ }
+ }
+ }
+
+ private void addArc(
+ FloatArray vertices,
+ ShortArray indices,
+ float centreX,
+ float centreY,
+ float radius,
+ float startAngle,
+ float endAngle,
+ boolean clockwise) {
+ var oldSize = vertices.size;
+ var oldVertexCount = oldSize / 2;
+
+ if (startAngle < 0) {
+ startAngle += MathUtils.PI2;
+ }
+
+ if (endAngle < 0) {
+ endAngle += MathUtils.PI2;
+ }
+
+ var deltaAngle = (endAngle + MathUtils.PI2 - startAngle) % MathUtils.PI2;
+ if (clockwise) {
+ deltaAngle = MathUtils.PI2 - deltaAngle;
+ }
+ var sides = estimateSidesRequired(radius, radius);
+ sides *= deltaAngle / MathUtils.PI2;
+
+ var dAnglePerSide = deltaAngle / sides;
+ var angle = startAngle;
+ angle += dAnglePerSide;
+ sides -= 1;
+ if (clockwise) {
+ dAnglePerSide *= -1;
+ angle += 2 * dAnglePerSide;
+ }
+
+ for (var i = 1; i <= sides; i++) {
+ var cos = MathUtils.cos(angle);
+ var sin = MathUtils.sin(angle);
+ angle += dAnglePerSide;
+ var x = centreX + cos * radius;
+ var y = centreY + sin * radius;
+
+ vertices.add(x);
+ vertices.add(y);
+ }
+ var vertexCount = (vertices.size - oldSize) / 2;
+
+ for (int j = 0; j < vertexCount; j++) {
+ indices.add(oldVertexCount - 1);
+ indices.add(oldVertexCount + j);
+ indices.add(oldVertexCount + j + 1);
+ }
+ }
+
+ private final SideEstimator sideEstimator = new DefaultSideEstimator();
+
+ protected int estimateSidesRequired(float radiusX, float radiusY) {
+ return sideEstimator.estimateSidesRequired(1, radiusX, radiusY);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/GdxRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/GdxRenderer.java
new file mode 100644
index 0000000000..0c1f004794
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/GdxRenderer.java
@@ -0,0 +1,2310 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
+import com.badlogic.gdx.graphics.*;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.*;
+import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
+import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGeneratorLoader;
+import com.badlogic.gdx.graphics.g2d.freetype.FreetypeFontLoader;
+import com.badlogic.gdx.graphics.glutils.FrameBuffer;
+import com.badlogic.gdx.math.*;
+import com.badlogic.gdx.math.Rectangle;
+import com.badlogic.gdx.scenes.scene2d.utils.TiledDrawable;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.ScreenUtils;
+import com.google.common.eventbus.Subscribe;
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Area;
+import java.awt.geom.GeneralPath;
+import java.text.NumberFormat;
+import java.util.*;
+import java.util.List;
+import java.util.zip.Deflater;
+import javax.swing.*;
+import net.rptools.lib.CodeTimer;
+import net.rptools.maptool.client.*;
+import net.rptools.maptool.client.events.ZoneActivated;
+import net.rptools.maptool.client.swing.ImageBorder;
+import net.rptools.maptool.client.swing.SwingUtil;
+import net.rptools.maptool.client.tool.Tool;
+import net.rptools.maptool.client.tool.WallTopologyTool;
+import net.rptools.maptool.client.ui.Scale;
+import net.rptools.maptool.client.ui.theme.Borders;
+import net.rptools.maptool.client.ui.theme.RessourceManager;
+import net.rptools.maptool.client.ui.token.AbstractTokenOverlay;
+import net.rptools.maptool.client.ui.token.BarTokenOverlay;
+import net.rptools.maptool.client.ui.zone.DrawableLight;
+import net.rptools.maptool.client.ui.zone.PlayerView;
+import net.rptools.maptool.client.ui.zone.gdx.drawing.DrawnElementRenderer;
+import net.rptools.maptool.client.ui.zone.gdx.label.ItemRenderer;
+import net.rptools.maptool.client.ui.zone.gdx.label.LabelRenderer;
+import net.rptools.maptool.client.ui.zone.gdx.label.TextRenderer;
+import net.rptools.maptool.client.ui.zone.gdx.label.TokenLabelRenderer;
+import net.rptools.maptool.client.ui.zone.renderer.SelectionSet;
+import net.rptools.maptool.client.walker.ZoneWalker;
+import net.rptools.maptool.events.MapToolEventBus;
+import net.rptools.maptool.language.I18N;
+import net.rptools.maptool.model.*;
+import net.rptools.maptool.model.Label;
+import net.rptools.maptool.model.Path;
+import net.rptools.maptool.model.drawing.DrawableColorPaint;
+import net.rptools.maptool.model.drawing.DrawnElement;
+import net.rptools.maptool.util.GraphicsUtil;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import space.earlygrey.shapedrawer.JoinType;
+import space.earlygrey.shapedrawer.ShapeDrawer;
+
+/**
+ * The coordinates in the model are y-down, x-left. The world coordinates are y-up, x-left. I moved
+ * the world to the 4th quadrant of the coordinate system. So if you would draw a token t awt at
+ * (x,y) you have to draw it at (x, -y - t.width)
+ *
+ *
+ */
+public class GdxRenderer extends ApplicationAdapter {
+
+ private static final Logger log = LogManager.getLogger(GdxRenderer.class);
+
+ public static final float POINTS_PER_BEZIER = 10f;
+ private static GdxRenderer _instance;
+
+ // renderFog
+ private final String ATLAS = "net/rptools/maptool/client/maptool.atlas";
+ private final String FONT_NORMAL = "normalFont.ttf";
+ private final String FONT_BOLD = "boldFont.ttf";
+
+ private final String font = "NotoSansSymbols";
+
+ // from renderToken:
+ private Area visibleScreenArea;
+ private Area exposedFogArea;
+ private PlayerView lastView;
+ private final List itemRenderList = new LinkedList<>();
+
+ // zone specific resources
+ private ZoneCache zoneCache;
+ private int offsetX = 0;
+ private int offsetY = 0;
+ private float zoom = 1.0f;
+ private float stateTime = 0f;
+ private boolean renderZone = false;
+ private boolean showAstarDebugging = false;
+
+ // general resources
+ private OrthographicCamera cam;
+ private PerspectiveCamera cam3d;
+ private OrthographicCamera hudCam;
+ private PolygonSpriteBatch batch;
+ private boolean initialized = false;
+ private int width;
+ private int height;
+ private BitmapFont normalFont;
+ private BitmapFont boldFont;
+ private float boldFontScale = 0;
+ private final CodeTimer timer = new CodeTimer("GdxRenderer.renderZone");
+ private FrameBuffer backBuffer;
+ private com.badlogic.gdx.assets.AssetManager manager;
+ private TextureAtlas atlas;
+ private Texture onePixel;
+ private ShapeDrawer drawer;
+ private final GlyphLayout glyphLayout = new GlyphLayout();
+ private TextRenderer textRenderer;
+ private TextRenderer hudTextRenderer;
+ private AreaRenderer areaRenderer;
+ private DrawnElementRenderer drawnElementRenderer;
+ private TokenOverlayRenderer tokenOverlayRenderer;
+ private GridRenderer gridRenderer;
+
+ // temorary objects. Stored here to avoid garbage collection;
+ private final Vector3 tmpWorldCoord = new Vector3();
+ private final Color tmpColor = new Color();
+ private final FloatArray tmpFloat = new FloatArray();
+ private final Vector2 tmpVector = new Vector2();
+ private final Vector2 tmpVectorOut = new Vector2();
+ private final Vector2 tmpVector0 = new Vector2();
+ private final Vector2 tmpVector1 = new Vector2();
+ private final Vector2 tmpVector2 = new Vector2();
+ private final Matrix4 tmpMatrix = new Matrix4();
+ private final Area tmpArea = new Area();
+ private final TiledDrawable tmpTile = new TiledDrawable();
+
+ public GdxRenderer() {
+ new MapToolEventBus().getMainEventBus().register(this);
+ }
+
+ public static GdxRenderer getInstance() {
+ if (_instance == null) _instance = new GdxRenderer();
+ return _instance;
+ }
+
+ @Override
+ public void create() {
+ // with jogl create is called every time we change the parent frame of the GLJPanel
+ // e.g. change from fullcreen to window or the other way around. Reinit everthing in this case.
+ if (initialized) {
+ initialized = false;
+ dispose();
+
+ atlas = null;
+ normalFont = null;
+ boldFont = null;
+ }
+
+ manager = new com.badlogic.gdx.assets.AssetManager();
+ loadAssets();
+
+ var resolver = new InternalFileHandleResolver();
+ manager.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver));
+ manager.setLoader(BitmapFont.class, ".ttf", new FreetypeFontLoader(resolver));
+
+ width = Gdx.graphics.getWidth();
+ height = Gdx.graphics.getHeight();
+
+ // Cam for 3D-Models
+ cam3d = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
+ cam3d.lookAt(0, 0, 0);
+
+ cam = new OrthographicCamera();
+ cam.setToOrtho(false);
+
+ hudCam = new OrthographicCamera();
+ hudCam.setToOrtho(false);
+
+ updateCam();
+
+ batch = new PolygonSpriteBatch();
+ batch.enableBlending();
+
+ backBuffer = new FrameBuffer(Pixmap.Format.RGBA8888, width, height, false);
+
+ // TODO: Add it to the texture atlas
+ Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
+ pixmap.setColor(Color.WHITE);
+ pixmap.drawPixel(0, 0);
+ onePixel = new Texture(pixmap);
+ pixmap.dispose();
+ TextureRegion region = new TextureRegion(onePixel, 0, 0, 1, 1);
+ drawer = new ShapeDrawer(batch, region);
+
+ areaRenderer = new AreaRenderer(drawer);
+ drawnElementRenderer = new DrawnElementRenderer(areaRenderer);
+ tokenOverlayRenderer = new TokenOverlayRenderer(areaRenderer);
+ gridRenderer = new GridRenderer(areaRenderer, hudCam);
+
+ initialized = true;
+ }
+
+ @Override
+ public void dispose() {
+ manager.dispose();
+ batch.dispose();
+ if (zoneCache != null) {
+ zoneCache.dispose();
+ }
+ onePixel.dispose();
+ }
+
+ @Override
+ public void resize(int width, int height) {
+ this.width = width;
+ this.height = height;
+ backBuffer.dispose();
+ backBuffer = new FrameBuffer(Pixmap.Format.RGBA8888, width, height, false);
+
+ updateCam();
+ }
+
+ private void updateCam() {
+ if (cam == null) return;
+
+ cam3d.viewportWidth = width;
+ cam3d.viewportHeight = height;
+
+ cam3d.position.x = zoom * (width / 2f + offsetX);
+ cam3d.position.y = zoom * (height / 2f * -1 + offsetY);
+ cam3d.position.z =
+ (zoom * height) / (2f * (float) Math.tan(Math.toRadians(cam3d.fieldOfView / 2f)));
+ cam3d.far = 1.1f * cam3d.position.z;
+ cam3d.near = 0.1f * cam3d.position.z;
+ cam3d.update();
+
+ cam.viewportWidth = width;
+ cam.viewportHeight = height;
+ cam.position.x = zoom * (width / 2f + offsetX);
+ cam.position.y = zoom * (height / 2f * -1 + offsetY);
+ cam.zoom = zoom;
+ cam.update();
+
+ hudCam.viewportWidth = width;
+ hudCam.viewportHeight = height;
+ hudCam.position.x = width / 2f;
+ hudCam.position.y = height / 2f;
+ hudCam.update();
+ }
+
+ @Override
+ public void render() {
+ // System.out.println("FPS: " + Gdx.graphics.getFramesPerSecond());
+ var delta = Gdx.graphics.getDeltaTime();
+ stateTime += delta;
+ manager.finishLoading();
+
+ if (atlas == null) {
+ atlas = manager.get(ATLAS, TextureAtlas.class);
+ zoneCache.setSharedAtlas(atlas);
+ }
+
+ if (normalFont == null) {
+ normalFont = manager.get(FONT_NORMAL, BitmapFont.class);
+ textRenderer = new TextRenderer(atlas, batch, normalFont);
+ hudTextRenderer = new TextRenderer(atlas, batch, normalFont, false);
+ }
+
+ ensureTtfFont();
+ ScreenUtils.clear(Color.BLACK);
+ try {
+ doRendering();
+ } catch (Exception ex) {
+ log.warn(ex);
+ }
+ }
+
+ private void ensureTtfFont() {
+ if (zoneCache == null) return;
+
+ var fontScale =
+ (float) zoneCache.getZone().getGrid().getSize()
+ / 50; // Font size of 12 at grid size 50 is default
+
+ if (fontScale == this.boldFontScale && boldFont != null) return;
+
+ var fontParams = new FreetypeFontLoader.FreeTypeFontLoaderParameter();
+ // fontParams.fontFileName = "net/rptools/maptool/client/fonts/OpenSans-Bold.ttf";
+ fontParams.fontFileName =
+ String.format("net/rptools/maptool/client/fonts/%s/%s-Bold.ttf", font, font);
+ fontParams.fontParameters.size = (int) (12 * fontScale);
+ manager.load(FONT_BOLD, BitmapFont.class, fontParams);
+ manager.finishLoading();
+ boldFont = manager.get(FONT_BOLD, BitmapFont.class);
+ boldFontScale = fontScale;
+ }
+
+ private void loadAssets() {
+ manager.load(ATLAS, TextureAtlas.class);
+ var fontParams = new FreetypeFontLoader.FreeTypeFontLoaderParameter();
+ fontParams.fontFileName =
+ String.format("net/rptools/maptool/client/fonts/%s/%s-Regular.ttf", font, font);
+ fontParams.fontParameters.size = 12;
+ manager.load(FONT_NORMAL, BitmapFont.class, fontParams);
+ }
+
+ private void doRendering() {
+ batch.enableBlending();
+ batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
+
+ // this happens sometimes when starting with ide (non-debug)
+ if (batch.isDrawing()) batch.end();
+ batch.begin();
+
+ if (zoneCache == null || !renderZone) return;
+
+ initializeTimer();
+ if (zoneCache.getZoneRenderer() == null) return;
+
+ setScale(zoneCache.getZoneRenderer().getZoneScale());
+
+ timer.start("paintComponent:createView");
+ PlayerView playerView = zoneCache.getZoneRenderer().getPlayerView();
+ timer.stop("paintComponent:createView");
+
+ setProjectionMatrix(cam.combined);
+
+ renderZone(playerView);
+
+ setProjectionMatrix(hudCam.combined);
+
+ if (zoneCache.getZoneRenderer().isLoading())
+ hudTextRenderer.drawBoxedString(
+ zoneCache.getZoneRenderer().getLoadingProgress(), width / 2f, height / 2f);
+ else if (MapTool.getCampaign().isBeingSerialized())
+ hudTextRenderer.drawBoxedString(" Please Wait ", width / 2f, height / 2f);
+
+ float noteVPos = 20;
+ if (!zoneCache.getZone().isVisible() && playerView.isGMView()) {
+ hudTextRenderer.drawBoxedString(
+ I18N.getText("zone.map_not_visible"), width / 2f, height - noteVPos);
+ noteVPos += 20;
+ }
+ if (AppState.isShowAsPlayer()) {
+ hudTextRenderer.drawBoxedString(
+ I18N.getText("zone.player_view"), width / 2f, height - noteVPos);
+ }
+
+ hudTextRenderer.drawString("FPS: " + Gdx.graphics.getFramesPerSecond(), width - 30, 30);
+ hudTextRenderer.drawString("Draws: " + batch.renderCalls, width - 30, 16);
+
+ batch.end();
+ collectTimerResults();
+ }
+
+ private void collectTimerResults() {
+ if (timer.isEnabled()) {
+ String results = timer.toString();
+ MapTool.getProfilingNoteFrame().addText(results);
+ if (log.isDebugEnabled()) {
+ log.debug(results);
+ }
+ timer.clear();
+ }
+ }
+
+ private void initializeTimer() {
+ timer.setEnabled(AppState.isCollectProfilingData() || log.isDebugEnabled());
+ timer.clear();
+ timer.setThreshold(10);
+ }
+
+ public void invalidateCurrentViewCache() {
+ visibleScreenArea = null;
+ lastView = null;
+ }
+
+ private void renderZone(PlayerView view) {
+ if (zoneCache.getZoneRenderer().isLoading() || MapTool.getCampaign().isBeingSerialized())
+ return;
+
+ if (lastView != null && !lastView.equals(view)) {
+ invalidateCurrentViewCache();
+ }
+ lastView = view;
+ itemRenderList.clear();
+
+ // Calculations
+ timer.start("calcs-1");
+ timer.start("ZoneRenderer-getVisibleArea");
+ if (visibleScreenArea == null) {
+ visibleScreenArea =
+ zoneCache.getZoneView().getVisibleArea(zoneCache.getZoneRenderer().getPlayerView());
+ }
+ timer.stop("ZoneRenderer-getVisibleArea");
+
+ timer.stop("calcs-1");
+ timer.start("calcs-2");
+ exposedFogArea = new Area(zoneCache.getZone().getExposedArea());
+ timer.stop("calcs-2");
+
+ renderBoard();
+
+ if (zoneCache.getZoneRenderer().shouldRenderLayer(Zone.Layer.BACKGROUND, view)) {
+ List drawables = zoneCache.getZone().getDrawnElements(Zone.Layer.BACKGROUND);
+
+ timer.start("drawableBackground");
+ drawnElementRenderer.render(batch, zoneCache.getZone(), drawables);
+ timer.stop("drawableBackground");
+
+ List background = zoneCache.getZone().getTokensOnLayer(Zone.Layer.BACKGROUND, false);
+ if (!background.isEmpty()) {
+ timer.start("tokensBackground");
+ renderTokens(background, view, false);
+ timer.stop("tokensBackground");
+ }
+ }
+ if (zoneCache.getZoneRenderer().shouldRenderLayer(Zone.Layer.OBJECT, view)) {
+ // Drawables on the object layer are always below the grid, and...
+ List drawables = zoneCache.getZone().getDrawnElements(Zone.Layer.OBJECT);
+ // if (!drawables.isEmpty()) {
+ timer.start("drawableObjects");
+ drawnElementRenderer.render(batch, zoneCache.getZone(), drawables);
+ timer.stop("drawableObjects");
+ // }
+ }
+ timer.start("grid");
+ setProjectionMatrix(hudCam.combined);
+ gridRenderer.render();
+ setProjectionMatrix(cam.combined);
+
+ timer.stop("grid");
+
+ if (zoneCache.getZoneRenderer().shouldRenderLayer(Zone.Layer.OBJECT, view)) {
+ // ... Images on the object layer are always ABOVE the grid.
+ List stamps = zoneCache.getZone().getTokensOnLayer(Zone.Layer.OBJECT, false);
+ if (!stamps.isEmpty()) {
+ timer.start("tokensStamp");
+ renderTokens(stamps, view, false);
+ timer.stop("tokensStamp");
+ }
+ }
+ if (zoneCache.getZoneRenderer().shouldRenderLayer(Zone.Layer.TOKEN, view)) {
+ timer.start("lights");
+ renderLights(view);
+ timer.stop("lights");
+
+ timer.start("auras");
+ renderAuras(view);
+ timer.stop("auras");
+ }
+ renderPlayerDarkness(view);
+ // *
+ // * The following sections used to handle rendering of the Hidden (i.e. "GM") layer
+ // followed by
+ // * the Token layer. The problem was that we want all drawables to appear below all tokens,
+ // and
+ // * the old configuration performed the rendering in the following order:
+ // *
+ // *
+ // * - Render Hidden-layer tokens
+ // *
- Render Hidden-layer drawables
+ // *
- Render Token-layer drawables
+ // *
- Render Token-layer tokens
+ // *
+ // *
+ // * That's fine for players, but clearly wrong if the view is for the GM. We now use:
+ // *
+ // *
+ // * - Render Token-layer drawables // Player-drawn images shouldn't obscure GM's
+ // images?
+ // *
- Render Hidden-layer drawables // GM could always use "View As Player" if needed?
+ // *
- Render Hidden-layer tokens
+ // *
- Render Token-layer tokens
+ // *
+ // *
+ if (zoneCache.getZoneRenderer().shouldRenderLayer(Zone.Layer.TOKEN, view)) {
+ List drawables = zoneCache.getZone().getDrawnElements(Zone.Layer.TOKEN);
+ // if (!drawables.isEmpty()) {
+ timer.start("drawableTokens");
+ drawnElementRenderer.render(batch, zoneCache.getZone(), drawables);
+ timer.stop("drawableTokens");
+ // }
+
+ if (zoneCache.getZoneRenderer().shouldRenderLayer(Zone.Layer.GM, view)) {
+ drawables = zoneCache.getZone().getDrawnElements(Zone.Layer.GM);
+ // if (!drawables.isEmpty()) {
+ timer.start("drawableGM");
+ drawnElementRenderer.render(batch, zoneCache.getZone(), drawables);
+ timer.stop("drawableGM");
+ // }
+ List stamps = zoneCache.getZone().getTokensOnLayer(Zone.Layer.GM, false);
+ if (!stamps.isEmpty()) {
+ timer.start("tokensGM");
+ renderTokens(stamps, view, false);
+ timer.stop("tokensGM");
+ }
+ }
+ List tokens = zoneCache.getZone().getTokensOnLayer(Zone.Layer.TOKEN, false);
+ if (!tokens.isEmpty()) {
+ timer.start("tokens");
+ renderTokens(tokens, view, false);
+ timer.stop("tokens");
+ }
+ timer.start("unowned movement");
+ showBlockedMoves(view, zoneCache.getZoneRenderer().getUnOwnedMovementSet(view));
+ timer.stop("unowned movement");
+ }
+
+ // *
+ // * FJE It's probably not appropriate for labels to be above everything, including tokens.
+ // Above
+ // * drawables, yes. Above tokens, no. (Although in that case labels could be completely
+ // obscured.
+ // * Hm.)
+ // *
+ // Drawing labels is slooooow. :(
+ // Perhaps we should draw the fog first and use hard fog to determine whether labels need to be
+ // drawn?
+ // (This method has it's own 'timer' calls)
+ if (AppState.getShowTextLabels()) {
+ renderLabels(view);
+ }
+
+ // if(zoneCache.getZone().getLightingStyle() == Zone.LightingStyle.ENVIRONMENTAL &&
+ // AppState.isShowLights()) {
+ if (view.isGMView()) {
+ // rayHandler.setAmbientLight(0.6f);
+ } else {
+ // rayHandler.setAmbientLight(1.0f);
+ }
+ // rayHandler.setCombinedMatrix(cam);
+ // rayHandler.updateAndRender();
+ // }
+
+ // (This method has it's own 'timer' calls)
+ if (zoneCache.getZone().hasFog()) {
+ renderFog(view);
+ }
+
+ if (zoneCache.getZoneRenderer().shouldRenderLayer(Zone.Layer.TOKEN, view)) {
+ // Jamz: If there is fog or vision we may need to re-render vision-blocking type tokens
+ // For example. this allows a "door" stamp to block vision but still allow you to see the
+ // door.
+ List vblTokens = zoneCache.getZone().getTokensAlwaysVisible();
+ if (!vblTokens.isEmpty()) {
+ timer.start("tokens - always visible");
+ renderTokens(vblTokens, view, true);
+ timer.stop("tokens - always visible");
+ }
+
+ // if there is fog or vision we may need to re-render figure type tokens
+ // and figure tokens need sorting via alternative logic.
+ List tokens = zoneCache.getZone().getFigureTokens();
+ List sortedTokens = new ArrayList<>(tokens);
+ sortedTokens.sort(zoneCache.getZone().getFigureZOrderComparator());
+ if (!tokens.isEmpty()) {
+ timer.start("tokens - figures");
+ renderTokens(sortedTokens, view, true);
+ timer.stop("tokens - figures");
+ }
+
+ timer.start("owned movement");
+ showBlockedMoves(view, zoneCache.getZoneRenderer().getOwnedMovementSet(view));
+ timer.stop("owned movement");
+
+ // Text associated with tokens being moved is added to a list to be drawn after, i.e. on top
+ // of, the tokens
+ // themselves.
+ // So if one moving token is on top of another moving token, at least the textual identifiers
+ // will be
+ // visible.
+
+ setProjectionMatrix(hudCam.combined);
+ timer.start("token name/labels");
+ renderRenderables();
+ timer.stop("token name/labels");
+ setProjectionMatrix(cam.combined);
+ }
+
+ // if (zoneCache.getZone().visionType ...)
+ if (view.isGMView()) {
+ timer.start("visionOverlayGM");
+ renderGMVisionOverlay(view);
+ timer.stop("visionOverlayGM");
+ } else {
+ timer.start("visionOverlayPlayer");
+ renderPlayerVisionOverlay(view);
+ timer.stop("visionOverlayPlayer");
+ }
+
+ timer.start("renderCoordinates");
+ renderCoordinates(view);
+ timer.stop("renderCoordinates");
+
+ timer.start("lightSourceIconOverlay.paintOverlay");
+ if (zoneCache.getZoneRenderer().shouldRenderLayer(Zone.Layer.TOKEN, view)
+ && view.isGMView()
+ && AppState.isShowLightSources()) {
+ paintlightSourceIconOverlay();
+ }
+ timer.stop("lightSourceIconOverlay.paintOverlay");
+ }
+
+ private void renderCoordinates(PlayerView view) {
+ if (!AppState.isShowCoordinates()
+ || !(zoneCache.getZone().getGrid() instanceof SquareGrid grid)) return;
+
+ batch.setProjectionMatrix(hudCam.combined);
+ var font = boldFont;
+
+ float cellSize = (float) zoneCache.getZoneRenderer().getScaledGridSize();
+ CellPoint topLeft =
+ grid.convert(new ScreenPoint(0, 0).convertToZone(zoneCache.getZoneRenderer()));
+ ScreenPoint sp = ScreenPoint.fromZonePoint(zoneCache.getZoneRenderer(), grid.convert(topLeft));
+
+ Dimension size = zoneCache.getZoneRenderer().getSize();
+ glyphLayout.setText(font, "MMM");
+ float startX = glyphLayout.width + 10;
+
+ float x = (float) (sp.x + cellSize / 2); // Start at middle of the cell that's on screen
+ float nextAvailableSpace = -1;
+ while (x < size.width) {
+ String coord = Integer.toString(topLeft.x);
+ glyphLayout.setText(font, coord);
+ float strWidth = glyphLayout.width;
+ float strX = (int) x - strWidth / 2;
+
+ if (x > startX && strX > nextAvailableSpace) {
+ font.setColor(Color.BLACK);
+ font.draw(batch, coord, strX, height - glyphLayout.height / 2 - 1);
+ font.setColor(Color.ORANGE);
+ font.draw(batch, coord, strX - 1, height - glyphLayout.height / 2);
+
+ nextAvailableSpace = strX + strWidth + 10;
+ }
+ x += cellSize;
+ topLeft.x++;
+ }
+ float y = (float) sp.y + cellSize / 2f; // Start at middle of the cell that's on screen
+ nextAvailableSpace = -1;
+ while (y < size.height) {
+ String coord = grid.decimalToAlphaCoord(topLeft.y);
+
+ float strY = y + font.getAscent() / 2;
+
+ if (y > glyphLayout.height && strY > nextAvailableSpace) {
+ font.setColor(Color.BLACK);
+ font.draw(batch, coord, 10, height - strY + glyphLayout.height / 2 - 1);
+ font.setColor(Color.YELLOW);
+ font.draw(batch, coord, 10 - 1, height - strY + glyphLayout.height / 2);
+
+ nextAvailableSpace = strY + font.getAscent() / 2 + 10;
+ }
+ y += cellSize;
+ topLeft.y++;
+ }
+ batch.setProjectionMatrix(cam.combined);
+ }
+
+ private void paintlightSourceIconOverlay() {
+ var lightbulb = zoneCache.fetch("lightbulb");
+ for (Token token : zoneCache.getZone().getAllTokens()) {
+
+ if (token.hasLightSources()) {
+ boolean foundNormalLight = false;
+ for (AttachedLightSource attachedLightSource : token.getLightSources()) {
+ LightSource lightSource = attachedLightSource.resolve(token, MapTool.getCampaign());
+ if (lightSource != null && lightSource.getType() == LightSource.Type.NORMAL) {
+ foundNormalLight = true;
+ break;
+ }
+ }
+ if (!foundNormalLight) {
+ continue;
+ }
+
+ Area area = zoneCache.getZoneRenderer().getTokenBounds(token);
+ if (area == null) {
+ continue;
+ }
+
+ int x = area.getBounds().x + (area.getBounds().width - lightbulb.getRegionWidth()) / 2;
+ int y = -area.getBounds().y - (area.getBounds().height + lightbulb.getRegionHeight()) / 2;
+ batch.draw(lightbulb, x, y);
+ }
+ }
+ }
+
+ private void renderPlayerDarkness(PlayerView view) {
+ if (view.isGMView()) {
+ // GMs see the darkness rendered as lights, not as blackness.
+ return;
+ }
+
+ final var darkness = zoneCache.getZoneView().getIllumination(view).getDarkenedArea();
+ if (darkness.isEmpty()) {
+ // Skip the rendering work if it isn't necessary.
+ return;
+ }
+ areaRenderer.setColor(Color.BLACK);
+ areaRenderer.fillArea(batch, darkness);
+ }
+
+ private void renderPlayerVisionOverlay(PlayerView view) {
+ /* // This doesn't seem to have any effect ??
+ if (zoneCache.getZone().hasFog()) {
+ Area clip = new Area(new Rectangle(getSize().width, getSize().height));
+
+ Area viewArea = new Area(exposedFogArea);
+ List tokens = view.getTokens();
+ if (tokens != null && !tokens.isEmpty()) {
+ for (Token tok : tokens) {
+ ExposedAreaMetaData exposedMeta = zoneCache.getZone().getExposedAreaMetaData(tok.getExposedAreaGUID());
+ viewArea.add(exposedMeta.getExposedAreaHistory());
+ }
+ }
+ if (!viewArea.isEmpty()) {
+ clip.intersect(new Area(viewArea.getBounds2D()));
+ }
+ // Note: the viewArea doesn't need to be transform()'d because exposedFogArea has been
+ // already.
+ g2.setClip(clip);
+ }*/
+ renderVisionOverlay(view);
+ }
+
+ private void renderGMVisionOverlay(PlayerView view) {
+ renderVisionOverlay(view);
+ }
+
+ private void renderVisionOverlay(PlayerView view) {
+ var tokenUnderMouse = zoneCache.getZoneRenderer().getTokenUnderMouse();
+ if (tokenUnderMouse == null) return;
+
+ Area currentTokenVisionArea = zoneCache.getZoneView().getVisibleArea(tokenUnderMouse, view);
+ if (currentTokenVisionArea == null) {
+ return;
+ }
+ Area combined = new Area(currentTokenVisionArea);
+ ExposedAreaMetaData meta =
+ zoneCache.getZone().getExposedAreaMetaData(tokenUnderMouse.getExposedAreaGUID());
+
+ Area tmpArea = new Area(meta.getExposedAreaHistory());
+ tmpArea.add(zoneCache.getZone().getExposedArea());
+ if (zoneCache.getZone().hasFog()) {
+ if (tmpArea.isEmpty()) {
+ return;
+ }
+ combined.intersect(tmpArea);
+ }
+ boolean isOwner = AppUtil.playerOwns(tokenUnderMouse);
+ boolean tokenIsPC = tokenUnderMouse.getType() == Token.Type.PC;
+ boolean strictOwnership =
+ MapTool.getServerPolicy() != null && MapTool.getServerPolicy().useStrictTokenManagement();
+ boolean showVisionAndHalo = isOwner || view.isGMView() || (tokenIsPC && !strictOwnership);
+
+ /*
+ * The vision arc and optional halo-filled visible area shouldn't be shown to everyone. If we are in GM view, or if we are the owner of the token in question, or if the token is a PC and
+ * strict token ownership is off... then the vision arc should be displayed.
+ */
+ if (showVisionAndHalo) {
+ areaRenderer.setColor(Color.WHITE);
+ areaRenderer.drawArea(batch, combined, false, 1);
+ renderHaloArea(combined);
+ }
+ }
+
+ private void renderHaloArea(Area visible) {
+ var tokenUnderMouse = zoneCache.getZoneRenderer().getTokenUnderMouse();
+ if (tokenUnderMouse == null) return;
+
+ boolean useHaloColor =
+ tokenUnderMouse.getHaloColor() != null && AppPreferences.useHaloColorOnVisionOverlay.get();
+ if (tokenUnderMouse.getVisionOverlayColor() != null || useHaloColor) {
+ java.awt.Color visionColor =
+ useHaloColor ? tokenUnderMouse.getHaloColor() : tokenUnderMouse.getVisionOverlayColor();
+
+ tmpColor.set(
+ visionColor.getRed() / 255f,
+ visionColor.getGreen() / 255f,
+ visionColor.getBlue() / 255f,
+ AppPreferences.haloOverlayOpacity.get() / 255f);
+ areaRenderer.setColor(tmpColor);
+ areaRenderer.fillArea(batch, visible);
+ }
+ }
+
+ private void renderRenderables() {
+ for (ItemRenderer renderer : itemRenderList) {
+ renderer.render(cam, zoom);
+ }
+ }
+
+ private void renderFog(PlayerView view) {
+ Area combined = null;
+
+ timer.start("renderFog");
+
+ batch.flush();
+
+ backBuffer.begin();
+ ScreenUtils.clear(Color.CLEAR);
+
+ batch.setBlendFunction(GL20.GL_ONE, GL20.GL_NONE);
+ setProjectionMatrix(cam.combined);
+
+ timer.start("renderFog-allocateBufferedImage");
+ timer.stop("renderFog-allocateBufferedImage");
+
+ timer.start("renderFog-fill");
+
+ // Fill
+ batch.setColor(Color.WHITE);
+ var paint = zoneCache.getZone().getFogPaint();
+ var fogPaint = zoneCache.getPaint(paint);
+ var fogColor = fogPaint.color();
+ fogPaint.color().set(fogColor.r, fogColor.g, fogColor.b, view.isGMView() ? .6f : 1f);
+ fillViewportWith(fogPaint);
+
+ var zoneView = zoneCache.getZoneView();
+
+ timer.start("renderFog-visibleArea");
+ Area visibleArea = zoneView.getVisibleArea(view);
+ timer.stop("renderFog-visibleArea");
+
+ String msg = null;
+ if (timer.isEnabled()) {
+ msg = "renderFog-combined(" + (view.isUsingTokenView() ? view.getTokens().size() : 0) + ")";
+ }
+ timer.start(msg);
+ combined = zoneView.getExposedArea(view);
+ timer.stop(msg);
+
+ timer.start("renderFogArea");
+ areaRenderer.setColor(Color.CLEAR);
+ areaRenderer.fillArea(batch, combined);
+ // renderFogArea(combined, visibleArea);
+ renderFogOutline();
+ timer.stop("renderFogArea");
+
+ batch.flush();
+ // createScreenshot("fog");
+
+ backBuffer.end();
+
+ batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
+ setProjectionMatrix(hudCam.combined);
+ batch.setColor(Color.WHITE);
+ batch.draw(backBuffer.getColorBufferTexture(), 0, 0, width, height, 0, 0, 1, 1);
+
+ setProjectionMatrix(cam.combined);
+ timer.stop("renderFog");
+ }
+
+ private void setProjectionMatrix(Matrix4 matrix) {
+ batch.setProjectionMatrix(matrix);
+ drawer.update();
+ }
+
+ private void renderFogArea(Area softFog, Area visibleArea) {
+ if (zoneCache.getZoneView().isUsingVision()) {
+ if (visibleArea != null && !visibleArea.isEmpty()) {
+ tmpColor.set(0, 0, 0, AppPreferences.fogOverlayOpacity.get() / 255.0f);
+ areaRenderer.setColor(tmpColor);
+ // Fill in the exposed area
+ areaRenderer.fillArea(batch, softFog);
+
+ areaRenderer.setColor(Color.CLEAR);
+
+ visibleArea.intersect(softFog);
+
+ areaRenderer.fillArea(batch, visibleArea);
+ } else {
+ tmpColor.set(0, 0, 0, AppPreferences.fogOverlayOpacity.get() / 255.0f);
+ areaRenderer.setColor(tmpColor);
+ areaRenderer.fillArea(batch, softFog);
+ }
+ } else {
+ areaRenderer.setColor(Color.CLEAR);
+ areaRenderer.fillArea(batch, softFog);
+ }
+ }
+
+ private void renderFogOutline() {
+ if (visibleScreenArea == null) return;
+
+ areaRenderer.setColor(Color.BLACK);
+ areaRenderer.drawArea(batch, visibleScreenArea, false, 1);
+ }
+
+ private void renderLabels(PlayerView view) {
+ timer.start("labels-1");
+
+ for (Label label : zoneCache.getZone().getLabels()) {
+ timer.start("labels-1.1");
+ Color.argb8888ToColor(tmpColor, label.getForegroundColor().getRGB());
+ if (label.isShowBackground()) {
+ textRenderer.drawBoxedString(
+ label.getLabel(),
+ label.getX(),
+ -label.getY(),
+ SwingUtilities.CENTER,
+ TextRenderer.Background.Gray,
+ tmpColor);
+ } else {
+ textRenderer.drawString(label.getLabel(), label.getX(), -label.getY(), tmpColor);
+ }
+ timer.stop("labels-1.1");
+ }
+ timer.stop("labels-1");
+ }
+
+ private void showBlockedMoves(PlayerView view, Set movementSet) {
+ var selectionSetMap = zoneCache.getZoneRenderer().getSelectionSetMap();
+ if (selectionSetMap.isEmpty()) {
+ return;
+ }
+
+ boolean clipInstalled = false;
+ for (SelectionSet set : movementSet) {
+ Token keyToken = zoneCache.getZone().getToken(set.getKeyToken());
+ if (keyToken == null) {
+ // It was removed ?
+ selectionSetMap.remove(set.getKeyToken());
+ continue;
+ }
+ // Hide the hidden layer
+ if (keyToken.getLayer() == Zone.Layer.GM && !view.isGMView()) {
+ continue;
+ }
+ ZoneWalker walker = set.getWalker();
+
+ for (GUID tokenGUID : set.getTokens()) {
+ Token token = zoneCache.getZone().getToken(tokenGUID);
+
+ // Perhaps deleted?
+ if (token == null) {
+ continue;
+ }
+
+ // Don't bother if it's not visible
+ if (!token.isVisible() && !view.isGMView()) {
+ continue;
+ }
+
+ // ... or if it's visible only to the owner and that's not us!
+ if (token.isVisibleOnlyToOwner() && !AppUtil.playerOwns(token)) {
+ continue;
+ }
+
+ // ... or there are no lights/visibleScreen and you are not the owner or gm and there is fow
+ // or vision
+ if (!view.isGMView()
+ && !AppUtil.playerOwns(token)
+ && visibleScreenArea == null
+ && zoneCache.getZone().hasFog()
+ && zoneCache.getZoneView().isUsingVision()) {
+ continue;
+ }
+
+ // ... or if it doesn't have an image to display. (Hm, should still show *something*?)
+ Asset asset = AssetManager.getAsset(token.getImageAssetId());
+ if (asset == null) {
+ continue;
+ }
+
+ // OPTIMIZE: combine this with the code in renderTokens()
+ java.awt.Rectangle footprintBounds = token.getBounds(zoneCache.getZone());
+
+ // get token image, using image table if present
+ Sprite image = zoneCache.getSprite(token.getImageAssetId(), stateTime);
+ if (image == null) continue;
+
+ // Vision visibility
+ boolean isOwner = view.isGMView() || AppUtil.playerOwns(token); // ||
+ // set.getPlayerId().equals(MapTool.getPlayer().getName());
+ if (!view.isGMView() && visibleScreenArea != null && !isOwner) {
+ // FJE Um, why not just assign the clipping area at the top of the routine?
+ // TODO: Path clipping
+ if (!clipInstalled) {
+ // Only show the part of the path that is visible
+ // Area visibleArea = new Area(g.getClipBounds());
+ // visibleArea.intersect(visibleScreenArea);
+
+ // g = (Graphics2D) g.create();
+ // g.setClip(new GeneralPath(visibleArea));
+
+ clipInstalled = true;
+ // System.out.println("Adding Clip: " + MapTool.getPlayer().getName());
+ }
+ }
+ // Show path only on the key token on token layer that are visible to the owner or gm while
+ // fow and vision is on
+ if (token == keyToken && token.getLayer().supportsWalker()) {
+ renderPath(
+ walker != null ? walker.getPath() : set.getGridlessPath(),
+ token.getFootprint(zoneCache.getZone().getGrid()));
+ }
+
+ // Show current Blocked Movement directions for A*
+ if (walker != null && (log.isDebugEnabled() || showAstarDebugging)) {
+ Map> blockedMovesByTarget = walker.getBlockedMoves();
+ // Color currentColor = g.getColor();
+ for (var entry : blockedMovesByTarget.entrySet()) {
+ var position = entry.getKey();
+ var blockedMoves = entry.getValue();
+
+ for (CellPoint point : blockedMoves) {
+ ZonePoint zp =
+ point.midZonePoint(zoneCache.getZoneRenderer().getZone().getGrid(), position);
+ double r = (zp.x - 1) * 45;
+ showBlockedMoves(zp, r, zoneCache.getSprite("block_move"), 1.0f);
+ }
+ }
+ }
+
+ footprintBounds.x += set.getOffsetX();
+ footprintBounds.y += set.getOffsetY();
+
+ prepareTokenSprite(image, token, footprintBounds);
+ image.draw(batch);
+
+ // Other details
+ if (token == keyToken) {
+ var x = footprintBounds.x;
+ var y = footprintBounds.y;
+ var w = footprintBounds.width;
+ var h = footprintBounds.height;
+
+ Grid grid = zoneCache.getZone().getGrid();
+ boolean checkForFog =
+ MapTool.getServerPolicy().isUseIndividualFOW()
+ && zoneCache.getZoneView().isUsingVision();
+ boolean showLabels = isOwner;
+ if (checkForFog) {
+ Path extends AbstractPoint> path =
+ set.getWalker() != null ? set.getWalker().getPath() : set.getGridlessPath();
+ List extends AbstractPoint> thePoints = path.getCellPath();
+
+ // now that we have the last point, we can check to see if it's gridless or not. If not
+ // gridless, get the last point the token was at and see if the token's footprint is
+ // inside
+ // the visible area to show the label.
+
+ if (thePoints.isEmpty()) {
+ showLabels = false;
+ } else {
+ AbstractPoint lastPoint = thePoints.get(thePoints.size() - 1);
+
+ java.awt.Rectangle tokenRectangle = null;
+ if (lastPoint instanceof CellPoint) {
+ tokenRectangle = token.getFootprint(grid).getBounds(grid, (CellPoint) lastPoint);
+ } else {
+ java.awt.Rectangle tokBounds = token.getBounds(zoneCache.getZone());
+ tokenRectangle = new java.awt.Rectangle();
+ tokenRectangle.setBounds(
+ lastPoint.x,
+ lastPoint.y,
+ (int) tokBounds.getWidth(),
+ (int) tokBounds.getHeight());
+ }
+ showLabels =
+ showLabels
+ || zoneCache
+ .getZoneRenderer()
+ .getZoneView()
+ .getVisibleArea(view)
+ .intersects(tokenRectangle);
+ }
+ } else {
+ boolean hasFog = zoneCache.getZone().hasFog();
+ boolean fogIntersects = exposedFogArea.intersects(footprintBounds);
+ showLabels = showLabels || (visibleScreenArea == null && !hasFog); // no vision - fog
+ showLabels =
+ showLabels
+ || (visibleScreenArea == null && hasFog && fogIntersects); // no vision + fog
+ showLabels =
+ showLabels
+ || (visibleScreenArea != null
+ && visibleScreenArea.intersects(footprintBounds)
+ && fogIntersects); // vision
+ }
+ if (showLabels) {
+
+ y += 10 + h;
+ x += w / 2;
+
+ if (token.getLayer().supportsWalker() && AppState.getShowMovementMeasurements()) {
+ String distance = "";
+ if (walker != null) { // This wouldn't be true unless token.isSnapToGrid() &&
+ // grid.isPathingSupported()
+ double distanceTraveled = walker.getDistance();
+ if (distanceTraveled >= 0) {
+ distance = NumberFormat.getInstance().format(distanceTraveled);
+ }
+ } else {
+ double c = 0;
+ ZonePoint lastPoint = null;
+ for (ZonePoint zp : set.getGridlessPath().getCellPath()) {
+ if (lastPoint == null) {
+ lastPoint = zp;
+ continue;
+ }
+ int a = lastPoint.x - zp.x;
+ int b = lastPoint.y - zp.y;
+ c += Math.hypot(a, b);
+ lastPoint = zp;
+ }
+ c /= zoneCache.getZone().getGrid().getSize(); // Number of "cells"
+ c *= zoneCache.getZone().getUnitsPerCell(); // "actual" distance traveled
+ distance = NumberFormat.getInstance().format(c);
+ }
+ if (!distance.isEmpty()) {
+ itemRenderList.add(new LabelRenderer(distance, x, -y, textRenderer));
+ y += 20;
+ }
+ }
+ if (set.getPlayerId() != null && set.getPlayerId().length() >= 1) {
+ itemRenderList.add(new LabelRenderer(set.getPlayerId(), x, -y, textRenderer));
+ }
+ } // showLabels
+ } // token == keyToken
+ }
+ }
+ }
+
+ private void showBlockedMoves(ZonePoint zp, double angle, Sprite image, float size) {
+ // Resize image to size of 1/4 size of grid
+ var resizeWidth =
+ (float) zoneCache.getZone().getGrid().getCellWidth() / image.getWidth() * .25f;
+ var resizeHeight =
+ (float) zoneCache.getZone().getGrid().getCellHeight() / image.getHeight() * .25f;
+
+ var w = image.getWidth() * resizeWidth * size;
+ var h = image.getHeight() * resizeHeight * size;
+
+ image.setSize(w, h);
+ image.setPosition(zp.x - w / 2f, -(zp.y - h / 2f));
+ image.draw(batch);
+ }
+
+ private void renderAuras(PlayerView view) {
+ var alpha = AppPreferences.auraOverlayOpacity.get() / 255.0f;
+
+ // Setup
+ timer.start("renderAuras:getAuras");
+ final var drawableAuras = zoneCache.getZoneView().getDrawableAuras(view);
+ timer.stop("renderAuras:getAuras");
+
+ timer.start("renderAuras:renderAuraOverlay");
+ renderLightOverlay(drawableAuras, alpha, GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
+ timer.stop("renderAuras:renderAuraOverlay");
+ }
+
+ private void renderLights(PlayerView view) {
+ // Collect and organize lights
+ timer.start("renderLights:getLights");
+ final var drawableLights = zoneCache.getZoneView().getDrawableLights(view);
+ timer.stop("renderLights:getLights");
+
+ if (AppState.isShowLights()
+ && zoneCache.getZone().getLightingStyle() != Zone.LightingStyle.ENVIRONMENTAL) {
+ // Lighting enabled.
+ timer.start("renderLights:renderLightOverlay");
+ // zoneCache.getZone().getLightingStyle() is not supported currently as you would probably
+ // need a custom shader, reusing it for box2dlights
+
+ renderLightOverlay(
+ drawableLights,
+ AppPreferences.lightOverlayOpacity.get() / 255.f,
+ GL20.GL_SRC_COLOR,
+ GL20.GL_ONE_MINUS_SRC_COLOR);
+ timer.stop("renderLights:renderLightOverlay");
+ }
+
+ if (AppState.isShowLumensOverlay()) {
+ // Lumens overlay enabled.
+ timer.start("renderLights:renderLumensOverlay");
+ renderLumensOverlay(view, AppPreferences.lumensOverlayOpacity.get() / 255.0f);
+ timer.stop("renderLights:renderLumensOverlay");
+ }
+ }
+
+ private void renderLumensOverlay(PlayerView view, float overlayAlpha) {
+ final var disjointLumensLevels = zoneCache.getZoneView().getDisjointObscuredLumensLevels(view);
+
+ timer.start("renderLumensOverlay:allocateBuffer");
+ batch.flush();
+ backBuffer.begin();
+ timer.stop("renderLumensOverlay:allocateBuffer");
+
+ batch.setBlendFunction(GL20.GL_ONE, GL20.GL_NONE);
+ var A_d = overlayAlpha;
+ // At night, show any uncovered areas as dark. In daylight, show them as light (clear).
+ if (zoneCache.getZone().getVisionType() == Zone.VisionType.NIGHT) {
+ ScreenUtils.clear(0, 0, 0, overlayAlpha);
+ } else {
+ A_d = 0;
+ ScreenUtils.clear(Color.CLEAR);
+ }
+
+ batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
+ timer.start("renderLumensOverlay:drawLumens");
+ for (final var lumensLevel : disjointLumensLevels) {
+ final var lumensStrength = lumensLevel.lumensStrength();
+
+ // Light is weaker than darkness, so do it first.
+ float lightOpacity;
+ float lightShade;
+ if (lumensStrength == 0) {
+ // This area represents daylight, so draw it as clear despite the low value.
+ lightShade = 1.f;
+ lightOpacity = 0;
+ } else if (lumensStrength >= 100) {
+ // Bright light, render mostly clear.
+ lightShade = 1.f;
+ lightOpacity = 1.f / 10.f;
+ } else {
+ lightShade = Math.max(0.f, Math.min(lumensStrength / 100.f, 1.f));
+ lightShade *= lightShade;
+ lightOpacity = 1.f;
+ }
+
+ timer.start("renderLumensOverlay:drawLights:fillArea");
+
+ // for SRC_OVER on transparent destination we need GL_ONE, GL_ONE_MINUS_SRC_ALPHA
+ // we have to render the fbo with the same blending function to the screen not with
+ // GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA !
+ // See https://apoorvaj.io/alpha-compositing-opengl-blending-and-premultiplied-alpha/
+
+ lightOpacity *= overlayAlpha;
+ lightShade *= lightOpacity;
+ areaRenderer.setColor(tmpColor.set(lightShade, lightShade, lightShade, lightOpacity));
+ areaRenderer.fillArea(batch, lumensLevel.lightArea());
+
+ areaRenderer.setColor(tmpColor.set(0.f, 0.f, 0.f, overlayAlpha));
+ areaRenderer.fillArea(batch, lumensLevel.darknessArea());
+ timer.stop("renderLumensOverlay:drawLights:fillArea");
+ }
+
+ timer.stop("renderLumensOverlay:drawLumens");
+ batch.flush();
+ // createScreenshot("lumens");
+ backBuffer.end();
+
+ timer.start("renderLumensOverlay:drawBuffer");
+ // batch.setColor(1,1,1,overlayAlpha);
+ batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
+ setProjectionMatrix(hudCam.combined);
+ batch.draw(backBuffer.getColorBufferTexture(), 0, 0, width, height, 0, 0, 1, 1);
+ setProjectionMatrix(cam.combined);
+ timer.stop("renderLumensOverlay:drawBuffer");
+ batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
+ // Now draw borders around each region if configured.
+ batch.setColor(Color.WHITE);
+ final var borderThickness = AppPreferences.lumensOverlayBorderThickness.get();
+ if (borderThickness > 0) {
+ tmpColor.set(0.f, 0.f, 0.f, 1.f);
+ for (final var lumensLevel : disjointLumensLevels) {
+ timer.start("renderLumensOverlay:drawLights:drawArea");
+ areaRenderer.setColor(tmpColor);
+ areaRenderer.drawArea(batch, lumensLevel.lightArea(), true, borderThickness);
+ areaRenderer.setColor(tmpColor);
+ areaRenderer.drawArea(batch, lumensLevel.darknessArea(), true, borderThickness);
+ timer.stop("renderLumensOverlay:drawLights:drawArea");
+ }
+ }
+ }
+
+ private void renderLightOverlay(
+ Collection lights, float alpha, int srcBlendFunc, int dstBlendFunc) {
+ if (lights.isEmpty()) {
+ // No points spending resources accomplishing nothing.
+ return;
+ }
+
+ // Set up a buffer image for lights to be drawn onto before the map
+ timer.start("renderLightOverlay:allocateBuffer");
+ batch.flush();
+ backBuffer.begin();
+
+ ScreenUtils.clear(Color.CLEAR);
+ setProjectionMatrix(cam.combined);
+ batch.setBlendFunctionSeparate(srcBlendFunc, dstBlendFunc, GL20.GL_ONE, GL20.GL_NONE);
+ timer.stop("renderLightOverlay:allocateBuffer");
+
+ // Draw lights onto the buffer image so the map doesn't affect how they blend
+ timer.start("renderLightOverlay:drawLights");
+ for (var light : lights) {
+ var paint = light.getPaint().getPaint();
+
+ if (paint instanceof DrawableColorPaint) {
+ var colorPaint = (DrawableColorPaint) paint;
+ Color.argb8888ToColor(tmpColor, colorPaint.getColor());
+
+ } else if (paint instanceof java.awt.Color) {
+ Color.argb8888ToColor(tmpColor, ((java.awt.Color) paint).getRGB());
+ } else {
+ System.out.println("unexpected color type");
+ continue;
+ }
+ tmpColor.set(tmpColor.r, tmpColor.g, tmpColor.b, alpha);
+ areaRenderer.setColor(tmpColor);
+ areaRenderer.fillArea(batch, light.getArea());
+ }
+
+ batch.flush();
+ backBuffer.end();
+ timer.stop("renderLightOverlay:drawLights");
+
+ // Draw the buffer image with all the lights onto the map
+ timer.start("renderLightOverlay:drawBuffer");
+ batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
+ setProjectionMatrix(hudCam.combined);
+ batch.draw(backBuffer.getColorBufferTexture(), 0, 0, width, height, 0, 0, 1, 1);
+ setProjectionMatrix(cam.combined);
+ // batch.setColor(Color.WHITE);
+ timer.stop("renderLightOverlay:drawBuffer");
+ }
+
+ private void createScreenshot(String name) {
+ var file = Gdx.files.absolute("C:\\Users\\tkunze\\OneDrive\\Desktop\\" + name + ".png");
+ if (!file.exists()) {
+ Pixmap pixmap = Pixmap.createFromFrameBuffer(0, 0, width, height);
+ PixmapIO.writePNG(file, pixmap, Deflater.DEFAULT_COMPRESSION, true);
+ pixmap.dispose();
+ }
+ }
+
+ private void renderBoard() {
+ if (!zoneCache.getZone().drawBoard()) return;
+
+ var paint = zoneCache.getZone().getBackgroundPaint();
+ fillViewportWith(zoneCache.getPaint(paint));
+
+ var map = zoneCache.getSprite(zoneCache.getZone().getMapAssetId(), stateTime);
+ if (map != null) {
+ map.setPosition(
+ zoneCache.getZone().getBoardX(), zoneCache.getZone().getBoardY() - map.getHeight());
+ map.draw(batch);
+ }
+ }
+
+ private void fillViewportWith(ZoneCache.GdxPaint paint) {
+ var w = cam.viewportWidth * zoom;
+ var h = cam.viewportHeight * zoom;
+ var startX = (cam.position.x - cam.viewportWidth * zoom / 2);
+
+ var startY = (cam.position.y - cam.viewportHeight * zoom / 2);
+ var vertices =
+ new float[] {
+ startX, startY, startX, startY + h, startX + w, startY + h, startX + w, startY
+ };
+
+ var indices = new short[] {1, 0, 3, 3, 2, 1};
+
+ var polySprite = new PolygonSprite(new PolygonRegion(paint.textureRegion(), vertices, indices));
+ polySprite.setColor(paint.color());
+ polySprite.draw(batch);
+ }
+
+ private void renderTokens(List tokenList, PlayerView view, boolean figuresOnly) {
+ boolean isGMView = view.isGMView(); // speed things up
+
+ if (visibleScreenArea == null) return;
+
+ for (Token token : tokenList) {
+ if (token.getShape() != Token.TokenShape.FIGURE && figuresOnly && !token.isAlwaysVisible()) {
+ continue;
+ }
+
+ timer.start("tokenlist-1");
+ try {
+ if (token.getLayer().isStampLayer() && zoneCache.getZoneRenderer().isTokenMoving(token)) {
+ continue;
+ }
+ // Don't bother if it's not visible
+ // NOTE: Not going to use zoneCache.getZone().isTokenVisible as it is very slow. In fact,
+ // it's faster
+ // to just draw the tokens and let them be clipped
+ if ((!token.isVisible() || !token.getLayer().isVisibleToPlayers()) && !isGMView) {
+ continue;
+ }
+ if (token.isVisibleOnlyToOwner() && !AppUtil.playerOwns(token)) {
+ continue;
+ }
+ } finally {
+ // This ensures that the timer is always stopped
+ timer.stop("tokenlist-1");
+ }
+
+ java.awt.Rectangle footprintBounds = token.getBounds(zoneCache.getZone());
+ java.awt.Rectangle origBounds = (java.awt.Rectangle) footprintBounds.clone();
+ Area tokenBounds = new Area(footprintBounds);
+
+ timer.start("tokenlist-1d");
+ if (token.hasFacing() && token.getShape() == Token.TokenShape.TOP_DOWN) {
+ double sx = footprintBounds.width / 2f + footprintBounds.x - (token.getAnchor().x);
+ double sy = footprintBounds.height / 2f + footprintBounds.y - (token.getAnchor().y);
+ tokenBounds.transform(
+ AffineTransform.getRotateInstance(
+ Math.toRadians(-token.getFacing() - 90), sx, sy)); // facing
+ // defaults to down, or -90 degrees
+ }
+ timer.stop("tokenlist-1d");
+
+ timer.start("tokenlist-1e");
+ try {
+
+ // Vision visibility
+ if (!isGMView
+ && token.getLayer().supportsVision()
+ && zoneCache.getZoneView().isUsingVision()) {
+ if (!GraphicsUtil.intersects(visibleScreenArea, tokenBounds)) {
+ continue;
+ }
+ }
+ } finally {
+ // This ensures that the timer is always stopped
+ timer.stop("tokenlist-1e");
+ }
+
+ // Previous path
+ timer.start("renderTokens:ShowPath");
+ if (zoneCache.getZoneRenderer().getShowPathList().contains(token)
+ && token.getLastPath() != null) {
+ renderPath(token.getLastPath(), token.getFootprint(zoneCache.getZone().getGrid()));
+ }
+ timer.stop("renderTokens:ShowPath");
+
+ // get token image sprite, using image table if present
+ var imageKey = token.getTokenImageAssetId();
+ Sprite image = zoneCache.getSprite(imageKey, stateTime);
+
+ prepareTokenSprite(image, token, footprintBounds);
+
+ // Render Halo
+ if (token.hasHalo()) {
+ Color.argb8888ToColor(tmpColor, token.getHaloColor().getRGB());
+ areaRenderer.setColor(tmpColor);
+ areaRenderer.drawArea(
+ batch,
+ zoneCache.getZone().getGrid().getTokenCellArea(tokenBounds),
+ false,
+ AppPreferences.haloLineWidth.get());
+ }
+
+ // Calculate alpha Transparency from token and use opacity for indicating that token is moving
+ float opacity = token.getTokenOpacity();
+ if (zoneCache.getZoneRenderer().isTokenMoving(token)) opacity = opacity / 2.0f;
+
+ Area tokenCellArea = zoneCache.getZone().getGrid().getTokenCellArea(tokenBounds);
+ Area cellArea = new Area(visibleScreenArea);
+ cellArea.intersect(tokenCellArea);
+
+ // Finally render the token image
+ timer.start("tokenlist-7");
+ image.setColor(1, 1, 1, opacity);
+ if (!isGMView
+ && zoneCache.getZoneView().isUsingVision()
+ && (token.getShape() == Token.TokenShape.FIGURE)) {
+ if (zoneCache
+ .getZone()
+ .getGrid()
+ .checkCenterRegion(tokenCellArea.getBounds(), visibleScreenArea)) {
+ // if we can see the centre, draw the whole token
+ image.draw(batch);
+ } else {
+ // else draw the clipped token
+ paintClipped(image, tokenCellArea, cellArea);
+ }
+ } else if (!isGMView && zoneCache.getZoneView().isUsingVision() && token.isAlwaysVisible()) {
+ // Jamz: Always Visible tokens will get rendered again here to place on top of FoW
+ if (GraphicsUtil.intersects(visibleScreenArea, tokenCellArea)) {
+ // if we can see a portion of the stamp/token, draw the whole thing, defaults to 2/9ths
+ if (zoneCache
+ .getZone()
+ .getGrid()
+ .checkRegion(
+ tokenCellArea.getBounds(),
+ visibleScreenArea,
+ token.getAlwaysVisibleTolerance())) {
+
+ image.draw(batch);
+
+ } else {
+ // else draw the clipped stamp/token
+ // This will only show the part of the token that does not have VBL on it
+ // as any VBL on the token will block LOS, affecting the clipping.
+ paintClipped(image, tokenCellArea, cellArea);
+ }
+ }
+ } else {
+ // fallthrough normal token rendered against visible area
+
+ if (zoneCache.getZoneRenderer().isTokenInNeedOfClipping(token, tokenCellArea, isGMView)) {
+ paintClipped(image, tokenCellArea, cellArea);
+ } else image.draw(batch);
+ }
+ image.setColor(Color.WHITE);
+ timer.stop("tokenlist-7");
+
+ timer.start("tokenlist-8");
+
+ // Facing
+ if (token.hasFacing()) {
+ Token.TokenShape tokenType = token.getShape();
+ switch (tokenType) {
+ case FIGURE:
+ if (token.getHasImageTable()
+ && token.hasFacing()
+ && AppPreferences.forceFacingArrow.get() == false) {
+ break;
+ }
+ java.awt.Shape arrow =
+ getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2);
+
+ if (!zoneCache.getZone().getGrid().isIsometric()) {
+ arrow = getCircleFacingArrow(token.getFacing(), footprintBounds.width / 2);
+ }
+
+ float fx = origBounds.x + origBounds.width / zoom / 2f;
+ float fy = origBounds.y + origBounds.height / zoom / 2f;
+
+ tmpMatrix.idt();
+ tmpMatrix.translate(fx, -fy, 0);
+ batch.setTransformMatrix(tmpMatrix);
+ drawer.update();
+
+ if (token.getFacing() < 0) {
+ tmpColor.set(Color.YELLOW);
+ } else {
+ tmpColor.set(1, 1, 0, 0.5f);
+ }
+
+ var arrowArea = new Area(arrow);
+ areaRenderer.setColor(tmpColor);
+ areaRenderer.fillArea(batch, arrowArea);
+
+ areaRenderer.setColor(Color.DARK_GRAY);
+ areaRenderer.drawArea(batch, arrowArea, false, 1);
+
+ break;
+ case TOP_DOWN:
+ if (AppPreferences.forceFacingArrow.get() == false) {
+ break;
+ }
+ case CIRCLE:
+ arrow = getCircleFacingArrow(token.getFacing(), footprintBounds.width / 2);
+ if (zoneCache.getZone().getGrid().isIsometric()) {
+ arrow = getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2);
+ }
+ arrowArea = new Area(arrow);
+
+ float cx = origBounds.x + origBounds.width / 2f;
+ float cy = origBounds.y + origBounds.height / 2f;
+
+ tmpMatrix.idt();
+ tmpMatrix.translate(cx, -cy, 0);
+ batch.setTransformMatrix(tmpMatrix);
+
+ areaRenderer.setColor(Color.YELLOW);
+ areaRenderer.fillArea(batch, arrowArea);
+ areaRenderer.setColor(Color.DARK_GRAY);
+ areaRenderer.drawArea(batch, arrowArea, false, 1);
+ tmpMatrix.idt();
+ batch.setTransformMatrix(tmpMatrix);
+ break;
+ case SQUARE:
+ if (zoneCache.getZone().getGrid().isIsometric()) {
+ arrow = getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2);
+ cx = origBounds.x + origBounds.width / 2f;
+ cy = origBounds.y + origBounds.height / 2f;
+ } else {
+ int facing = token.getFacing();
+ arrow = getSquareFacingArrow(facing, footprintBounds.width / 2);
+
+ cx = origBounds.x + origBounds.width / 2f;
+ cy = origBounds.y + origBounds.height / 2f;
+
+ // Find the edge of the image
+ double xp = origBounds.getWidth() / 2;
+ double yp = origBounds.getHeight() / 2;
+ if (facing >= 45 && facing <= 135 || facing >= 225 && facing <= 315) {
+ xp = (int) (yp / Math.tan(Math.toRadians(facing)));
+ if (facing > 180) {
+ xp = -xp;
+ yp = -yp;
+ }
+ } else {
+ yp = (int) (xp * Math.tan(Math.toRadians(facing)));
+ if (facing > 90 && facing < 270) {
+ xp = -xp;
+ yp = -yp;
+ }
+ }
+ cx += xp;
+ cy -= yp;
+ }
+
+ arrowArea = new Area(arrow);
+
+ tmpMatrix.translate(cx, -cy, 0);
+ batch.setTransformMatrix(tmpMatrix);
+ areaRenderer.setColor(Color.YELLOW);
+
+ areaRenderer.fillArea(batch, arrowArea);
+ areaRenderer.setColor(Color.DARK_GRAY);
+ areaRenderer.drawArea(batch, arrowArea, false, 1);
+ batch.setTransformMatrix(tmpMatrix.idt());
+ break;
+ }
+ }
+ timer.stop("tokenlist-8");
+
+ timer.start("tokenlist-9");
+
+ // Check each of the set values
+ for (String state : MapTool.getCampaign().getTokenStatesMap().keySet()) {
+ Object stateValue = token.getState(state);
+ AbstractTokenOverlay overlay = MapTool.getCampaign().getTokenStatesMap().get(state);
+ if (stateValue instanceof AbstractTokenOverlay) {
+ overlay = (AbstractTokenOverlay) stateValue;
+ }
+ if (overlay == null
+ || overlay.isMouseover() && token != zoneCache.getZoneRenderer().getTokenUnderMouse()
+ || !overlay.showPlayer(token, MapTool.getPlayer())) {
+ continue;
+ }
+ tokenOverlayRenderer.render(stateTime, overlay, token, stateValue);
+ }
+ timer.stop("tokenlist-9");
+
+ timer.start("tokenlist-10");
+
+ for (String bar : MapTool.getCampaign().getTokenBarsMap().keySet()) {
+ Object barValue = token.getState(bar);
+ BarTokenOverlay overlay = MapTool.getCampaign().getTokenBarsMap().get(bar);
+ if (overlay == null
+ || overlay.isMouseover() && token != zoneCache.getZoneRenderer().getTokenUnderMouse()
+ || !overlay.showPlayer(token, MapTool.getPlayer())) {
+ continue;
+ }
+ tokenOverlayRenderer.render(stateTime, overlay, token, barValue);
+ } // endfor
+ timer.stop("tokenlist-10");
+
+ timer.start("tokenlist-11");
+ // Keep track of which tokens have been drawn so we can perform post-processing on them later
+ // (such as selection borders and names/labels)
+ if (!zoneCache.getZoneRenderer().getActiveLayer().equals(token.getLayer())) continue;
+
+ timer.stop("tokenlist-11");
+ timer.start("tokenlist-12");
+
+ boolean useIF = MapTool.getServerPolicy().isUseIndividualFOW();
+
+ // Selection and labels
+
+ var tokenRectangle = token.getBounds(zoneCache.getZone());
+ var gdxTokenRectangle =
+ new Rectangle(
+ tokenRectangle.x,
+ -tokenRectangle.y - tokenRectangle.height,
+ tokenRectangle.width,
+ tokenRectangle.height);
+ boolean isSelected =
+ zoneCache.getZoneRenderer().getSelectedTokenSet().contains(token.getId());
+ if (isSelected) {
+ ImageBorder selectedBorder =
+ token.getLayer().isStampLayer()
+ ? AppStyle.selectedStampBorder
+ : AppStyle.selectedBorder;
+ if (zoneCache.getZoneRenderer().getHighlightCommonMacros().contains(token)) {
+ selectedBorder = AppStyle.commonMacroBorder;
+ }
+ if (!AppUtil.playerOwns(token)) {
+ selectedBorder = AppStyle.selectedUnownedBorder;
+ }
+ if (useIF && token.getLayer().supportsVision() && zoneCache.getZoneView().isUsingVision()) {
+ Tool tool = MapTool.getFrame().getToolbox().getSelectedTool();
+ if (tool instanceof WallTopologyTool) {
+ selectedBorder = RessourceManager.getBorder(Borders.FOW_TOOLS);
+ }
+ }
+
+ setProjectionMatrix(hudCam.combined);
+ tmpWorldCoord.set(gdxTokenRectangle.x, gdxTokenRectangle.y, 0);
+ cam.project(tmpWorldCoord);
+
+ gdxTokenRectangle.set(
+ tmpWorldCoord.x,
+ tmpWorldCoord.y,
+ gdxTokenRectangle.width / zoom,
+ gdxTokenRectangle.height / zoom);
+
+ if (token.hasFacing()
+ && (token.getShape() == Token.TokenShape.TOP_DOWN || token.getLayer().isStampLayer())) {
+
+ var transX = gdxTokenRectangle.width / 2f - token.getAnchor().x / zoom;
+ var transY = gdxTokenRectangle.height / 2f + token.getAnchor().y / zoom;
+
+ tmpMatrix.idt();
+ tmpMatrix.translate(tmpWorldCoord.x + transX, tmpWorldCoord.y + transY, 0);
+ tmpMatrix.rotate(0, 0, 1, token.getFacing() + 90);
+ tmpMatrix.translate(-transX, -transY, 0);
+ gdxTokenRectangle.x = 0;
+ gdxTokenRectangle.y = 0;
+ batch.setTransformMatrix(tmpMatrix);
+ renderImageBorderAround(selectedBorder, gdxTokenRectangle);
+ tmpMatrix.idt();
+ batch.setTransformMatrix(tmpMatrix);
+
+ } else {
+ renderImageBorderAround(selectedBorder, gdxTokenRectangle);
+ }
+
+ setProjectionMatrix(cam.combined);
+ }
+
+ // Token names and labels
+ boolean showCurrentTokenLabel =
+ AppState.isShowTokenNames() || token == zoneCache.getZoneRenderer().getTokenUnderMouse();
+
+ // if policy does not auto-reveal FoW, check if fog covers the token (slow)
+ if (showCurrentTokenLabel
+ && !isGMView
+ && (!zoneCache.getZoneView().isUsingVision()
+ || !MapTool.getServerPolicy().isAutoRevealOnMovement())
+ && !zoneCache.getZone().isTokenVisible(token)) {
+ showCurrentTokenLabel = false;
+ }
+ if (showCurrentTokenLabel) {
+ itemRenderList.add(
+ new TokenLabelRenderer(token, zoneCache.getZone(), isGMView, textRenderer));
+ }
+ timer.stop("tokenlist-12");
+ }
+
+ timer.start("tokenlist-13");
+
+ var tokenStackMap = zoneCache.getZoneRenderer().getTokenStackMap();
+
+ // Stacks
+ // TODO: find a cleaner way to indicate token layer
+ if (!tokenList.isEmpty() && tokenList.get(0).getLayer().isTokenLayer()) {
+ boolean hideTSI = AppPreferences.hideTokenStackIndicator.get();
+ if (tokenStackMap != null
+ && !hideTSI) { // FIXME Needed to prevent NPE but how can it be null?
+ for (Token token : tokenStackMap.keySet()) {
+ var tokenRectangle = token.getBounds(zoneCache.getZone());
+ var stackImage = zoneCache.fetch("stack");
+ batch.draw(
+ stackImage,
+ tokenRectangle.x + tokenRectangle.width - stackImage.getRegionWidth() + 2,
+ -tokenRectangle.y - stackImage.getRegionHeight() + 2);
+ }
+ }
+ }
+ timer.stop("tokenlist-13");
+ }
+
+ private void prepareTokenSprite(Sprite image, Token token, java.awt.Rectangle footprintBounds) {
+ image.setRotation(0);
+
+ // Tokens are centered on the image center point
+ float x = footprintBounds.x;
+ float y = footprintBounds.y;
+
+ timer.start("tokenlist-5");
+
+ // handle flipping
+ image.setFlip(token.isFlippedX(), token.isFlippedY());
+ timer.stop("tokenlist-5");
+
+ image.setOriginCenter();
+
+ timer.start("tokenlist-5a");
+ if (token.isFlippedIso()) {
+ image = zoneCache.getIsoSprite(token.getImageAssetId());
+ token.setHeight((int) image.getHeight());
+ token.setWidth((int) image.getWidth());
+ footprintBounds = token.getBounds(zoneCache.getZone());
+ }
+ timer.stop("tokenlist-5a");
+
+ timer.start("tokenlist-6");
+ // Position
+ // For Isometric Grid we alter the height offset
+ float iso_ho = 0;
+ java.awt.Dimension imgSize =
+ new java.awt.Dimension((int) image.getWidth(), (int) image.getHeight());
+ if (token.getShape() == Token.TokenShape.FIGURE) {
+ float th = token.getHeight() * (float) footprintBounds.width / token.getWidth();
+ iso_ho = footprintBounds.height - th;
+ footprintBounds =
+ new java.awt.Rectangle(
+ footprintBounds.x, footprintBounds.y - (int) iso_ho, footprintBounds.width, (int) th);
+ }
+ SwingUtil.constrainTo(imgSize, footprintBounds.width, footprintBounds.height);
+
+ int offsetx = 0;
+ int offsety = 0;
+ if (token.isSnapToScale()) {
+ offsetx =
+ (int)
+ (imgSize.width < footprintBounds.width
+ ? (footprintBounds.width - imgSize.width) / 2
+ : 0);
+ offsety =
+ (int)
+ (imgSize.height < footprintBounds.height
+ ? (footprintBounds.height - imgSize.height) / 2
+ : 0);
+ }
+ float tx = x + offsetx;
+ float ty = y + offsety + iso_ho;
+
+ // Snap
+ var scaleX = 1f;
+ var scaleY = 1f;
+ if (token.isSnapToScale()) {
+ scaleX = imgSize.width / image.getWidth();
+ scaleY = imgSize.height / image.getHeight();
+ } else {
+ if (token.getShape() == Token.TokenShape.FIGURE) {
+ scaleX = footprintBounds.width / image.getHeight();
+ scaleY = footprintBounds.width / image.getWidth();
+ } else {
+ scaleX = footprintBounds.width / image.getWidth();
+ scaleY = footprintBounds.height / image.getHeight();
+ }
+ }
+ image.setSize(scaleX * image.getWidth(), scaleY * image.getHeight());
+
+ image.setPosition(tx, -image.getHeight() - ty);
+
+ image.setOriginCenter();
+
+ // Rotated
+ if (token.hasFacing() && token.getShape() == Token.TokenShape.TOP_DOWN) {
+ var originX = image.getWidth() / 2 - token.getAnchorX();
+ var originY = image.getHeight() / 2 + token.getAnchorY();
+ image.setOrigin(originX, originY);
+ image.setRotation(token.getFacing() + 90);
+ }
+
+ timer.stop("tokenlist-6");
+ }
+
+ private void renderImageBorderAround(ImageBorder border, Rectangle bounds) {
+ var imagePath = border.getImagePath();
+ var index = imagePath.indexOf("border/");
+ var bordername = imagePath.substring(index);
+
+ var topRight = zoneCache.fetch(bordername + "/tr");
+ var top = zoneCache.fetch(bordername + "/top");
+ var topLeft = zoneCache.fetch(bordername + "/tl");
+ var left = zoneCache.fetch(bordername + "/left");
+ var bottomLeft = zoneCache.fetch(bordername + "/bl");
+ var bottom = zoneCache.fetch(bordername + "/bottom");
+ var bottomRight = zoneCache.fetch(bordername + "/br");
+ var right = zoneCache.fetch(bordername + "/right");
+
+ // x,y is bottom left of the rectangle
+ var leftMargin = border.getLeftMargin();
+ var rightMargin = border.getRightMargin();
+ var topMargin = border.getTopMargin();
+ var bottomMargin = border.getBottomMargin();
+
+ var x = bounds.x - leftMargin;
+ var y = bounds.y - bottomMargin;
+
+ var width = bounds.width + leftMargin + rightMargin;
+ var height = bounds.height + topMargin + bottomMargin;
+
+ // Draw Corners
+
+ batch.draw(
+ bottomLeft,
+ x + leftMargin - bottomLeft.getRegionWidth(),
+ y + topMargin - bottomLeft.getRegionHeight());
+ batch.draw(bottomRight, x + width - rightMargin, y + topMargin - bottomRight.getRegionHeight());
+ batch.draw(topLeft, x + leftMargin - topLeft.getRegionWidth(), y + height - bottomMargin);
+ batch.draw(topRight, x + width - rightMargin, y + height - bottomMargin);
+
+ tmpTile.setRegion(top);
+ tmpTile.draw(
+ batch,
+ x + leftMargin,
+ y + height - bottomMargin,
+ width - leftMargin - rightMargin,
+ top.getRegionHeight());
+
+ tmpTile.setRegion(bottom);
+ tmpTile.draw(
+ batch,
+ x + leftMargin,
+ y + topMargin - bottom.getRegionHeight(),
+ width - leftMargin - rightMargin,
+ bottom.getRegionHeight());
+
+ tmpTile.setRegion(left);
+ tmpTile.draw(
+ batch,
+ x + leftMargin - left.getRegionWidth(),
+ y + topMargin,
+ left.getRegionWidth(),
+ height - topMargin - bottomMargin);
+
+ tmpTile.setRegion(right);
+ tmpTile.draw(
+ batch,
+ x + width - rightMargin,
+ y + topMargin,
+ right.getRegionWidth(),
+ height - topMargin - bottomMargin);
+ }
+
+ // FIXME: I don't like this hardwiring
+ protected java.awt.Shape getFigureFacingArrow(int angle, int size) {
+ int base = (int) (size * .75);
+ int width = (int) (size * .35);
+
+ var facingArrow = new GeneralPath();
+ facingArrow.moveTo(base, -width);
+ facingArrow.lineTo(size, 0);
+ facingArrow.lineTo(base, width);
+ facingArrow.lineTo(base, -width);
+
+ return facingArrow.createTransformedShape(
+ AffineTransform.getRotateInstance(-Math.toRadians(angle)));
+ }
+
+ // FIXME: I don't like this hardwiring
+ protected java.awt.Shape getCircleFacingArrow(int angle, int size) {
+ int base = (int) (size * .75);
+ int width = (int) (size * .35);
+
+ var facingArrow = new GeneralPath();
+ facingArrow.moveTo(base, -width);
+ facingArrow.lineTo(size, 0);
+ facingArrow.lineTo(base, width);
+ facingArrow.lineTo(base, -width);
+
+ return facingArrow.createTransformedShape(
+ AffineTransform.getRotateInstance(-Math.toRadians(angle)));
+ }
+
+ // FIXME: I don't like this hardwiring
+ protected java.awt.Shape getSquareFacingArrow(int angle, int size) {
+ int base = (int) (size * .75);
+ int width = (int) (size * .35);
+
+ var facingArrow = new GeneralPath();
+ facingArrow.moveTo(0, 0);
+ facingArrow.lineTo(-(size - base), -width);
+ facingArrow.lineTo(-(size - base), width);
+ facingArrow.lineTo(0, 0);
+
+ return facingArrow.createTransformedShape(
+ AffineTransform.getRotateInstance(-Math.toRadians(angle)));
+ }
+
+ private void paintClipped(Sprite image, Area bounds, Area clip) {
+ batch.flush();
+ backBuffer.begin();
+ ScreenUtils.clear(Color.CLEAR);
+
+ setProjectionMatrix(cam.combined);
+
+ image.draw(batch);
+
+ areaRenderer.setColor(Color.CLEAR);
+ tmpArea.reset();
+ tmpArea.add(bounds);
+ tmpArea.subtract(clip);
+ areaRenderer.fillArea(batch, tmpArea);
+ batch.flush();
+
+ backBuffer.end();
+
+ tmpWorldCoord.x = image.getX();
+ tmpWorldCoord.y = image.getY();
+ tmpWorldCoord.z = 0;
+ var screenCoord = cam.project(tmpWorldCoord);
+
+ var x = image.getX();
+ var y = image.getY();
+ var w = image.getWidth();
+ var h = image.getHeight();
+ var wsrc = image.getWidth() / zoom;
+ var hsrc = image.getHeight() / zoom;
+
+ batch.draw(
+ backBuffer.getColorBufferTexture(),
+ x,
+ y,
+ w,
+ h,
+ (int) screenCoord.x,
+ (int) screenCoord.y,
+ (int) wsrc,
+ (int) hsrc,
+ false,
+ true);
+ }
+
+ private void renderPath(Path path, TokenFootprint footprint) {
+ if (path == null) {
+ return;
+ }
+
+ if (path.getCellPath().isEmpty()) {
+ return;
+ }
+ Grid grid = zoneCache.getZone().getGrid();
+
+ // log.info("Rendering path..." + System.currentTimeMillis());
+
+ java.awt.Rectangle footprintBounds = footprint.getBounds(grid);
+ if (path.getCellPath().get(0) instanceof CellPoint) {
+ timer.start("renderPath-1");
+ CellPoint previousPoint = null;
+ Point previousHalfPoint = null;
+
+ Path pathCP = (Path) path;
+ List cellPath = pathCP.getCellPath();
+
+ Set pathSet = new HashSet();
+ List waypointList = new LinkedList();
+ for (CellPoint p : cellPath) {
+ pathSet.addAll(footprint.getOccupiedCells(p));
+
+ if (pathCP.isWaypoint(p) && previousPoint != null) {
+ ZonePoint zp = grid.convert(p);
+ zp.x += footprintBounds.width / 2;
+ zp.y += footprintBounds.height / 2;
+ waypointList.add(zp);
+ }
+ previousPoint = p;
+ }
+
+ // Don't show the final path point as a waypoint, it's redundant, and ugly
+ if (waypointList.size() > 0) {
+ waypointList.remove(waypointList.size() - 1);
+ }
+ timer.stop("renderPath-1");
+ // log.info("pathSet size: " + pathSet.size());
+
+ timer.start("renderPath-2");
+ Dimension cellOffset = zoneCache.getZone().getGrid().getCellOffset();
+ for (CellPoint p : pathSet) {
+ ZonePoint zp = grid.convert(p);
+ zp.x += grid.getCellWidth() / 2 + cellOffset.width;
+ zp.y += grid.getCellHeight() / 2 + cellOffset.height;
+ highlightCell(zp, getCellHighlight(), 1.0f);
+ }
+ if (AppState.getShowMovementMeasurements()) {
+ double cellAdj = grid.isHex() ? 2.5 : 2;
+ for (CellPoint p : cellPath) {
+ ZonePoint zp = grid.convert(p);
+ zp.x += grid.getCellWidth() / cellAdj + cellOffset.width;
+ zp.y += grid.getCellHeight() / cellAdj + cellOffset.height;
+ addDistanceText(
+ zp,
+ 1.0f,
+ (float) p.getDistanceTraveled(zoneCache.getZone()),
+ (float) p.getDistanceTraveledWithoutTerrain());
+ }
+ }
+ int w = 0;
+ for (ZonePoint p : waypointList) {
+ ZonePoint zp = new ZonePoint(p.x + cellOffset.width, p.y + cellOffset.height);
+ highlightCell(zp, zoneCache.fetch("redDot"), .333f);
+ }
+
+ // Line path
+ if (grid.getCapabilities().isPathLineSupported()) {
+ ZonePoint lineOffset;
+ if (grid.isHex()) {
+ lineOffset = new ZonePoint(0, 0);
+ } else {
+ lineOffset =
+ new ZonePoint(
+ footprintBounds.x + footprintBounds.width / 2 - grid.getOffsetX(),
+ footprintBounds.y + footprintBounds.height / 2 - grid.getOffsetY());
+ }
+
+ int xOffset = (int) (lineOffset.x);
+ int yOffset = (int) (lineOffset.y);
+
+ drawer.setColor(Color.BLUE);
+
+ previousPoint = null;
+ tmpFloat.clear();
+ for (CellPoint p : cellPath) {
+ if (previousPoint != null) {
+ ZonePoint ozp = grid.convert(previousPoint);
+ int ox = ozp.x;
+ int oy = ozp.y;
+
+ ZonePoint dzp = grid.convert(p);
+ int dx = dzp.x;
+ int dy = dzp.y;
+
+ int halfx = ((ox + dx) / 2);
+ int halfy = ((oy + dy) / 2);
+ Point halfPoint = new Point(halfx, halfy);
+
+ if (previousHalfPoint != null) {
+ int x1 = previousHalfPoint.x + xOffset;
+ int y1 = previousHalfPoint.y + yOffset;
+
+ int x2 = ox + xOffset;
+ int y2 = oy + yOffset;
+
+ int xh = halfPoint.x + xOffset;
+ int yh = halfPoint.y + yOffset;
+
+ tmpVector0.set(x1, -y1);
+ tmpVector1.set(x2, -y2);
+ tmpVector2.set(xh, -yh);
+
+ for (var i = 1; i <= POINTS_PER_BEZIER; i++) {
+ Bezier.quadratic(
+ tmpVectorOut,
+ i / POINTS_PER_BEZIER,
+ tmpVector0,
+ tmpVector1,
+ tmpVector2,
+ tmpVector);
+ tmpFloat.add(tmpVectorOut.x, tmpVectorOut.y);
+ }
+ }
+ previousHalfPoint = halfPoint;
+ }
+ previousPoint = p;
+ }
+ drawer.path(tmpFloat.toArray(), drawer.getDefaultLineWidth(), JoinType.NONE, true);
+ }
+ drawer.setColor(Color.WHITE);
+ timer.stop("renderPath-2");
+ } else {
+ timer.start("renderPath-3");
+ // Zone point/gridless path
+
+ // Line
+ var highlight = tmpColor;
+ highlight.set(1, 1, 1, 80 / 255f);
+ var highlightStroke = 9f;
+
+ ScreenPoint lastPoint = null;
+
+ Path pathZP = (Path) path;
+ List pathList = pathZP.getCellPath();
+ for (ZonePoint zp : pathList) {
+ if (lastPoint == null) {
+ lastPoint =
+ ScreenPoint.fromZonePointRnd(
+ zoneCache.getZoneRenderer(),
+ zp.x + (footprintBounds.width / 2) * footprint.getScale(),
+ zp.y + (footprintBounds.height / 2) * footprint.getScale());
+ continue;
+ }
+ ScreenPoint nextPoint =
+ ScreenPoint.fromZonePoint(
+ zoneCache.getZoneRenderer(),
+ zp.x + (footprintBounds.width / 2) * footprint.getScale(),
+ zp.y + (footprintBounds.height / 2) * footprint.getScale());
+
+ drawer.line(
+ (float) lastPoint.x,
+ -(float) lastPoint.y,
+ (float) nextPoint.x,
+ -(float) nextPoint.y,
+ highlight,
+ highlightStroke);
+
+ drawer.line(
+ (float) lastPoint.x,
+ -(float) lastPoint.y,
+ (float) nextPoint.x,
+ -(float) nextPoint.y,
+ Color.BLUE,
+ drawer.getDefaultLineWidth());
+ lastPoint = nextPoint;
+ }
+
+ // Waypoints
+ boolean originPoint = true;
+ for (ZonePoint p : pathList) {
+ // Skip the first point (it's the path origin)
+ if (originPoint) {
+ originPoint = false;
+ continue;
+ }
+
+ // Skip the final point
+ if (p == pathList.get(pathList.size() - 1)) {
+ continue;
+ }
+ p =
+ new ZonePoint(
+ (p.x + (footprintBounds.width / 2)), (p.y + (footprintBounds.height / 2)));
+ highlightCell(p, zoneCache.fetch("redDot"), .333f);
+ }
+ timer.stop("renderPath-3");
+ }
+ }
+
+ private TextureRegion getCellHighlight() {
+ if (zoneCache.getZone().getGrid() instanceof SquareGrid) return zoneCache.fetch("whiteBorder");
+ if (zoneCache.getZone().getGrid() instanceof HexGrid) return zoneCache.fetch("hexBorder");
+ if (zoneCache.getZone().getGrid() instanceof IsometricGrid) return zoneCache.fetch("isoBorder");
+
+ return null;
+ }
+
+ private void addDistanceText(
+ ZonePoint point, float size, float distance, float distanceWithoutTerrain) {
+ if (distance == 0) return;
+
+ Grid grid = zoneCache.getZone().getGrid();
+ float cwidth = (float) grid.getCellWidth();
+ float cheight = (float) grid.getCellHeight();
+
+ float iwidth = cwidth * size;
+ float iheight = cheight * size;
+
+ var cellX = (point.x - iwidth / 2);
+ var cellY = (-point.y + iheight / 2) + boldFont.getLineHeight();
+
+ // Draw distance for each cell
+ var textOffset = 7 * boldFontScale; // 7 pixels at 100% zoom & grid size of 50
+
+ String distanceText = NumberFormat.getInstance().format(distance);
+ if (log.isDebugEnabled() || showAstarDebugging) {
+ distanceText += " (" + NumberFormat.getInstance().format(distanceWithoutTerrain) + ")";
+ }
+
+ glyphLayout.setText(boldFont, distanceText);
+
+ var textWidth = glyphLayout.width;
+
+ boldFont.setColor(Color.BLACK);
+
+ boldFont.draw(
+ batch,
+ distanceText,
+ cellX + cwidth - textWidth - textOffset,
+ cellY - cheight /*- textOffset*/);
+ }
+
+ private void highlightCell(ZonePoint zp, TextureRegion image, float size) {
+ Grid grid = zoneCache.getZone().getGrid();
+ float cwidth = (float) grid.getCellWidth() * size;
+ float cheight = (float) grid.getCellHeight() * size;
+
+ float rotation = 0;
+ if (zoneCache.getZone().getGrid() instanceof HexGridHorizontal) rotation = 90;
+
+ batch.draw(
+ image, zp.x - cwidth / 2, -zp.y - cheight / 2, 0, 0, cwidth, cheight, 1f, 1f, rotation);
+ }
+
+ @Subscribe
+ void onZoneActivated(ZoneActivated event) {
+ Gdx.app.postRunnable(
+ () -> {
+ renderZone = false;
+
+ var newZone = event.zone();
+ zoneCache = new ZoneCache(newZone, atlas);
+ drawnElementRenderer.setZoneCache(zoneCache);
+ tokenOverlayRenderer.setZoneCache(zoneCache);
+ gridRenderer.setZoneCache(zoneCache);
+ renderZone = true;
+ });
+ }
+
+ public void setScale(Scale scale) {
+ if (!initialized) {
+ return;
+ }
+
+ offsetX = (int) (scale.getOffsetX() * -1);
+ offsetY = (int) (scale.getOffsetY());
+ zoom = (float) (1f / scale.getScale());
+ updateCam();
+ }
+
+ public void flushFog() {
+ visibleScreenArea = null;
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/GridRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/GridRenderer.java
new file mode 100644
index 0000000000..cb43525a32
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/GridRenderer.java
@@ -0,0 +1,209 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx;
+
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.Batch;
+import com.badlogic.gdx.math.Matrix4;
+import com.badlogic.gdx.utils.Pools;
+import java.awt.*;
+import net.rptools.maptool.client.AppState;
+import net.rptools.maptool.client.ui.zone.renderer.ZoneRendererConstants;
+import net.rptools.maptool.model.*;
+import space.earlygrey.shapedrawer.JoinType;
+import space.earlygrey.shapedrawer.ShapeDrawer;
+
+public class GridRenderer {
+ private ZoneCache zoneCache;
+ private final AreaRenderer areaRenderer;
+ private final ShapeDrawer drawer;
+ private final Batch batch;
+ private final Camera hudCam;
+
+ public GridRenderer(AreaRenderer areaRenderer, Camera hudCam) {
+ this.areaRenderer = areaRenderer;
+ this.drawer = areaRenderer.getShapeDrawer();
+ batch = drawer.getBatch();
+ this.hudCam = hudCam;
+ }
+
+ public void setZoneCache(ZoneCache zoneCache) {
+ this.zoneCache = zoneCache;
+ }
+
+ public void render() {
+ var grid = zoneCache.getZone().getGrid();
+ var scale = (float) zoneCache.getZoneRenderer().getScale();
+ int gridSize = (int) (grid.getSize() * scale);
+
+ if (!AppState.isShowGrid() || gridSize < ZoneRendererConstants.MIN_GRID_SIZE) {
+ return;
+ }
+
+ // Do nothing for GridlessGrid
+ if (grid instanceof HexGrid hexGrid) {
+ renderGrid(hexGrid);
+ } else if (grid instanceof SquareGrid squareGrid) {
+ renderGrid(squareGrid);
+ } else if (grid instanceof IsometricGrid isometricGrid) {
+ renderGrid(isometricGrid);
+ }
+ }
+
+ private void renderGrid(HexGrid grid) {
+ var renderer = zoneCache.getZoneRenderer();
+ var scale = renderer.getScale();
+ var scaledMinorRadius = grid.getMinorRadius() * scale;
+ var scaledEdgeLength = grid.getEdgeLength() * scale;
+ var scaledEdgeProjection = grid.getEdgeProjection() * scale;
+ var scaledHex = grid.createHalfShape(scaledMinorRadius, scaledEdgeProjection, scaledEdgeLength);
+
+ int offU = grid.getOffU(renderer);
+ int offV = grid.getOffV(renderer);
+ int count = 0;
+
+ var tmpColor = Pools.obtain(Color.class);
+ Color.argb8888ToColor(tmpColor, zoneCache.getZone().getGridColor());
+ drawer.setColor(tmpColor);
+ var floats = areaRenderer.pathToFloatArray(scaledHex.getPathIterator(null));
+ var lineWidth = AppState.getGridSize();
+
+ for (double v = offV % (scaledMinorRadius * 2) - (scaledMinorRadius * 2);
+ v < grid.getRendererSizeV(renderer);
+ v += scaledMinorRadius) {
+ double offsetU = (int) ((count & 1) == 0 ? 0 : -(scaledEdgeProjection + scaledEdgeLength));
+ count++;
+
+ double start =
+ offU % (2 * scaledEdgeLength + 2 * scaledEdgeProjection)
+ - (2 * scaledEdgeLength + 2 * scaledEdgeProjection);
+ double end =
+ grid.getRendererSizeU(renderer) + 2 * scaledEdgeLength + 2 * scaledEdgeProjection;
+ double incr = 2 * scaledEdgeLength + 2 * scaledEdgeProjection;
+ for (double u = start; u < end; u += incr) {
+ float transX;
+ float transY;
+ if (grid instanceof HexGridVertical) {
+ transX = (float) (u + offsetU);
+ transY = hudCam.viewportHeight - (float) v;
+ } else {
+ transX = (float) v;
+ transY = (float) (-u - offsetU) + hudCam.viewportHeight;
+ }
+
+ var tmpMatrix = Pools.obtain(Matrix4.class);
+ tmpMatrix.translate(transX, transY, 0);
+ batch.setTransformMatrix(tmpMatrix);
+ drawer.update();
+
+ drawer.path(floats, lineWidth, JoinType.SMOOTH, true);
+ tmpMatrix.idt();
+ batch.setTransformMatrix(tmpMatrix);
+ Pools.free(tmpMatrix);
+ drawer.update();
+ }
+ }
+ Pools.free(tmpColor);
+ }
+
+ private void renderGrid(IsometricGrid grid) {
+ var scale = (float) zoneCache.getZoneRenderer().getScale();
+ int gridSize = (int) (grid.getSize() * scale);
+
+ var tmpColor = Pools.obtain(Color.class);
+ Color.argb8888ToColor(tmpColor, zoneCache.getZone().getGridColor());
+
+ drawer.setColor(tmpColor);
+
+ var x = hudCam.position.x - hudCam.viewportWidth / 2;
+ var y = hudCam.position.y - hudCam.viewportHeight / 2;
+ var w = hudCam.viewportWidth;
+ var h = hudCam.viewportHeight;
+
+ double isoHeight = grid.getSize() * scale;
+ double isoWidth = grid.getSize() * 2 * scale;
+
+ int offX =
+ (int) (zoneCache.getZoneRenderer().getViewOffsetX() % isoWidth + grid.getOffsetX() * scale)
+ + 1;
+ int offY =
+ (int) (zoneCache.getZoneRenderer().getViewOffsetY() % gridSize + grid.getOffsetY() * scale)
+ + 1;
+
+ int startCol = (int) ((int) (x / isoWidth) * isoWidth);
+ int startRow = (int) (y / gridSize) * gridSize;
+
+ for (double row = startRow; row < y + h + gridSize; row += gridSize) {
+ for (double col = startCol; col < x + w + isoWidth; col += isoWidth) {
+ drawHatch(grid, (int) (col + offX), h - (int) (row + offY));
+ }
+ }
+
+ for (double row = startRow - (isoHeight / 2); row < y + h + gridSize; row += gridSize) {
+ for (double col = startCol - (isoWidth / 2); col < x + w + isoWidth; col += isoWidth) {
+ drawHatch(grid, (int) (col + offX), h - (int) (row + offY));
+ }
+ }
+ Pools.free(tmpColor);
+ }
+
+ private void drawHatch(IsometricGrid grid, float x, float y) {
+ double isoWidth = grid.getSize() * zoneCache.getZoneRenderer().getScale();
+ int hatchSize = isoWidth > 10 ? (int) isoWidth / 8 : 2;
+
+ var lineWidth = AppState.getGridSize();
+
+ drawer.line(x - (hatchSize * 2), y - hatchSize, x + (hatchSize * 2), y + hatchSize, lineWidth);
+ drawer.line(x - (hatchSize * 2), y + hatchSize, x + (hatchSize * 2), y - hatchSize, lineWidth);
+ }
+
+ private void renderGrid(SquareGrid grid) {
+ var scale = (float) zoneCache.getZoneRenderer().getScale();
+ float gridSize = (grid.getSize() * scale);
+ var tmpColor = Pools.obtain(Color.class);
+ Color.argb8888ToColor(tmpColor, zoneCache.getZone().getGridColor());
+
+ drawer.setColor(tmpColor);
+
+ var x = hudCam.position.x - hudCam.viewportWidth / 2;
+ var y = hudCam.position.y - hudCam.viewportHeight / 2;
+ var w = hudCam.viewportWidth;
+ var h = hudCam.viewportHeight;
+
+ var offX =
+ Math.round(
+ zoneCache.getZoneRenderer().getViewOffsetX() % gridSize + grid.getOffsetX() * scale);
+ var offY =
+ Math.round(
+ zoneCache.getZoneRenderer().getViewOffsetY() % gridSize + grid.getOffsetY() * scale);
+
+ var startCol = ((int) (x / gridSize) * gridSize);
+ var startRow = ((int) (y / gridSize) * gridSize);
+
+ var lineWidth = AppState.getGridSize();
+
+ for (float row = startRow; row < y + h + gridSize; row += gridSize) {
+ var rounded = Math.round(h - (row + offY));
+ drawer.line(x, rounded, x + w, rounded, lineWidth);
+ }
+
+ for (float col = startCol; col < x + w + gridSize; col += gridSize) {
+ var rounded = Math.round(col + offX);
+ drawer.line(rounded, y, rounded, y + h, lineWidth);
+ }
+ Pools.free(tmpColor);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/TokenOverlayRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/TokenOverlayRenderer.java
new file mode 100644
index 0000000000..3f28d1412d
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/TokenOverlayRenderer.java
@@ -0,0 +1,619 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import java.awt.*;
+import java.awt.geom.Area;
+import net.rptools.maptool.client.swing.SwingUtil;
+import net.rptools.maptool.client.ui.token.*;
+import net.rptools.maptool.model.Token;
+import net.rptools.maptool.util.FunctionUtil;
+import space.earlygrey.shapedrawer.JoinType;
+import space.earlygrey.shapedrawer.ShapeDrawer;
+
+public class TokenOverlayRenderer {
+ private final Color tmpColor = Color.WHITE;
+ private ZoneCache zoneCache;
+ private final AreaRenderer areaRenderer;
+ private final ShapeDrawer drawer;
+ private final PolygonSpriteBatch batch;
+
+ public TokenOverlayRenderer(AreaRenderer areaRenderer) {
+ this.areaRenderer = areaRenderer;
+ drawer = areaRenderer.getShapeDrawer();
+ batch = (PolygonSpriteBatch) drawer.getBatch();
+ }
+
+ public void setZoneCache(ZoneCache zoneCache) {
+ this.zoneCache = zoneCache;
+ }
+
+ public void render(float stateTime, AbstractTokenOverlay overlay, Token token, Object value) {
+ if (overlay instanceof BarTokenOverlay barTokenOverlay)
+ renderBarTokenOverlay(stateTime, barTokenOverlay, token, value);
+ else if (overlay instanceof BooleanTokenOverlay booleanTokenOverlay)
+ renderTokenOverlay(stateTime, booleanTokenOverlay, token, value);
+ }
+
+ private void renderBarTokenOverlay(
+ float stateTime, BarTokenOverlay overlay, Token token, Object value) {
+ if (value == null) return;
+ double val;
+ if (value instanceof Number) {
+ val = ((Number) value).doubleValue();
+ } else {
+ try {
+ val = Double.parseDouble(value.toString());
+ } catch (NumberFormatException e) {
+ return; // Bad value so don't paint.
+ }
+ } // endif
+ if (val < 0) val = 0;
+ if (val > 1) val = 1;
+
+ if (overlay instanceof MultipleImageBarTokenOverlay actualOverlay)
+ renderTokenOverlay(stateTime, actualOverlay, token, val);
+ else if (overlay instanceof SingleImageBarTokenOverlay actualOverlay)
+ renderTokenOverlay(stateTime, actualOverlay, token, val);
+ else if (overlay instanceof TwoToneBarTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token, val);
+ else if (overlay instanceof DrawnBarTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token, val);
+ else if (overlay instanceof TwoImageBarTokenOverlay actualOverlay)
+ renderTokenOverlay(stateTime, actualOverlay, token, val);
+ }
+
+ private void renderTokenOverlay(
+ float stateTime, MultipleImageBarTokenOverlay overlay, Token token, double barValue) {
+ int increment = overlay.findIncrement(barValue);
+
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+
+ // Get the images
+ var image = zoneCache.getSprite(overlay.getAssetIds()[increment], stateTime);
+
+ Dimension d = bounds.getSize();
+ Dimension size = new Dimension((int) image.getWidth(), (int) image.getHeight());
+ SwingUtil.constrainTo(size, d.width, d.height);
+
+ // Find the position of the image according to the size and side where they are placed
+ switch (overlay.getSide()) {
+ case LEFT:
+ case TOP:
+ y += d.height - size.height;
+ break;
+ case RIGHT:
+ x += d.width - size.width;
+ y += d.height - size.height;
+ break;
+ }
+
+ image.setPosition(x, y);
+ image.setSize(size.width, size.height);
+ image.draw(batch, overlay.getOpacity() / 100f);
+ }
+
+ private void renderTokenOverlay(
+ float stateTime, SingleImageBarTokenOverlay overlay, Token token, double barValue) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+
+ // Get the images
+ var image = zoneCache.getSprite(overlay.getAssetId(), stateTime);
+
+ Dimension d = bounds.getSize();
+ Dimension size = new Dimension((int) image.getWidth(), (int) image.getHeight());
+ SwingUtil.constrainTo(size, d.width, d.height);
+
+ var side = overlay.getSide();
+ // Find the position of the images according to the size and side where they are placed
+ switch (side) {
+ case LEFT:
+ case TOP:
+ y += d.height - size.height;
+ break;
+ case RIGHT:
+ x += d.width - size.width;
+ y += d.height - size.height;
+ break;
+ }
+
+ int screenWidth =
+ (side == BarTokenOverlay.Side.TOP || side == BarTokenOverlay.Side.BOTTOM)
+ ? overlay.calcBarSize(size.width, barValue)
+ : size.width;
+ int screenHeight =
+ (side == BarTokenOverlay.Side.LEFT || side == BarTokenOverlay.Side.RIGHT)
+ ? overlay.calcBarSize(size.height, barValue)
+ : size.height;
+
+ image.setPosition(x + size.width - screenWidth, y + size.height - screenHeight);
+ image.setSize(screenWidth, screenHeight);
+
+ var u = image.getU();
+ var v = image.getV();
+ var u2 = image.getU2();
+ var v2 = image.getV2();
+
+ var uFactor = screenWidth * 1.0f / size.width;
+ var uDiff = (u2 - u) * uFactor;
+ image.setU(u2 - uDiff);
+
+ var vFactor = screenHeight * 1.0f / size.height;
+ var vDiff = (v2 - v) * vFactor;
+ image.setV(v2 - vDiff);
+
+ image.draw(batch, overlay.getOpacity() / 100f);
+
+ image.setU(u);
+ image.setV(v);
+ }
+
+ private void renderTokenOverlay(DrawnBarTokenOverlay overlay, Token token, double barValue) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ var side = overlay.getSide();
+ var thickness = overlay.getThickness();
+
+ int width =
+ (side == BarTokenOverlay.Side.TOP || side == BarTokenOverlay.Side.BOTTOM) ? w : thickness;
+ int height =
+ (side == BarTokenOverlay.Side.LEFT || side == BarTokenOverlay.Side.RIGHT) ? h : thickness;
+
+ switch (side) {
+ case LEFT:
+ case TOP:
+ y += h - height;
+ break;
+ case RIGHT:
+ x += w - width;
+ break;
+ }
+
+ if (side == BarTokenOverlay.Side.TOP || side == BarTokenOverlay.Side.BOTTOM) {
+ width = overlay.calcBarSize(width, barValue);
+ } else {
+ height = overlay.calcBarSize(height, barValue);
+ y += bounds.height - height;
+ }
+
+ var barColor = overlay.getBarColor();
+ tmpColor.set(
+ barColor.getRed() / 255f,
+ barColor.getGreen() / 255f,
+ barColor.getBlue() / 255f,
+ barColor.getAlpha() / 255f);
+ drawer.filledRectangle(x, y, width, height, tmpColor);
+ }
+
+ private void renderTokenOverlay(TwoToneBarTokenOverlay overlay, Token token, double barValue) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ var side = overlay.getSide();
+ var thickness = overlay.getThickness();
+
+ int width =
+ (side == BarTokenOverlay.Side.TOP || side == BarTokenOverlay.Side.BOTTOM) ? w : thickness;
+ int height =
+ (side == BarTokenOverlay.Side.LEFT || side == BarTokenOverlay.Side.RIGHT) ? h : thickness;
+
+ switch (side) {
+ case LEFT:
+ case TOP:
+ y += h - height;
+ break;
+ case RIGHT:
+ x += w - width;
+ break;
+ }
+
+ var color = overlay.getBgColor();
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ color.getAlpha() / 255f);
+ drawer.filledRectangle(x, y, width, height, tmpColor);
+
+ // Draw the bar
+ int borderSize = thickness > 5 ? 2 : 1;
+ x += borderSize;
+ y += borderSize;
+ width -= borderSize * 2;
+ height -= borderSize * 2;
+ if (side == BarTokenOverlay.Side.TOP || side == BarTokenOverlay.Side.BOTTOM) {
+ width = overlay.calcBarSize(width, barValue);
+ } else {
+ height = overlay.calcBarSize(height, barValue);
+ }
+
+ color = overlay.getBarColor();
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ color.getAlpha() / 255f);
+ drawer.filledRectangle(x, y, width, height, tmpColor);
+ }
+
+ private void renderTokenOverlay(
+ float stateTime, TwoImageBarTokenOverlay overlay, Token token, double barValue) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+
+ // Get the images
+ var topImage = zoneCache.getSprite(overlay.getTopAssetId(), stateTime);
+ var bottomImage = zoneCache.getSprite(overlay.getBottomAssetId(), stateTime);
+
+ Dimension d = bounds.getSize();
+ Dimension size = new Dimension((int) topImage.getWidth(), (int) topImage.getHeight());
+ SwingUtil.constrainTo(size, d.width, d.height);
+
+ var side = overlay.getSide();
+ // Find the position of the images according to the size and side where they are placed
+ switch (side) {
+ case LEFT:
+ case TOP:
+ y += d.height - size.height;
+ break;
+ case RIGHT:
+ x += d.width - size.width;
+ y += d.height - size.height;
+ break;
+ }
+
+ var screenWidth =
+ (side == BarTokenOverlay.Side.TOP || side == BarTokenOverlay.Side.BOTTOM)
+ ? overlay.calcBarSize(size.width, barValue)
+ : size.width;
+ var screenHeight =
+ (side == BarTokenOverlay.Side.LEFT || side == BarTokenOverlay.Side.RIGHT)
+ ? overlay.calcBarSize(size.height, barValue)
+ : size.height;
+
+ bottomImage.setPosition(x, y);
+ bottomImage.setSize(size.width, size.height);
+ bottomImage.draw(batch, overlay.getOpacity() / 100f);
+
+ var u = topImage.getU();
+ var v = topImage.getV();
+ var u2 = topImage.getU2();
+ var v2 = topImage.getV2();
+
+ var wFactor = screenWidth * 1.0f / size.width;
+ var uDiff = (u2 - u) * wFactor;
+
+ var vFactor = screenHeight * 1.0f / size.height;
+ var vDiff = (v2 - v) * vFactor;
+
+ topImage.setPosition(x, y);
+ topImage.setSize(screenWidth, screenHeight);
+
+ if (side == BarTokenOverlay.Side.LEFT || side == BarTokenOverlay.Side.RIGHT) {
+ topImage.setU(u2 - uDiff);
+ topImage.setV(v2 - vDiff);
+ } else {
+
+ topImage.setU2(u + uDiff);
+ topImage.setV2(v + vDiff);
+ }
+ topImage.draw(batch, overlay.getOpacity() / 100f);
+
+ topImage.setU(u);
+ topImage.setV(v);
+ topImage.setU2(u2);
+ topImage.setV2(v2);
+ }
+
+ private void renderTokenOverlay(
+ float stateTime, BooleanTokenOverlay overlay, Token token, Object value) {
+ if (!FunctionUtil.getBooleanValue(value)) return;
+
+ if (overlay instanceof ImageTokenOverlay actualOverlay)
+ renderTokenOverlay(stateTime, actualOverlay, token);
+ else if (overlay instanceof FlowColorDotTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ else if (overlay instanceof YieldTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ else if (overlay instanceof OTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ else if (overlay instanceof ColorDotTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ else if (overlay instanceof DiamondTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ else if (overlay instanceof TriangleTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ else if (overlay instanceof CrossTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ else if (overlay instanceof XTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ else if (overlay instanceof ShadedTokenOverlay actualOverlay)
+ renderTokenOverlay(actualOverlay, token);
+ }
+
+ private void renderTokenOverlay(ShadedTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ tmpColor.set(1, 1, 1, overlay.getOpacity() / 100f);
+ drawer.setColor(tmpColor);
+ drawer.filledRectangle(x, y, w, h);
+ drawer.setColor(com.badlogic.gdx.graphics.Color.WHITE);
+ }
+
+ private void renderTokenOverlay(float stateTime, ImageTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y;
+
+ // Get the image
+ java.awt.Rectangle iBounds = overlay.getImageBounds(bounds, token);
+ Dimension d = iBounds.getSize();
+
+ var image = zoneCache.getSprite(overlay.getAssetId(), stateTime);
+
+ Dimension size = new Dimension((int) image.getWidth(), (int) image.getHeight());
+ SwingUtil.constrainTo(size, d.width, d.height);
+
+ // Paint it at the right location
+ int width = size.width;
+ int height = size.height;
+
+ if (overlay instanceof CornerImageTokenOverlay) {
+ x += iBounds.x + (d.width - width) / 2;
+ y -= iBounds.y + (d.height - height) / 2 + iBounds.height;
+ } else {
+ x = iBounds.x + (d.width - width) / 2;
+ y = -(iBounds.y + (d.height - height) / 2) - iBounds.height;
+ }
+
+ image.setPosition(x, y);
+ image.setSize(size.width, size.height);
+ image.draw(batch, overlay.getOpacity() / 100f);
+ }
+
+ private void renderTokenOverlay(XTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ var color = overlay.getColor();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(tmpColor, color.getRGB());
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ overlay.getOpacity() / 100f);
+
+ var stroke = overlay.getStroke();
+
+ drawer.setColor(tmpColor);
+ drawer.line(x, y, x + w, y + h, stroke.getLineWidth());
+ drawer.line(x, y + h, x + w, y, stroke.getLineWidth());
+ drawer.setColor(com.badlogic.gdx.graphics.Color.WHITE);
+ }
+
+ private void renderTokenOverlay(FlowColorDotTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+
+ var color = overlay.getColor();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(tmpColor, color.getRGB());
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ overlay.getOpacity() / 100f);
+ drawer.setColor(tmpColor);
+ Shape s = overlay.getShape(bounds, token);
+ areaRenderer.fillArea(batch, new Area(s));
+ drawer.setColor(com.badlogic.gdx.graphics.Color.WHITE);
+ }
+
+ private void renderTokenOverlay(YieldTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ var color = overlay.getColor();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(tmpColor, color.getRGB());
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ overlay.getOpacity() / 100f);
+
+ var stroke = overlay.getStroke();
+ var hc = w / 2f;
+ var vc = h * (1 - 0.134f);
+
+ var floats =
+ new float[] {
+ x, y + vc, x + w, y + vc, x + hc, y,
+ };
+
+ drawer.setColor(tmpColor);
+ drawer.path(floats, stroke.getLineWidth(), JoinType.POINTY, false);
+ drawer.setColor(com.badlogic.gdx.graphics.Color.WHITE);
+ }
+
+ private void renderTokenOverlay(OTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ var color = overlay.getColor();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(tmpColor, color.getRGB());
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ overlay.getOpacity() / 100f);
+
+ var stroke = overlay.getStroke();
+ var lineWidth = stroke.getLineWidth();
+
+ var centerX = x + w / 2f;
+ var centerY = y + h / 2f;
+ var radiusX = w / 2f - lineWidth / 2f;
+ var radiusY = h / 2f - lineWidth / 2f;
+
+ drawer.setColor(tmpColor);
+ drawer.ellipse(centerX, centerY, radiusX, radiusY, 0, lineWidth);
+ drawer.setColor(com.badlogic.gdx.graphics.Color.WHITE);
+ }
+
+ private void renderTokenOverlay(ColorDotTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+
+ var color = overlay.getColor();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(tmpColor, color.getRGB());
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ overlay.getOpacity() / 100f);
+
+ var size = w * 0.1f;
+ var offset = w * 0.8f;
+
+ var posX = x + size;
+ var posY = y + size;
+
+ switch (overlay.getCorner()) {
+ case SOUTH_EAST:
+ posX += offset;
+ break;
+ case SOUTH_WEST:
+ break;
+ case NORTH_EAST:
+ posX += offset;
+ posY += offset;
+ break;
+ case NORTH_WEST:
+ posY += offset;
+ break;
+ }
+
+ drawer.setColor(tmpColor);
+ drawer.filledEllipse(posX, posY, size, size);
+ drawer.setColor(com.badlogic.gdx.graphics.Color.WHITE);
+ }
+
+ private void renderTokenOverlay(DiamondTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ var color = overlay.getColor();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(tmpColor, color.getRGB());
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ overlay.getOpacity() / 100f);
+ var stroke = overlay.getStroke();
+
+ var hc = w / 2f;
+ var vc = h / 2f;
+
+ var floats =
+ new float[] {
+ x, y + vc, x + hc, y, x + w, y + vc, x + hc, y + h,
+ };
+
+ drawer.setColor(tmpColor);
+ drawer.path(floats, stroke.getLineWidth(), JoinType.POINTY, false);
+ drawer.setColor(com.badlogic.gdx.graphics.Color.WHITE);
+ }
+
+ private void renderTokenOverlay(TriangleTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ var color = overlay.getColor();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(tmpColor, color.getRGB());
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ overlay.getOpacity() / 100f);
+ var stroke = overlay.getStroke();
+
+ var hc = w / 2f;
+ var vc = h * (1 - 0.866f);
+
+ var floats =
+ new float[] {
+ x, y + vc, x + w, y + vc, x + hc, y + h,
+ };
+
+ drawer.setColor(tmpColor);
+ drawer.path(floats, stroke.getLineWidth(), JoinType.POINTY, false);
+ drawer.setColor(com.badlogic.gdx.graphics.Color.WHITE);
+ }
+
+ private void renderTokenOverlay(CrossTokenOverlay overlay, Token token) {
+ var bounds = token.getBounds(zoneCache.getZone());
+ var x = bounds.x;
+ var y = -bounds.y - bounds.height;
+ var w = bounds.width;
+ var h = bounds.height;
+
+ var color = overlay.getColor();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(tmpColor, color.getRGB());
+ tmpColor.set(
+ color.getRed() / 255f,
+ color.getGreen() / 255f,
+ color.getBlue() / 255f,
+ overlay.getOpacity() / 100f);
+ var stroke = overlay.getStroke();
+
+ drawer.setColor(tmpColor);
+ drawer.line(x, y + h / 2f, x + w, y + h / 2f, stroke.getLineWidth());
+ drawer.line(x + w / 2f, y, x + w / 2f, y + h, stroke.getLineWidth());
+ drawer.setColor(Color.WHITE);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/ZoneCache.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/ZoneCache.java
new file mode 100644
index 0000000000..a3d541a09c
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/ZoneCache.java
@@ -0,0 +1,369 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.Pixmap;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.g2d.*;
+import com.badlogic.gdx.utils.Disposable;
+import com.badlogic.gdx.video.VideoPlayer;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import net.rptools.lib.MD5Key;
+import net.rptools.lib.image.ImageUtil;
+import net.rptools.maptool.client.MapTool;
+import net.rptools.maptool.client.ui.zone.ZoneView;
+import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer;
+import net.rptools.maptool.model.AssetManager;
+import net.rptools.maptool.model.IsometricGrid;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.DrawableColorPaint;
+import net.rptools.maptool.model.drawing.DrawablePaint;
+import net.rptools.maptool.model.drawing.DrawableTexturePaint;
+import net.rptools.maptool.util.ImageManager;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class ZoneCache implements Disposable {
+
+ public record GdxPaint(Color color, TextureRegion textureRegion) {}
+
+ private static final Logger log = LogManager.getLogger(ZoneCache.class);
+ private final Zone zone;
+ private final ZoneRenderer zoneRenderer;
+ private final PixmapPacker packer =
+ new PixmapPacker(2048, 2048, Pixmap.Format.RGBA8888, 2, false);
+ private final TextureAtlas tokenAtlas = new TextureAtlas();
+
+ // this atlas is shared by all zones and must not be disposed here.
+ private TextureAtlas sharedAtlas;
+ private final Map> animationMap = new HashMap<>();
+ private final Map videoPlayerMap = new HashMap<>();
+ private final Map fetchedSprites = new HashMap<>();
+ private final Map isoSprites = new HashMap<>();
+ private final Map fetchedRegions = new HashMap<>();
+ private final Map bigSprites = new HashMap<>();
+ private final Map paintTextures = new HashMap<>();
+ private final Texture whitePixel;
+ private final TextureRegion whitePixelRegion;
+
+ public Zone getZone() {
+ return zone;
+ }
+
+ public ZoneRenderer getZoneRenderer() {
+ return zoneRenderer;
+ }
+
+ public ZoneView getZoneView() {
+ return zoneRenderer.getZoneView();
+ }
+
+ private Sprite TRANSFERING_SPRITE;
+ private Sprite BROKEN_SPRITE;
+
+ public void setSharedAtlas(TextureAtlas atlas) {
+ sharedAtlas = atlas;
+ if (atlas == null) return;
+ TRANSFERING_SPRITE = new Sprite(sharedAtlas.findRegion("unknown"));
+ BROKEN_SPRITE = new Sprite(sharedAtlas.findRegion("broken"));
+ }
+
+ public ZoneCache(@Nonnull Zone zone, @Nonnull TextureAtlas sharedAtlas) {
+ this.zone = zone;
+ setSharedAtlas(sharedAtlas);
+ zoneRenderer = MapTool.getFrame().getZoneRenderer(zone);
+
+ Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
+ pixmap.setColor(Color.WHITE);
+ pixmap.drawPixel(0, 0);
+ whitePixel = new Texture(pixmap);
+ pixmap.dispose();
+ whitePixelRegion = new TextureRegion(whitePixel, 0, 0, 1, 1);
+ }
+
+ /*
+ @Override
+ public void assetAvailable(MD5Key key) {
+ var asset = AssetManager.getAsset(key);
+ if (asset.getExtension().equals("gif")) {
+
+ Gdx.app.postRunnable(
+ () -> {
+ // var ass = AssetManager.getAsset(key);
+ var is = new ByteArrayInputStream(asset.getData());
+ var animation = GifDecoder.loadGIFAnimation(Animation.PlayMode.LOOP, is);
+ animationMap.put(key, animation);
+ });
+ return;
+ }
+ if (asset.getExtension().equals("data")) {
+ var videoPlayer = VideoPlayerCreator.createVideoPlayer();
+ videoPlayerMap.put(key, videoPlayer);
+ return;
+ }
+ BufferedImage img;
+ byte[] bytes;
+ try {
+ img =
+ ImageUtil.createCompatibleImage(
+ ImageUtil.bytesToImage(asset.getData(), asset.getName()), null);
+ bytes = ImageUtil.imageToBytes(img, "png");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ // without ImageUtil there seem to be some issues with transparency for some images.
+ // (black background instead of transparent)
+ var pix = new Pixmap(bytes, 0, bytes.length);
+
+ try {
+ var name = key.toString();
+ synchronized (packer) {
+ if (packer.getRect(name) == null) packer.pack(name, pix);
+
+ pix.dispose();
+ }
+ } catch (GdxRuntimeException x) {
+ // this means that the pixmap is too big for the atlas.
+ Gdx.app.postRunnable(
+ () -> {
+ synchronized (bigSprites) {
+ if (!bigSprites.containsKey(key)) bigSprites.put(key, new Sprite(new Texture(pix)));
+ }
+ pix.dispose();
+ });
+ }
+ Gdx.app.postRunnable(
+ () -> {
+ packer.updateTextureAtlas(
+ tokenAtlas, Texture.TextureFilter.Linear, Texture.TextureFilter.Linear, false);
+ });
+ }
+ */
+
+ private void imageToSprite(MD5Key key, BufferedImage image) {
+ byte[] bytes;
+
+ try {
+ bytes = ImageUtil.imageToBytes(image, "png");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ var name = key.toString();
+ var pixmap = new Pixmap(bytes, 0, bytes.length);
+ try {
+ synchronized (packer) {
+ if (packer.getRect(name) == null) {
+ packer.pack(name, pixmap);
+ }
+ pixmap.dispose();
+ }
+ } catch (Exception x) {
+ // this means that the pixmap is too big for the atlas.
+
+ synchronized (bigSprites) {
+ if (!bigSprites.containsKey(key)) {
+ bigSprites.put(key, new Sprite(new Texture(pixmap)));
+ }
+ }
+ pixmap.dispose();
+ }
+ packer.updateTextureAtlas(
+ tokenAtlas, Texture.TextureFilter.Linear, Texture.TextureFilter.Linear, false);
+ }
+
+ public TextureRegion fetch(String regionName) {
+ var region = fetchedRegions.get(regionName);
+ if (region != null) {
+ return region;
+ }
+
+ region = tokenAtlas.findRegion(regionName);
+ if (region == null) {
+ region = sharedAtlas.findRegion(regionName);
+ }
+
+ if (region != null) {
+ fetchedRegions.put(regionName, region);
+ }
+ return region;
+ }
+
+ public Sprite getSprite(String name) {
+ var sprite = fetchedSprites.get(name);
+ if (sprite != null) {
+ var region = fetchedRegions.get(name);
+ sprite.setSize(region.getRegionWidth(), region.getRegionHeight());
+ return sprite;
+ }
+
+ var region = fetch(name);
+
+ if (region == null) {
+ var key = new MD5Key(name);
+ var image = ImageManager.getImage(key);
+ if (image == ImageManager.TRANSFERING_IMAGE) {
+ return TRANSFERING_SPRITE;
+ }
+
+ if (image == ImageManager.BROKEN_IMAGE) {
+ return BROKEN_SPRITE;
+ }
+
+ imageToSprite(key, image);
+
+ region = fetch(name);
+ }
+
+ if (region == null) {
+ sprite = bigSprites.get(name);
+ } else {
+ sprite = new Sprite(region);
+ sprite.setSize(region.getRegionWidth(), region.getRegionHeight());
+ }
+
+ if (sprite == null) {
+ return BROKEN_SPRITE;
+ }
+
+ fetchedSprites.put(name, sprite);
+ return sprite;
+ }
+
+ public Sprite getIsoSprite(MD5Key key) {
+ if (isoSprites.containsKey(key)) {
+ return isoSprites.get(key);
+ }
+
+ var workImage = IsometricGrid.isoImage(ImageManager.getImage(key));
+
+ byte[] bytes;
+ try {
+ bytes = ImageUtil.imageToBytes(workImage, "png");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ var pix = new Pixmap(bytes, 0, bytes.length);
+ var image = new Sprite(new Texture(pix));
+ pix.dispose();
+ isoSprites.put(key, image);
+ return image;
+ }
+
+ public Sprite getSprite(MD5Key key, float stateTime) {
+ if (key == null) return null;
+
+ var videoPlayer = videoPlayerMap.get(key);
+ if (videoPlayer != null) {
+ boolean skip = false;
+ if (!videoPlayer.isPlaying()) {
+ try {
+ var file = AssetManager.getAssetCacheFile(key);
+ if (file.exists()) {
+ videoPlayer.play(Gdx.files.absolute(file.getAbsolutePath()));
+ videoPlayer.setVolume(0);
+ } else skip = true;
+
+ } catch (FileNotFoundException ex) {
+ log.warn(ex.toString());
+ skip = true;
+ }
+ }
+ if (!skip) {
+ videoPlayer.update();
+ var texture = videoPlayer.getTexture();
+ if (texture != null) {
+ var sprite = new Sprite(texture);
+ sprite.setSize(texture.getWidth(), texture.getHeight());
+ return sprite;
+ }
+ }
+ }
+
+ var animation = animationMap.get(key);
+ if (animation != null) {
+ var currentFrame = animation.getKeyFrame(stateTime, true);
+ var sprite = new Sprite(currentFrame);
+ sprite.setSize(currentFrame.getRegionWidth(), currentFrame.getRegionHeight());
+ return sprite;
+ }
+
+ var sprite = bigSprites.get(key);
+ if (sprite != null) {
+ sprite.setSize(sprite.getTexture().getWidth(), sprite.getTexture().getHeight());
+ return sprite;
+ }
+
+ return getSprite(key.toString());
+ }
+
+ public GdxPaint getPaint(DrawablePaint paint) {
+
+ if (paint instanceof DrawableColorPaint) {
+ var color = new Color();
+ Color.argb8888ToColor(color, ((DrawableColorPaint) paint).getColor());
+ return new GdxPaint(color, null);
+ }
+
+ var texturePaint = (DrawableTexturePaint) paint;
+ var asset = texturePaint.getAsset();
+ if (!paintTextures.containsKey(asset.getMD5Key())) {
+ var image = asset.getData();
+ var pix = new Pixmap(image, 0, image.length);
+ var texture = new Texture(pix);
+ texture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat);
+ pix.dispose();
+ paintTextures.put(asset.getMD5Key(), texture);
+ }
+ return new GdxPaint(Color.WHITE, new TextureRegion(paintTextures.get(asset.getMD5Key())));
+ }
+
+ @Override
+ public void dispose() {
+ fetchedRegions.clear();
+ animationMap.clear();
+ fetchedSprites.clear();
+
+ Gdx.app.postRunnable(
+ () -> {
+ packer.dispose();
+ tokenAtlas.dispose();
+ whitePixel.dispose();
+
+ for (var sprite : isoSprites.values()) {
+ sprite.getTexture().dispose();
+ }
+ isoSprites.clear();
+
+ for (var texture : paintTextures.values()) {
+ texture.dispose();
+ }
+ paintTextures.clear();
+
+ for (var sprite : bigSprites.values()) {
+ sprite.getTexture().dispose();
+ }
+ bigSprites.clear();
+ });
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/AbstractDrawingDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/AbstractDrawingDrawer.java
new file mode 100644
index 0000000000..9d3aa29689
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/AbstractDrawingDrawer.java
@@ -0,0 +1,96 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import com.badlogic.gdx.utils.FloatArray;
+import java.awt.geom.Area;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.client.ui.zone.gdx.ZoneCache;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.*;
+
+public abstract class AbstractDrawingDrawer {
+
+ protected Float alpha = null;
+ protected AreaRenderer areaRenderer;
+
+ protected ZoneCache zoneCache;
+
+ public void setZoneCache(ZoneCache zoneCache) {
+ this.zoneCache = zoneCache;
+ }
+
+ public AbstractDrawingDrawer(AreaRenderer areaRenderer) {
+ this.areaRenderer = areaRenderer;
+ }
+
+ public void draw(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ applyColor(pen.getBackgroundPaint(), true);
+ drawBackground(batch, zone, element, pen);
+
+ applyColor(pen.getPaint(), false);
+ drawBorder(batch, zone, element, pen);
+ }
+
+ protected void applyColor(DrawablePaint paint, boolean applyAlpha) {
+ var gdxPaint = zoneCache.getPaint(paint);
+ var color = gdxPaint.color();
+ var c2 = new Color().set(color);
+ if (alpha != null && applyAlpha) {
+
+ c2.set(color.r, color.g, color.b, alpha);
+ }
+
+ areaRenderer.setColor(c2);
+ // areaRenderer.setColor(gdxPaint.color());
+ if (gdxPaint.textureRegion() != null) {
+ areaRenderer.setTextureRegion(gdxPaint.textureRegion());
+ }
+ }
+
+ protected void line(PolygonSpriteBatch batch, Pen pen, float x1, float y1, float x2, float y2) {
+ var floats = new FloatArray();
+ // negate y values because we are y-up
+ floats.add(x1, -y1, x2, -y2);
+ var polygon =
+ areaRenderer.drawPathWithJoin(
+ floats,
+ pen.getThickness(),
+ pen.getSquareCap() ? AreaRenderer.JoinType.Pointy : AreaRenderer.JoinType.Round,
+ false);
+ applyColor(pen.getPaint(), false);
+ areaRenderer.paintPolygon(batch, polygon);
+ }
+
+ protected void fillArea(PolygonSpriteBatch batch, Area area, Pen pen) {
+ alpha = pen.getOpacity();
+ applyColor(pen.getBackgroundPaint(), true);
+ areaRenderer.fillArea(batch, area);
+ }
+
+ protected void drawArea(PolygonSpriteBatch batch, Area area, Pen pen) {
+ alpha = pen.getOpacity();
+ applyColor(pen.getPaint(), true);
+ areaRenderer.drawArea(batch, area, !pen.getSquareCap(), pen.getThickness());
+ }
+
+ protected abstract void drawBackground(
+ PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen);
+
+ protected abstract void drawBorder(
+ PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen);
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/AbstractTemplateDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/AbstractTemplateDrawer.java
new file mode 100644
index 0000000000..ea0c9ee36f
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/AbstractTemplateDrawer.java
@@ -0,0 +1,178 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.AbstractTemplate;
+import net.rptools.maptool.model.drawing.Drawable;
+import net.rptools.maptool.model.drawing.Pen;
+
+public abstract class AbstractTemplateDrawer extends AbstractDrawingDrawer {
+
+ public AbstractTemplateDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void drawBackground(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ alpha = AbstractTemplate.DEFAULT_BG_ALPHA;
+ paint(batch, pen, zone, (AbstractTemplate) element, false, true);
+ }
+
+ @Override
+ protected void drawBorder(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ paint(batch, pen, zone, (AbstractTemplate) element, true, false);
+ }
+
+ protected void paint(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ Zone zone,
+ AbstractTemplate template,
+ boolean border,
+ boolean area) {
+ var radius = template.getRadius();
+
+ if (radius == 0) return;
+
+ if (zone == null) return;
+
+ // Find the proper distance
+ int gridSize = zone.getGrid().getSize();
+ for (int y = 0; y < radius; y++) {
+ for (int x = 0; x < radius; x++) {
+
+ // Get the offset to the corner of the square
+ int xOff = x * gridSize;
+ int yOff = y * gridSize;
+
+ // Template specific painting
+ if (border)
+ paintBorder(batch, pen, template, x, y, xOff, yOff, gridSize, template.getDistance(x, y));
+ if (area)
+ paintArea(batch, pen, template, x, y, xOff, yOff, gridSize, template.getDistance(x, y));
+ } // endfor
+ } // endfor
+ }
+
+ protected void paintArea(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int xOff,
+ int yOff,
+ int gridSize,
+ AbstractTemplate.Quadrant q) {
+ var vertex = template.getVertex();
+ int x = vertex.x + getXMult(q) * xOff + ((getXMult(q) - 1) / 2) * gridSize;
+ int y = vertex.y + getYMult(q) * yOff + ((getYMult(q) - 1) / 2) * gridSize;
+ var floats =
+ new float[] {x, -y - gridSize, x, -y, x + gridSize, -y, x + gridSize, -y - gridSize};
+ applyColor(pen.getBackgroundPaint(), true);
+ areaRenderer.paintVertices(batch, floats, null);
+ }
+
+ protected int getXMult(AbstractTemplate.Quadrant q) {
+ return ((q == AbstractTemplate.Quadrant.NORTH_WEST || q == AbstractTemplate.Quadrant.SOUTH_WEST)
+ ? -1
+ : +1);
+ }
+
+ protected int getYMult(AbstractTemplate.Quadrant q) {
+ return ((q == AbstractTemplate.Quadrant.NORTH_WEST || q == AbstractTemplate.Quadrant.NORTH_EAST)
+ ? -1
+ : +1);
+ }
+
+ protected void paintCloseVerticalBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int xOff,
+ int yOff,
+ int gridSize,
+ AbstractTemplate.Quadrant q) {
+ var vertex = template.getVertex();
+ int x = vertex.x + getXMult(q) * xOff;
+ int y = vertex.y + getYMult(q) * yOff;
+ line(batch, pen, x, y, x, y + getYMult(q) * gridSize);
+ }
+
+ protected void paintFarHorizontalBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int xOff,
+ int yOff,
+ int gridSize,
+ AbstractTemplate.Quadrant q) {
+ var vertex = template.getVertex();
+ int x = vertex.x + getXMult(q) * xOff;
+ int y = vertex.y + getYMult(q) * yOff + getYMult(q) * gridSize;
+ line(batch, pen, x, y, x + getXMult(q) * gridSize, y);
+ }
+
+ protected void paintFarVerticalBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int xOff,
+ int yOff,
+ int gridSize,
+ AbstractTemplate.Quadrant q) {
+ var vertex = template.getVertex();
+ int x = vertex.x + getXMult(q) * xOff + getXMult(q) * gridSize;
+ int y = vertex.y + getYMult(q) * yOff;
+ line(batch, pen, x, y, x, y + getYMult(q) * gridSize);
+ }
+
+ protected void paintCloseHorizontalBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int xOff,
+ int yOff,
+ int gridSize,
+ AbstractTemplate.Quadrant q) {
+ var vertex = template.getVertex();
+ int x = vertex.x + getXMult(q) * xOff;
+ int y = vertex.y + getYMult(q) * yOff;
+ line(batch, pen, x, y, x + getXMult(q) * gridSize, y);
+ }
+
+ protected abstract void paintArea(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance);
+
+ protected abstract void paintBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance);
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/BlastTemplateDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/BlastTemplateDrawer.java
new file mode 100644
index 0000000000..4437a4032a
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/BlastTemplateDrawer.java
@@ -0,0 +1,43 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.AbstractTemplate;
+import net.rptools.maptool.model.drawing.BlastTemplate;
+import net.rptools.maptool.model.drawing.Drawable;
+import net.rptools.maptool.model.drawing.Pen;
+
+public class BlastTemplateDrawer extends AbstractDrawingDrawer {
+
+ public BlastTemplateDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void drawBackground(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ var template = (BlastTemplate) element;
+ alpha = AbstractTemplate.DEFAULT_BG_ALPHA;
+ fillArea(batch, template.getArea(zone), pen);
+ }
+
+ @Override
+ protected void drawBorder(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ var template = (BlastTemplate) element;
+ drawArea(batch, template.getArea(zone), pen);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/BurstTemplateDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/BurstTemplateDrawer.java
new file mode 100644
index 0000000000..01bfdf1907
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/BurstTemplateDrawer.java
@@ -0,0 +1,45 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import java.awt.geom.Area;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.AbstractTemplate;
+import net.rptools.maptool.model.drawing.BurstTemplate;
+import net.rptools.maptool.model.drawing.Drawable;
+import net.rptools.maptool.model.drawing.Pen;
+
+public class BurstTemplateDrawer extends AbstractDrawingDrawer {
+
+ public BurstTemplateDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void drawBackground(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ var template = (BurstTemplate) element;
+ alpha = AbstractTemplate.DEFAULT_BG_ALPHA;
+ fillArea(batch, template.getArea(zone), pen);
+ }
+
+ @Override
+ protected void drawBorder(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ var template = (BurstTemplate) element;
+ drawArea(batch, template.getArea(zone), pen);
+ drawArea(batch, new Area(template.makeShape(zone)), pen);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/ConeTemplateDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/ConeTemplateDrawer.java
new file mode 100644
index 0000000000..7d47fc7f15
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/ConeTemplateDrawer.java
@@ -0,0 +1,222 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.drawing.AbstractTemplate;
+import net.rptools.maptool.model.drawing.ConeTemplate;
+import net.rptools.maptool.model.drawing.Pen;
+
+public class ConeTemplateDrawer extends RadiusTemplateDrawer {
+
+ public ConeTemplateDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void paintArea(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+ var coneTemplate = (ConeTemplate) template;
+
+ var direction = coneTemplate.getDirection();
+
+ // Drawing along the spines only?
+ if ((direction == AbstractTemplate.Direction.EAST
+ || direction == AbstractTemplate.Direction.WEST)
+ && y > x) return;
+ if ((direction == AbstractTemplate.Direction.NORTH
+ || direction == AbstractTemplate.Direction.SOUTH)
+ && x > y) return;
+
+ // Only squares w/in the radius
+ if (distance > coneTemplate.getRadius()) {
+ return;
+ }
+ for (AbstractTemplate.Quadrant q : AbstractTemplate.Quadrant.values()) {
+ if (coneTemplate.withinQuadrant(q)) {
+ paintArea(batch, pen, template, xOff, yOff, gridSize, q);
+ }
+ }
+ }
+
+ @Override
+ protected void paintBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+ var coneTemplate = (ConeTemplate) template;
+ paintBorderAtRadius(
+ batch, pen, coneTemplate, x, y, xOff, yOff, gridSize, distance, coneTemplate.getRadius());
+ paintEdges(batch, pen, coneTemplate, x, y, xOff, yOff, gridSize, distance);
+ }
+
+ protected void paintEdges(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ ConeTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+
+ // Handle the edges
+ int radius = template.getRadius();
+ var direction = template.getDirection();
+ if (direction.ordinal() % 2 == 0) {
+ if (x == 0) {
+ if (direction == AbstractTemplate.Direction.SOUTH_EAST
+ || direction == AbstractTemplate.Direction.SOUTH_WEST)
+ paintCloseVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ if (direction == AbstractTemplate.Direction.NORTH_EAST
+ || direction == AbstractTemplate.Direction.NORTH_WEST)
+ paintCloseVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ } // endif
+ if (y == 0) {
+ if (direction == AbstractTemplate.Direction.SOUTH_EAST
+ || direction == AbstractTemplate.Direction.NORTH_EAST)
+ paintCloseHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ if (direction == AbstractTemplate.Direction.SOUTH_WEST
+ || direction == AbstractTemplate.Direction.NORTH_WEST)
+ paintCloseHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ } // endif
+ } else if (direction.ordinal() % 2 == 1 && x == y && distance <= radius) {
+ if (direction == AbstractTemplate.Direction.SOUTH) {
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ paintCloseHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ paintCloseHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ } // endif
+ if (direction == AbstractTemplate.Direction.NORTH) {
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ paintCloseHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ paintCloseHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ } // endif
+ if (direction == AbstractTemplate.Direction.EAST) {
+ paintCloseVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ paintCloseVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ } // endif
+ if (direction == AbstractTemplate.Direction.WEST) {
+ paintCloseVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ paintCloseVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ } // endif
+ } // endif
+ }
+
+ protected void paintBorderAtRadius(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ ConeTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance,
+ int radius) {
+ // At the border?
+ if (distance == radius) {
+ var direction = template.getDirection();
+ // Paint lines between vertical boundaries if needed
+ if (template.getDistance(x + 1, y) > radius) {
+ if (direction == AbstractTemplate.Direction.SOUTH_EAST
+ || (direction == AbstractTemplate.Direction.SOUTH && y >= x)
+ || (direction == AbstractTemplate.Direction.EAST && x >= y))
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ if (direction == AbstractTemplate.Direction.NORTH_EAST
+ || (direction == AbstractTemplate.Direction.NORTH && y >= x)
+ || (direction == AbstractTemplate.Direction.EAST && x >= y))
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ if (direction == AbstractTemplate.Direction.SOUTH_WEST
+ || (direction == AbstractTemplate.Direction.SOUTH && y >= x)
+ || (direction == AbstractTemplate.Direction.WEST && x >= y))
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ if (direction == AbstractTemplate.Direction.NORTH_WEST
+ || (direction == AbstractTemplate.Direction.NORTH && y >= x)
+ || (direction == AbstractTemplate.Direction.WEST && x >= y))
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ } // endif
+
+ // Paint lines between horizontal boundaries if needed
+ if (template.getDistance(x, y + 1) > radius) {
+ if (direction == AbstractTemplate.Direction.SOUTH_EAST
+ || (direction == AbstractTemplate.Direction.SOUTH && y >= x)
+ || (direction == AbstractTemplate.Direction.EAST && x >= y))
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ if (direction == AbstractTemplate.Direction.SOUTH_WEST
+ || (direction == AbstractTemplate.Direction.SOUTH && y >= x)
+ || (direction == AbstractTemplate.Direction.WEST && x >= y))
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ if (direction == AbstractTemplate.Direction.NORTH_EAST
+ || (direction == AbstractTemplate.Direction.NORTH && y >= x)
+ || (direction == AbstractTemplate.Direction.EAST && x >= y))
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ if (direction == AbstractTemplate.Direction.NORTH_WEST
+ || (direction == AbstractTemplate.Direction.NORTH && y >= x)
+ || (direction == AbstractTemplate.Direction.WEST && x >= y))
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ } // endif
+ } // endif
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/DrawnElementRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/DrawnElementRenderer.java
new file mode 100644
index 0000000000..2e677dc437
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/DrawnElementRenderer.java
@@ -0,0 +1,81 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import java.util.List;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.client.ui.zone.gdx.ZoneCache;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.*;
+
+public class DrawnElementRenderer {
+ private final LineTemplateDrawer lineTemplateDrawer;
+ private final LineCellTemplateDrawer lineCellTemplateDrawer;
+ private final RadiusTemplateDrawer radiusTemplateDrawer;
+ private final BurstTemplateDrawer burstTemplateDrawer;
+ private final ConeTemplateDrawer coneTemplateDrawer;
+ private final BlastTemplateDrawer blastTemplateDrawer;
+ private final RadiusCellTemplateDrawer radiusCellTemplateDrawer;
+ private final ShapeDrawableDrawer shapeDrawableDrawer;
+
+ public DrawnElementRenderer(AreaRenderer areaRenderer) {
+ lineTemplateDrawer = new LineTemplateDrawer(areaRenderer);
+ lineCellTemplateDrawer = new LineCellTemplateDrawer(areaRenderer);
+ radiusTemplateDrawer = new RadiusTemplateDrawer(areaRenderer);
+ burstTemplateDrawer = new BurstTemplateDrawer(areaRenderer);
+ coneTemplateDrawer = new ConeTemplateDrawer(areaRenderer);
+ blastTemplateDrawer = new BlastTemplateDrawer(areaRenderer);
+ radiusCellTemplateDrawer = new RadiusCellTemplateDrawer(areaRenderer);
+ shapeDrawableDrawer = new ShapeDrawableDrawer(areaRenderer);
+ }
+
+ public void render(PolygonSpriteBatch batch, Zone zone, List drawables) {
+ for (var drawable : drawables) renderDrawable(batch, zone, drawable);
+ }
+
+ private void renderDrawable(PolygonSpriteBatch batch, Zone zone, DrawnElement element) {
+ var pen = element.getPen();
+ var drawable = element.getDrawable();
+
+ if (drawable instanceof ShapeDrawable) shapeDrawableDrawer.draw(batch, zone, drawable, pen);
+ else if (drawable instanceof DrawablesGroup)
+ for (var groupElement : ((DrawablesGroup) drawable).getDrawableList())
+ renderDrawable(batch, zone, groupElement);
+ else if (drawable instanceof RadiusCellTemplate)
+ radiusCellTemplateDrawer.draw(batch, zone, drawable, pen);
+ else if (drawable instanceof LineCellTemplate)
+ lineCellTemplateDrawer.draw(batch, zone, drawable, pen);
+ else if (drawable instanceof BlastTemplate)
+ blastTemplateDrawer.draw(batch, zone, drawable, pen);
+ else if (drawable instanceof ConeTemplate) coneTemplateDrawer.draw(batch, zone, drawable, pen);
+ else if (drawable instanceof BurstTemplate)
+ burstTemplateDrawer.draw(batch, zone, drawable, pen);
+ else if (drawable instanceof RadiusTemplate)
+ radiusTemplateDrawer.draw(batch, zone, drawable, pen);
+ else if (drawable instanceof LineTemplate) lineTemplateDrawer.draw(batch, zone, drawable, pen);
+ }
+
+ public void setZoneCache(ZoneCache zoneCache) {
+ lineTemplateDrawer.setZoneCache(zoneCache);
+ lineCellTemplateDrawer.setZoneCache(zoneCache);
+ radiusTemplateDrawer.setZoneCache(zoneCache);
+ burstTemplateDrawer.setZoneCache(zoneCache);
+ coneTemplateDrawer.setZoneCache(zoneCache);
+ blastTemplateDrawer.setZoneCache(zoneCache);
+ radiusCellTemplateDrawer.setZoneCache(zoneCache);
+ shapeDrawableDrawer.setZoneCache(zoneCache);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/LineCellTemplateDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/LineCellTemplateDrawer.java
new file mode 100644
index 0000000000..610c44a19f
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/LineCellTemplateDrawer.java
@@ -0,0 +1,132 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import java.util.ListIterator;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.CellPoint;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.AbstractTemplate;
+import net.rptools.maptool.model.drawing.LineCellTemplate;
+import net.rptools.maptool.model.drawing.Pen;
+
+public class LineCellTemplateDrawer extends AbstractTemplateDrawer {
+
+ public LineCellTemplateDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void paintArea(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+ var lineCellTemplate = (LineCellTemplate) template;
+ paintArea(batch, pen, template, xOff, yOff, gridSize, lineCellTemplate.getQuadrant());
+ }
+
+ @Override
+ protected void paintBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int pElement) {
+ var lineCellTemplate = (LineCellTemplate) template;
+ // Have to scan 3 points behind and ahead, since that is the maximum number of points
+ // that can be added to the path from any single intersection.
+ boolean[] noPaint = new boolean[4];
+ var path = lineCellTemplate.getPath();
+ for (int i = pElement - 3; i < pElement + 3; i++) {
+ if (i < 0 || i >= path.size() || i == pElement) continue;
+ CellPoint p = path.get(i);
+
+ // Ignore diagonal cells and cells that are not adjacent
+ int dx = p.x - x;
+ int dy = p.y - y;
+ if (Math.abs(dx) == Math.abs(dy) || Math.abs(dx) > 1 || Math.abs(dy) > 1) continue;
+
+ // Remove the border between the 2 points
+ noPaint[dx != 0 ? (dx < 0 ? 0 : 2) : (dy < 0 ? 3 : 1)] = true;
+ } // endif
+
+ var quadrant = lineCellTemplate.getQuadrant();
+ // Paint the borders as needed
+ if (!noPaint[0]) paintCloseVerticalBorder(batch, pen, template, xOff, yOff, gridSize, quadrant);
+ if (!noPaint[1]) paintFarHorizontalBorder(batch, pen, template, xOff, yOff, gridSize, quadrant);
+ if (!noPaint[2]) paintFarVerticalBorder(batch, pen, template, xOff, yOff, gridSize, quadrant);
+ if (!noPaint[3])
+ paintCloseHorizontalBorder(batch, pen, template, xOff, yOff, gridSize, quadrant);
+ }
+
+ @Override
+ protected void paint(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ Zone zone,
+ AbstractTemplate template,
+ boolean border,
+ boolean area) {
+ if (zone == null) {
+ return;
+ }
+ var lineCellTemplate = (LineCellTemplate) template;
+ var path = lineCellTemplate.getPath();
+ // Need to paint? We need a line and to translate the painting
+ if (lineCellTemplate.getPathVertex() == null) return;
+ if (lineCellTemplate.getRadius() == 0) return;
+ if (path == null && lineCellTemplate.calcPath() == null) return;
+
+ var quadrant = lineCellTemplate.getQuadrant();
+
+ // Paint each element in the path
+ int gridSize = zone.getGrid().getSize();
+ ListIterator i = path.listIterator();
+ while (i.hasNext()) {
+ CellPoint p = i.next();
+ int xOff = p.x * gridSize;
+ int yOff = p.y * gridSize;
+ int distance = template.getDistance(p.x, p.y);
+
+ if (quadrant == AbstractTemplate.Quadrant.NORTH_EAST) {
+ yOff = yOff - gridSize;
+ } else if (quadrant == AbstractTemplate.Quadrant.SOUTH_WEST) {
+ xOff = xOff - gridSize;
+ } else if (quadrant == AbstractTemplate.Quadrant.NORTH_WEST) {
+ xOff = xOff - gridSize;
+ yOff = yOff - gridSize;
+ }
+
+ // Paint what is needed.
+ if (area) {
+ paintArea(batch, pen, template, p.x, p.y, xOff, yOff, gridSize, distance);
+ } // endif
+ if (border) {
+ paintBorder(batch, pen, template, p.x, p.y, xOff, yOff, gridSize, i.previousIndex());
+ } // endif
+ } // endfor
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/LineTemplateDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/LineTemplateDrawer.java
new file mode 100644
index 0000000000..2c7ccfd5fc
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/LineTemplateDrawer.java
@@ -0,0 +1,122 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import java.util.ListIterator;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.CellPoint;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.AbstractTemplate;
+import net.rptools.maptool.model.drawing.LineTemplate;
+import net.rptools.maptool.model.drawing.Pen;
+
+public class LineTemplateDrawer extends AbstractTemplateDrawer {
+ public LineTemplateDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void paint(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ Zone zone,
+ AbstractTemplate template,
+ boolean border,
+ boolean area) {
+ if (zone == null) {
+ return;
+ }
+ var lineTemplate = (LineTemplate) template;
+
+ // Need to paint? We need a line and to translate the painting
+ if (lineTemplate.getPathVertex() == null) return;
+ if (template.getRadius() == 0) return;
+ if (lineTemplate.getPath() == null && lineTemplate.calcPath() == null) return;
+
+ // Paint each element in the path
+ int gridSize = zone.getGrid().getSize();
+ ListIterator i = lineTemplate.getPath().listIterator();
+ while (i.hasNext()) {
+ CellPoint p = i.next();
+ int xOff = p.x * gridSize;
+ int yOff = p.y * gridSize;
+ int distance = template.getDistance(p.x, p.y);
+
+ // Paint what is needed.
+ if (area) {
+ paintArea(batch, pen, template, p.x, p.y, xOff, yOff, gridSize, distance);
+ } // endif
+ if (border) {
+ paintBorder(batch, pen, template, p.x, p.y, xOff, yOff, gridSize, i.previousIndex());
+ } // endif
+ } // endfor
+ }
+
+ @Override
+ protected void paintArea(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+ var lineTemplate = (LineTemplate) template;
+
+ paintArea(batch, pen, template, xOff, yOff, gridSize, lineTemplate.getQuadrant());
+ }
+
+ @Override
+ protected void paintBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int pElement) {
+ var lineTemplate = (LineTemplate) template;
+
+ // Have to scan 3 points behind and ahead, since that is the maximum number of points
+ // that can be added to the path from any single intersection.
+ boolean[] noPaint = new boolean[4];
+ var path = lineTemplate.getPath();
+ for (int i = pElement - 3; i < pElement + 3; i++) {
+ if (i < 0 || i >= path.size() || i == pElement) continue;
+ CellPoint p = path.get(i);
+
+ // Ignore diagonal cells and cells that are not adjacent
+ int dx = p.x - x;
+ int dy = p.y - y;
+ if (Math.abs(dx) == Math.abs(dy) || Math.abs(dx) > 1 || Math.abs(dy) > 1) continue;
+
+ // Remove the border between the 2 points
+ noPaint[dx != 0 ? (dx < 0 ? 0 : 2) : (dy < 0 ? 3 : 1)] = true;
+ } // endif
+
+ var quadrant = lineTemplate.getQuadrant();
+ // Paint the borders as needed
+ if (!noPaint[0]) paintCloseVerticalBorder(batch, pen, template, xOff, yOff, gridSize, quadrant);
+ if (!noPaint[1]) paintFarHorizontalBorder(batch, pen, template, xOff, yOff, gridSize, quadrant);
+ if (!noPaint[2]) paintFarVerticalBorder(batch, pen, template, xOff, yOff, gridSize, quadrant);
+ if (!noPaint[3])
+ paintCloseHorizontalBorder(batch, pen, template, xOff, yOff, gridSize, quadrant);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/RadiusCellTemplateDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/RadiusCellTemplateDrawer.java
new file mode 100644
index 0000000000..a148c0cb52
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/RadiusCellTemplateDrawer.java
@@ -0,0 +1,231 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.ZonePoint;
+import net.rptools.maptool.model.drawing.AbstractTemplate;
+import net.rptools.maptool.model.drawing.Pen;
+
+public class RadiusCellTemplateDrawer extends AbstractTemplateDrawer {
+
+ public RadiusCellTemplateDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void paintArea(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+ // Only squares w/in the radius
+ int radius = template.getRadius();
+ if (distance <= radius) {
+ paintArea(batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ }
+
+ if (template.getDistance(x, y + 1) <= radius) {
+ paintArea(batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ }
+
+ if (template.getDistance(x + 1, y) <= radius) {
+ paintArea(batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ }
+
+ if (template.getDistance(x + 1, y + 1) <= radius) {
+ paintArea(batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ }
+ }
+
+ @Override
+ protected void paintArea(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int xOff,
+ int yOff,
+ int gridSize,
+ AbstractTemplate.Quadrant q) {
+ ZonePoint vertex = template.getVertex();
+ int x = vertex.x + getXMult(q) * xOff + ((getXMult(q) - 1) / 2) * gridSize;
+ int y = vertex.y + getYMult(q) * yOff + ((getYMult(q) - 1) / 2) * gridSize;
+ var floats =
+ new float[] {
+ x, -y - gridSize, x, -y, x + gridSize, -y, x + gridSize, -y - gridSize,
+ };
+ applyColor(pen.getBackgroundPaint(), true);
+ areaRenderer.paintVertices(batch, floats, null);
+ }
+
+ @Override
+ protected int getXMult(AbstractTemplate.Quadrant q) {
+ return ((q == AbstractTemplate.Quadrant.NORTH_WEST || q == AbstractTemplate.Quadrant.SOUTH_WEST)
+ ? -1
+ : +1);
+ }
+
+ @Override
+ protected int getYMult(AbstractTemplate.Quadrant q) {
+ return ((q == AbstractTemplate.Quadrant.NORTH_WEST || q == AbstractTemplate.Quadrant.NORTH_EAST)
+ ? -1
+ : +1);
+ }
+
+ @Override
+ protected void paintBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+ paintBorderAtRadius(
+ batch, pen, template, x, y, xOff, yOff, gridSize, distance, template.getRadius());
+ }
+
+ protected void paintBorderAtRadius(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance,
+ int radius) {
+ // At the border?
+ // Paint lines between vertical boundaries if needed
+
+ if (template.getDistance(x, y + 1) == radius && template.getDistance(x + 1, y + 1) > radius) {
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ }
+ if (distance == radius && template.getDistance(x + 1, y) > radius) {
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ }
+ if (template.getDistance(x + 1, y + 1) == radius
+ && template.getDistance(x + 2, y + 1) > radius) {
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ }
+ if (template.getDistance(x + 1, y) == radius && template.getDistance(x + 2, y) > radius) {
+ paintFarVerticalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ } // endif
+ if (x == 0 && y + 1 == radius) {
+ paintFarVerticalBorder(
+ batch,
+ pen,
+ template,
+ xOff - gridSize,
+ yOff,
+ gridSize,
+ AbstractTemplate.Quadrant.SOUTH_EAST);
+ }
+ if (x == 0 && y + 2 == radius) {
+ paintFarVerticalBorder(
+ batch,
+ pen,
+ template,
+ xOff - gridSize,
+ yOff,
+ gridSize,
+ AbstractTemplate.Quadrant.NORTH_WEST);
+ }
+
+ // Paint lines between horizontal boundaries if needed
+ if (template.getDistance(x, y + 1) == radius && template.getDistance(x, y + 2) > radius) {
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_EAST);
+ }
+ if (template.getDistance(x, y) == radius && template.getDistance(x, y + 1) > radius) {
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_EAST);
+ }
+ if (y == 0 && x + 1 == radius) {
+ paintFarHorizontalBorder(
+ batch,
+ pen,
+ template,
+ xOff,
+ yOff - gridSize,
+ gridSize,
+ AbstractTemplate.Quadrant.SOUTH_EAST);
+ }
+ if (y == 0 && x + 2 == radius) {
+ paintFarHorizontalBorder(
+ batch,
+ pen,
+ template,
+ xOff,
+ yOff - gridSize,
+ gridSize,
+ AbstractTemplate.Quadrant.NORTH_WEST);
+ }
+ if (template.getDistance(x + 1, y + 1) == radius
+ && template.getDistance(x + 1, y + 2) > radius) {
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.NORTH_WEST);
+ }
+ if (template.getDistance(x + 1, y) == radius && template.getDistance(x + 1, y + 1) > radius) {
+ paintFarHorizontalBorder(
+ batch, pen, template, xOff, yOff, gridSize, AbstractTemplate.Quadrant.SOUTH_WEST);
+ } // endif
+ }
+
+ @Override
+ protected void paint(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ Zone zone,
+ AbstractTemplate template,
+ boolean border,
+ boolean area) {
+ int radius = template.getRadius();
+
+ if (radius == 0) return;
+ if (zone == null) return;
+
+ // Find the proper distance
+ int gridSize = zone.getGrid().getSize();
+ for (int y = 0; y < radius; y++) {
+ for (int x = 0; x < radius; x++) {
+
+ // Get the offset to the corner of the square
+ int xOff = x * gridSize;
+ int yOff = y * gridSize;
+
+ // Template specific painting
+ if (border)
+ paintBorder(batch, pen, template, x, y, xOff, yOff, gridSize, template.getDistance(x, y));
+ if (area)
+ paintArea(batch, pen, template, x, y, xOff, yOff, gridSize, template.getDistance(x, y));
+ } // endfor
+ } // endfor
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/RadiusTemplateDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/RadiusTemplateDrawer.java
new file mode 100644
index 0000000000..01a69048b0
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/RadiusTemplateDrawer.java
@@ -0,0 +1,93 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.drawing.AbstractTemplate;
+import net.rptools.maptool.model.drawing.Pen;
+import net.rptools.maptool.model.drawing.RadiusTemplate;
+
+public class RadiusTemplateDrawer extends AbstractTemplateDrawer {
+ public RadiusTemplateDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void paintArea(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+ var radiusTemplate = (RadiusTemplate) template;
+ // Only squares w/in the radius
+ if (distance <= radiusTemplate.getRadius()) {
+ // Paint the squares
+ for (AbstractTemplate.Quadrant q : AbstractTemplate.Quadrant.values()) {
+ paintArea(batch, pen, template, xOff, yOff, gridSize, q);
+ }
+ }
+ }
+
+ @Override
+ protected void paintBorder(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance) {
+ var radiusTemplate = (RadiusTemplate) template;
+ paintBorderAtRadius(
+ batch, pen, template, x, y, xOff, yOff, gridSize, distance, radiusTemplate.getRadius());
+ }
+
+ private void paintBorderAtRadius(
+ PolygonSpriteBatch batch,
+ Pen pen,
+ AbstractTemplate template,
+ int x,
+ int y,
+ int xOff,
+ int yOff,
+ int gridSize,
+ int distance,
+ int radius) {
+ // At the border?
+ if (distance == radius) {
+ // Paint lines between vertical boundaries if needed
+ if (template.getDistance(x + 1, y) > radius) {
+ for (AbstractTemplate.Quadrant q : AbstractTemplate.Quadrant.values()) {
+ paintFarVerticalBorder(batch, pen, template, xOff, yOff, gridSize, q);
+ }
+ }
+
+ // Paint lines between horizontal boundaries if needed
+ if (template.getDistance(x, y + 1) > radius) {
+ for (AbstractTemplate.Quadrant q : AbstractTemplate.Quadrant.values()) {
+ paintFarHorizontalBorder(batch, pen, template, xOff, yOff, gridSize, q);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/ShapeDrawableDrawer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/ShapeDrawableDrawer.java
new file mode 100644
index 0000000000..db3423a3e9
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/drawing/ShapeDrawableDrawer.java
@@ -0,0 +1,41 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.drawing;
+
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import net.rptools.maptool.client.ui.zone.gdx.AreaRenderer;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.drawing.Drawable;
+import net.rptools.maptool.model.drawing.Pen;
+import net.rptools.maptool.model.drawing.ShapeDrawable;
+
+public class ShapeDrawableDrawer extends AbstractDrawingDrawer {
+
+ public ShapeDrawableDrawer(AreaRenderer renderer) {
+ super(renderer);
+ }
+
+ @Override
+ protected void drawBackground(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ var shape = (ShapeDrawable) element;
+ fillArea(batch, shape.getArea(zone), pen);
+ }
+
+ @Override
+ protected void drawBorder(PolygonSpriteBatch batch, Zone zone, Drawable element, Pen pen) {
+ var shape = (ShapeDrawable) element;
+ drawArea(batch, shape.getArea(zone), pen);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/ItemRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/ItemRenderer.java
new file mode 100644
index 0000000000..af1699f825
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/ItemRenderer.java
@@ -0,0 +1,21 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.label;
+
+import com.badlogic.gdx.graphics.Camera;
+
+public interface ItemRenderer {
+ void render(Camera camera, float zoom);
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/LabelRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/LabelRenderer.java
new file mode 100644
index 0000000000..c3f05d703a
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/LabelRenderer.java
@@ -0,0 +1,45 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.label;
+
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.math.Vector3;
+import javax.swing.*;
+
+public class LabelRenderer implements ItemRenderer {
+ private final TextRenderer renderer;
+ private float x;
+ private float y;
+ private String text;
+ private Vector3 tmpWorldCoord = new Vector3();
+ private Vector3 tmpScreenCoord = new Vector3();
+
+ public LabelRenderer(String text, float x, float y, TextRenderer renderer) {
+ this.x = x;
+ this.y = y;
+ this.text = text;
+ this.renderer = renderer;
+ }
+
+ @Override
+ public void render(Camera camera, float zoom) {
+ tmpWorldCoord.x = x;
+ tmpWorldCoord.y = y;
+ tmpWorldCoord.z = 0;
+ tmpScreenCoord = camera.project(tmpWorldCoord);
+
+ renderer.drawBoxedString(text, tmpScreenCoord.x, tmpScreenCoord.y, SwingUtilities.CENTER);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/TextRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/TextRenderer.java
new file mode 100644
index 0000000000..8af9c82b38
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/TextRenderer.java
@@ -0,0 +1,130 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.label;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.*;
+import javax.swing.*;
+
+public class TextRenderer {
+ public enum Background {
+ Gray,
+ Blue,
+ DarkGray
+ }
+
+ private GlyphLayout glyphLayout = new GlyphLayout();
+ private NinePatch blueLabel;
+ private NinePatch grayLabel;
+ private NinePatch darkGrayLabel;
+ private Batch batch;
+ private BitmapFont font;
+
+ private boolean scaling = true;
+
+ public TextRenderer(TextureAtlas atlas, Batch batch, BitmapFont font) {
+ this.font = font;
+ this.batch = batch;
+ blueLabel = atlas.createPatch("blueLabelbox");
+ grayLabel = atlas.createPatch("grayLabelbox");
+ darkGrayLabel = atlas.createPatch("darkGreyLabelbox");
+ }
+
+ public TextRenderer(TextureAtlas atlas, Batch batch, BitmapFont font, boolean scaling) {
+ this.font = font;
+ this.batch = batch;
+ blueLabel = atlas.createPatch("blueLabelbox");
+ grayLabel = atlas.createPatch("grayLabelbox");
+ darkGrayLabel = atlas.createPatch("darkGreyLabelbox");
+ this.scaling = scaling;
+ }
+
+ public BitmapFont getFont() {
+ return font;
+ }
+
+ public void drawString(String text, float centerX, float centerY, Color foreground) {
+ drawBoxedString(text, centerX, centerY, SwingUtilities.CENTER, null, foreground);
+ }
+
+ public void drawString(String text, float centerX, float centerY) {
+ drawBoxedString(text, centerX, centerY, SwingUtilities.CENTER, null, Color.WHITE);
+ }
+
+ public void drawBoxedString(String text, float centerX, float centerY) {
+ drawBoxedString(text, centerX, centerY, SwingUtilities.CENTER);
+ }
+
+ public void drawBoxedString(String text, float x, float y, int justification) {
+ drawBoxedString(text, x, y, justification, Background.Gray, Color.BLACK);
+ }
+
+ public void drawBoxedString(
+ String text, float x, float y, int justification, Background background, Color foreground) {
+ NinePatch backgroundPatch = null;
+ if (background != null) {
+ switch (background) {
+ case Gray -> {
+ backgroundPatch = grayLabel;
+ }
+ case Blue -> {
+ backgroundPatch = blueLabel;
+ }
+ case DarkGray -> {
+ backgroundPatch = darkGrayLabel;
+ }
+ }
+ }
+
+ var BOX_PADDINGX = 10;
+ var BOX_PADDINGY = 2;
+
+ if (text == null) text = "";
+
+ // the font size was already scaled. So don't scale it here.
+ glyphLayout.setText(font, text);
+ var strWidth = glyphLayout.width;
+ var fontHeight = font.getLineHeight();
+
+ var width = strWidth + BOX_PADDINGX * 2;
+ var height = fontHeight + BOX_PADDINGY * 2;
+
+ y = y - fontHeight / 2 - BOX_PADDINGY;
+
+ switch (justification) {
+ case SwingUtilities.CENTER:
+ x = x - strWidth / 2 - BOX_PADDINGX;
+ break;
+ case SwingUtilities.RIGHT:
+ x = x - strWidth - BOX_PADDINGX;
+ break;
+ case SwingUtilities.LEFT:
+ break;
+ }
+
+ // Box
+ if (backgroundPatch != null) {
+ backgroundPatch.draw(batch, x, y, width, height);
+ }
+
+ // Renderer message
+
+ var textX = x + BOX_PADDINGX;
+ var textY = y + height - BOX_PADDINGY - font.getAscent();
+
+ font.setColor(foreground);
+ font.draw(batch, text, textX, textY);
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/TokenLabelRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/TokenLabelRenderer.java
new file mode 100644
index 0000000000..327d60a7b2
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/gdx/label/TokenLabelRenderer.java
@@ -0,0 +1,90 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.gdx.label;
+
+import com.badlogic.gdx.graphics.Camera;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.math.Vector3;
+import javax.swing.*;
+import net.rptools.maptool.model.Token;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.util.GraphicsUtil;
+import net.rptools.maptool.util.StringUtil;
+
+public class TokenLabelRenderer implements ItemRenderer {
+ private final boolean isGMView;
+ private Token token;
+ private TextRenderer textRenderer;
+ private Vector3 tmpWorldCoord = new Vector3();
+ private Vector3 tmpScreenCoord = new Vector3();
+ private Zone zone;
+
+ public TokenLabelRenderer(Token token, Zone zone, boolean isGMView, TextRenderer textRenderer) {
+ this.token = token;
+ this.zone = zone;
+ this.isGMView = isGMView;
+ this.textRenderer = textRenderer;
+ }
+
+ @Override
+ public void render(Camera camera, float zoom) {
+ int offset = 3; // Keep it from tramping on the token border.
+ TextRenderer.Background background;
+ Color foreground;
+
+ if (token.isVisible()) {
+ if (token.getType() == Token.Type.NPC) {
+ background = TextRenderer.Background.Blue;
+ foreground = Color.WHITE;
+ } else {
+ background = TextRenderer.Background.Gray;
+ foreground = Color.BLACK;
+ }
+ } else {
+ background = TextRenderer.Background.DarkGray;
+ foreground = Color.WHITE;
+ }
+ String name = token.getName();
+ if (isGMView && token.getGMName() != null && !StringUtil.isEmpty(token.getGMName())) {
+ name += " (" + token.getGMName() + ")";
+ }
+
+ // Calculate image dimensions
+
+ float labelHeight = textRenderer.getFont().getLineHeight() + GraphicsUtil.BOX_PADDINGY * 2;
+
+ java.awt.Rectangle r = token.getBounds(zone);
+ tmpWorldCoord.x = r.x + r.width / 2;
+ tmpWorldCoord.y = (r.y + r.height + offset + labelHeight * zoom / 2) * -1;
+ tmpWorldCoord.z = 0;
+ tmpScreenCoord = camera.project(tmpWorldCoord);
+
+ textRenderer.drawBoxedString(
+ name, tmpScreenCoord.x, tmpScreenCoord.y, SwingUtilities.CENTER, background, foreground);
+
+ var label = token.getLabel();
+
+ // Draw name and label to image
+ if (label != null && label.trim().length() > 0) {
+ textRenderer.drawBoxedString(
+ label,
+ tmpScreenCoord.x,
+ tmpScreenCoord.y - labelHeight,
+ SwingUtilities.CENTER,
+ background,
+ foreground);
+ }
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java
index b297dde9d7..d3d991aeae 100644
--- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java
@@ -57,6 +57,7 @@
import net.rptools.maptool.client.ui.token.BarTokenOverlay;
import net.rptools.maptool.client.ui.token.dialog.create.NewTokenDialog;
import net.rptools.maptool.client.ui.zone.*;
+import net.rptools.maptool.client.ui.zone.gdx.GdxRenderer;
import net.rptools.maptool.client.walker.ZoneWalker;
import net.rptools.maptool.events.MapToolEventBus;
import net.rptools.maptool.language.I18N;
@@ -155,6 +156,20 @@ public class ZoneRenderer extends JComponent implements DropTargetListener {
private final VisionOverlayRenderer visionOverlayRenderer;
private final DebugRenderer debugRenderer;
+ public String getLoadingProgress() {
+ return loadingProgress;
+ }
+
+ public Token getTokenUnderMouse() {
+ return tokenUnderMouse;
+ }
+
+ public Map> getTokenStackMap() {
+ return tokenStackMap;
+ }
+
+ private boolean skipDrawing;
+
/**
* Constructor for the ZoneRenderer from a zone.
*
@@ -245,6 +260,10 @@ public void showPath(Token token, boolean show) {
}
}
+ public List getShowPathList() {
+ return showPathList;
+ }
+
/**
* If token is not null, center on it, set the active layer to it, select it, and request focus.
*
@@ -346,6 +365,10 @@ public void updateMoveSelectionSet(GUID keyToken, ZonePoint latestPoint) {
repaintDebouncer.dispatch(); // Jamz: may cause flicker when using AI
}
+ public Map getSelectionSetMap() {
+ return selectionSetMap;
+ }
+
public void toggleMoveSelectionSetWaypoint(GUID keyToken, ZonePoint location) {
SelectionSet set = selectionSetMap.get(keyToken);
if (set == null) {
@@ -543,6 +566,7 @@ public boolean isTokenMoving(Token token) {
protected void setViewOffset(int x, int y) {
zoneScale.setOffset(x, y);
+ GdxRenderer.getInstance().setScale(zoneScale);
}
public void centerOn(ZonePoint point) {
@@ -584,6 +608,7 @@ public void flush(Token token) {
tokenStackMap = null;
zoneView.flush(token);
+ GdxRenderer.getInstance().flushFog();
}
/**
@@ -622,6 +647,7 @@ public void flushLight() {
public void flushFog() {
visibleScreenArea = null;
repaintDebouncer.dispatch();
+ GdxRenderer.getInstance().flushFog();
}
/**
@@ -642,7 +668,6 @@ public void removeOverlay(ZoneOverlay overlay) {
}
public void moveViewBy(int dx, int dy) {
-
setViewOffset(getViewOffsetX() + dx, getViewOffsetY() + dy);
}
@@ -661,16 +686,19 @@ public void moveViewByCells(int dx, int dy) {
public void zoomReset(int x, int y) {
zoneScale.zoomReset(x, y);
MapTool.getFrame().getZoomStatusBar().update();
+ GdxRenderer.getInstance().setScale(zoneScale);
}
public void zoomIn(int x, int y) {
zoneScale.zoomIn(x, y);
MapTool.getFrame().getZoomStatusBar().update();
+ GdxRenderer.getInstance().setScale(zoneScale);
}
public void zoomOut(int x, int y) {
zoneScale.zoomOut(x, y);
MapTool.getFrame().getZoomStatusBar().update();
+ GdxRenderer.getInstance().setScale(zoneScale);
}
public void setView(int x, int y, double scale) {
@@ -679,6 +707,7 @@ public void setView(int x, int y, double scale) {
zoneScale.setScale(scale);
MapTool.getFrame().getZoomStatusBar().update();
+ GdxRenderer.getInstance().setScale(zoneScale);
}
public void enforceView(int x, int y, double scale, int gmWidth, int gmHeight) {
@@ -699,6 +728,7 @@ public void enforceView(int x, int y, double scale, int gmWidth, int gmHeight) {
setScale(scale);
centerOn(new ZonePoint(x, y));
+ GdxRenderer.getInstance().setScale(zoneScale);
}
public void restoreView() {
@@ -707,6 +737,7 @@ public void restoreView() {
centerOn(previousZonePoint);
setScale(previousScale);
+ GdxRenderer.getInstance().setScale(zoneScale);
}
public void forcePlayersView() {
@@ -733,6 +764,7 @@ public void paintComponent(Graphics g) {
timer.setThreshold(10);
timer.start("paintComponent");
+ skipDrawing = MapTool.getFrame().getGdxPanel().isVisible();
Graphics2D g2d = (Graphics2D) g;
@@ -752,27 +784,31 @@ public void paintComponent(Graphics g) {
PlayerView pl = getPlayerView();
timer.stop("paintComponent:createView");
- renderZone(bufferG2d, pl);
-
+ if (!skipDrawing) {
+ renderZone(bufferG2d, pl);
+ }
int noteVPos = 20;
bufferG2d.setFont(AppStyle.labelFont);
if (MapTool.getFrame().areFullScreenToolsShown()) {
noteVPos += 40;
}
if (!AppPreferences.mapVisibilityWarning.get()
- && (!zone.isVisible() && pl.isGMView())) {
+ && (!zone.isVisible() && pl.isGMView())
+ && !skipDrawing) {
GraphicsUtil.drawBoxedString(
bufferG2d, I18N.getText("zone.map_not_visible"), getSize().width / 2, noteVPos);
noteVPos += 20;
}
- if (AppState.isShowAsPlayer()) {
+ if (AppState.isShowAsPlayer() && !skipDrawing) {
GraphicsUtil.drawBoxedString(
bufferG2d, I18N.getText("zone.player_view"), getSize().width / 2, noteVPos);
}
timer.start("paintComponent:renderBuffer");
bufferG2d.dispose();
- g2d.drawImage(buffer, null, 0, 0);
+ if (!skipDrawing) {
+ g2d.drawImage(buffer, null, 0, 0);
+ }
timer.stop("paintComponent:renderBuffer");
}
@@ -843,7 +879,7 @@ public void disableLayer(Layer layer) {
disabledLayers.add(layer);
}
- private boolean shouldRenderLayer(Layer layer, PlayerView view) {
+ public boolean shouldRenderLayer(Layer layer, PlayerView view) {
return !disabledLayers.contains(layer) && (layer.isVisibleToPlayers() || view.isGMView());
}
@@ -888,16 +924,20 @@ public void renderZone(Graphics2D g2d, PlayerView view) {
}
// Are we still waiting to show the zone ?
if (isLoading()) {
- g2d.setColor(Color.black);
- g2d.fillRect(0, 0, viewRect.width, viewRect.height);
- GraphicsUtil.drawBoxedString(g2d, loadingProgress, viewRect.width / 2, viewRect.height / 2);
+ if (!skipDrawing) {
+ g2d.setColor(Color.black);
+ g2d.fillRect(0, 0, viewRect.width, viewRect.height);
+ GraphicsUtil.drawBoxedString(g2d, loadingProgress, viewRect.width / 2, viewRect.height / 2);
+ }
return;
}
if (MapTool.getCampaign().isBeingSerialized()) {
- g2d.setColor(Color.black);
- g2d.fillRect(0, 0, viewRect.width, viewRect.height);
- GraphicsUtil.drawBoxedString(
- g2d, " Please Wait ", viewRect.width / 2, viewRect.height / 2);
+ if (!skipDrawing) {
+ g2d.setColor(Color.black);
+ g2d.fillRect(0, 0, viewRect.width, viewRect.height);
+ GraphicsUtil.drawBoxedString(
+ g2d, " Please Wait ", viewRect.width / 2, viewRect.height / 2);
+ }
return;
}
if (zone == null) {
@@ -932,7 +972,7 @@ public void renderZone(Graphics2D g2d, PlayerView view) {
timer.stop("calcs-1");
// Rendering pipeline
- if (zone.drawBoard()) {
+ if (zone.drawBoard() && !skipDrawing) {
timer.start("board");
renderBoard(g2d, view);
timer.stop("board");
@@ -1100,7 +1140,8 @@ public void renderZone(Graphics2D g2d, PlayerView view) {
timer.start("lightSourceIconOverlay.paintOverlay");
if (shouldRenderLayer(Zone.Layer.TOKEN, view)
&& view.isGMView()
- && AppState.isShowLightSources()) {
+ && AppState.isShowLightSources()
+ && !skipDrawing) {
lightSourceIconOverlay.paintOverlay(this, g2d);
}
timer.stop("lightSourceIconOverlay.paintOverlay");
@@ -1113,6 +1154,7 @@ private void delayRendering(ItemRenderer renderer) {
}
private void renderRenderables(Graphics2D g) {
+ if (skipDrawing) return;
for (ItemRenderer renderer : itemRenderList) {
renderer.render(g);
}
@@ -1197,6 +1239,8 @@ public boolean isLoading() {
protected void renderDrawableOverlay(
Graphics g, DrawableRenderer renderer, PlayerView view, List drawnElements) {
+ if (skipDrawing) return;
+
Rectangle viewport =
new Rectangle(
zoneScale.getOffsetX(), zoneScale.getOffsetY(), getSize().width, getSize().height);
@@ -1260,7 +1304,7 @@ protected void renderBoard(Graphics2D g, PlayerView view) {
g.drawImage(backbuffer, 0, 0, this);
}
- private Set getOwnedMovementSet(PlayerView view) {
+ public Set getOwnedMovementSet(PlayerView view) {
Set movementSet = new HashSet();
for (SelectionSet selection : selectionSetMap.values()) {
if (selection.getPlayerId().equals(MapTool.getPlayer().getName())) {
@@ -1270,7 +1314,7 @@ private Set getOwnedMovementSet(PlayerView view) {
return movementSet;
}
- private Set getUnOwnedMovementSet(PlayerView view) {
+ public Set getUnOwnedMovementSet(PlayerView view) {
Set movementSet = new HashSet();
for (SelectionSet selection : selectionSetMap.values()) {
if (!selection.getPlayerId().equals(MapTool.getPlayer().getName())) {
@@ -1281,7 +1325,7 @@ private Set getUnOwnedMovementSet(PlayerView view) {
}
protected void showBlockedMoves(Graphics2D g, PlayerView view, Set movementSet) {
- if (selectionSetMap.isEmpty()) {
+ if (selectionSetMap.isEmpty() || skipDrawing) {
return;
}
g = (Graphics2D) g.create();
@@ -2218,6 +2262,8 @@ protected void renderTokens(
}
timer.stop("renderTokens:OnscreenCheck");
+ if (skipDrawing) continue;
+
// create a per token Graphics object - normally clipped, unless always visible
Area tokenCellArea = zone.getGrid().getTokenCellArea(tokenBounds);
Graphics2D tokenG;
@@ -2751,7 +2797,7 @@ protected void renderTokens(
* @param isGMView whether it is the view of a GM
* @return true if the token is need of clipping, false otherwise
*/
- private boolean isTokenInNeedOfClipping(Token token, Area tokenCellArea, boolean isGMView) {
+ public boolean isTokenInNeedOfClipping(Token token, Area tokenCellArea, boolean isGMView) {
// can view everything or zone is not using vision = no clipping needed
if (isGMView || !zoneView.isUsingVision()) {
diff --git a/src/main/java/net/rptools/maptool/model/HexGrid.java b/src/main/java/net/rptools/maptool/model/HexGrid.java
index f9c21c0133..fb067aa938 100644
--- a/src/main/java/net/rptools/maptool/model/HexGrid.java
+++ b/src/main/java/net/rptools/maptool/model/HexGrid.java
@@ -92,6 +92,10 @@ public Point2D.Double getCenterOffset() {
/** Distance from centerpoint to middle of a face. Set to gridSize/2. */
private transient double minorRadius;
+ public double getMinorRadius() {
+ return minorRadius;
+ }
+
/**
* The projection of a sloped edge onto the diameter.
*
@@ -100,12 +104,20 @@ public Point2D.Double getCenterOffset() {
*/
protected transient double edgeProjection;
+ public double getEdgeProjection() {
+ return edgeProjection;
+ }
+
/**
* Length all edges. For a regular hexagon, this will also be the distance from the center point
* to any vertex, but for a stretch hexagon this does not hold different.
*/
protected transient double edgeLength;
+ public double getEdgeLength() {
+ return edgeLength;
+ }
+
@Override
public boolean isHex() {
return true;
@@ -221,8 +233,7 @@ private void setDimensions(int size, double diameter) {
// edgeProjection = (diameter - edgeLength) / 2
}
- private GeneralPath createHalfShape(
- double minorRadius, double edgeProjection, double edgeLength) {
+ public GeneralPath createHalfShape(double minorRadius, double edgeProjection, double edgeLength) {
GeneralPath hex = new GeneralPath();
hex.moveTo(0, minorRadius);
hex.lineTo(edgeProjection, 0);
@@ -327,13 +338,13 @@ public int getTokenSpace() {
protected abstract void setGridDrawTranslation(Graphics2D g, double u, double v);
- protected abstract double getRendererSizeU(ZoneRenderer renderer);
+ public abstract double getRendererSizeU(ZoneRenderer renderer);
- protected abstract double getRendererSizeV(ZoneRenderer renderer);
+ public abstract double getRendererSizeV(ZoneRenderer renderer);
- protected abstract int getOffV(ZoneRenderer renderer);
+ public abstract int getOffV(ZoneRenderer renderer);
- protected abstract int getOffU(ZoneRenderer renderer);
+ public abstract int getOffU(ZoneRenderer renderer);
@Override
public void draw(ZoneRenderer renderer, Graphics2D g, Rectangle bounds) {
diff --git a/src/main/java/net/rptools/maptool/model/HexGridHorizontal.java b/src/main/java/net/rptools/maptool/model/HexGridHorizontal.java
index 107286d44d..88114f9e0f 100644
--- a/src/main/java/net/rptools/maptool/model/HexGridHorizontal.java
+++ b/src/main/java/net/rptools/maptool/model/HexGridHorizontal.java
@@ -211,22 +211,22 @@ protected void setGridDrawTranslation(Graphics2D g, double U, double V) {
}
@Override
- protected double getRendererSizeV(ZoneRenderer renderer) {
+ public double getRendererSizeV(ZoneRenderer renderer) {
return renderer.getSize().getWidth();
}
@Override
- protected double getRendererSizeU(ZoneRenderer renderer) {
+ public double getRendererSizeU(ZoneRenderer renderer) {
return renderer.getSize().getHeight();
}
@Override
- protected int getOffV(ZoneRenderer renderer) {
+ public int getOffV(ZoneRenderer renderer) {
return (int) (renderer.getViewOffsetX() + getOffsetX() * renderer.getScale());
}
@Override
- protected int getOffU(ZoneRenderer renderer) {
+ public int getOffU(ZoneRenderer renderer) {
return (int) (renderer.getViewOffsetY() + getOffsetY() * renderer.getScale());
}
diff --git a/src/main/java/net/rptools/maptool/model/HexGridVertical.java b/src/main/java/net/rptools/maptool/model/HexGridVertical.java
index 3dde4949ff..1e72b60e97 100644
--- a/src/main/java/net/rptools/maptool/model/HexGridVertical.java
+++ b/src/main/java/net/rptools/maptool/model/HexGridVertical.java
@@ -194,22 +194,22 @@ protected void setGridDrawTranslation(Graphics2D g, double U, double V) {
}
@Override
- protected double getRendererSizeV(ZoneRenderer renderer) {
+ public double getRendererSizeV(ZoneRenderer renderer) {
return renderer.getSize().getHeight();
}
@Override
- protected double getRendererSizeU(ZoneRenderer renderer) {
+ public double getRendererSizeU(ZoneRenderer renderer) {
return renderer.getSize().getWidth();
}
@Override
- protected int getOffV(ZoneRenderer renderer) {
+ public int getOffV(ZoneRenderer renderer) {
return (int) (renderer.getViewOffsetY() + getOffsetY() * renderer.getScale());
}
@Override
- protected int getOffU(ZoneRenderer renderer) {
+ public int getOffU(ZoneRenderer renderer) {
return (int) (renderer.getViewOffsetX() + getOffsetX() * renderer.getScale());
}
diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java
index b3e07086be..0cb0744a21 100644
--- a/src/main/java/net/rptools/maptool/model/Token.java
+++ b/src/main/java/net/rptools/maptool/model/Token.java
@@ -1185,6 +1185,24 @@ public MD5Key getImageAssetId() {
return assetId;
}
+ public MD5Key getTokenImageAssetId() {
+ if (!getHasImageTable() || !hasFacing() || getImageTableName() == null)
+ return getImageAssetId();
+
+ LookupTable lookupTable = MapTool.getCampaign().getLookupTableMap().get(getImageTableName());
+ if (lookupTable == null) return getImageAssetId();
+
+ try {
+ LookupTable.LookupEntry result = lookupTable.getLookup(String.valueOf(getFacing()));
+ if (result != null) return result.getImageId();
+
+ } catch (ParserException p) {
+ /* do nothing */
+ }
+
+ return getImageAssetId();
+ }
+
/**
* Store the token image, and set the native Width and Height.
*
diff --git a/src/main/java/net/rptools/maptool/model/drawing/BurstTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/BurstTemplate.java
index c0943d056d..27697388f5 100644
--- a/src/main/java/net/rptools/maptool/model/drawing/BurstTemplate.java
+++ b/src/main/java/net/rptools/maptool/model/drawing/BurstTemplate.java
@@ -60,7 +60,7 @@ private Rectangle makeVertexShape(Zone zone) {
return new Rectangle(getVertex().x, getVertex().y, gridSize, gridSize);
}
- private Rectangle makeShape(Zone zone) {
+ public Rectangle makeShape(Zone zone) {
int gridSize = zone.getGrid().getSize();
return new Rectangle(
getVertex().x - getRadius() * gridSize,
diff --git a/src/main/java/net/rptools/maptool/model/drawing/ConeTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/ConeTemplate.java
index 6c5645ffdc..ebb6e80162 100644
--- a/src/main/java/net/rptools/maptool/model/drawing/ConeTemplate.java
+++ b/src/main/java/net/rptools/maptool/model/drawing/ConeTemplate.java
@@ -239,7 +239,7 @@ protected void paintArea(
}
}
- private boolean withinQuadrant(Quadrant q) {
+ public boolean withinQuadrant(Quadrant q) {
Direction dir = getDirection();
switch (q) {
case SOUTH_EAST:
diff --git a/src/main/java/net/rptools/maptool/model/drawing/LineCellTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/LineCellTemplate.java
index fec9380ed0..a2bd4b8b2a 100644
--- a/src/main/java/net/rptools/maptool/model/drawing/LineCellTemplate.java
+++ b/src/main/java/net/rptools/maptool/model/drawing/LineCellTemplate.java
@@ -194,7 +194,7 @@ public void setRadius(int squares) {
*
* @return The new path or null if there is no path.
*/
- protected @Nullable List calcPath() {
+ public @Nullable List calcPath() {
int radius = getRadius();
ZonePoint vertex = getVertex();
@@ -313,7 +313,7 @@ private void clearPath() {
*
* @return Returns the current value of quadrant.
*/
- private @Nonnull Quadrant getQuadrant() {
+ public @Nonnull Quadrant getQuadrant() {
if (quadrant == null) {
final var vertex = getVertex();
if (vertex == null || pathVertex == null || pathVertex.equals(vertex)) {
@@ -335,7 +335,7 @@ private void clearPath() {
/**
* @return Getter for path
*/
- private @Nullable List getPath() {
+ public @Nullable List getPath() {
if (path == null) {
path = calcPath();
}
diff --git a/src/main/java/net/rptools/maptool/model/drawing/LineTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/LineTemplate.java
index ed8b5d90cb..554e4ff907 100644
--- a/src/main/java/net/rptools/maptool/model/drawing/LineTemplate.java
+++ b/src/main/java/net/rptools/maptool/model/drawing/LineTemplate.java
@@ -197,7 +197,7 @@ public void setRadius(int squares) {
*
* @return The new path or null if there is no path.
*/
- protected List calcPath() {
+ public List calcPath() {
if (getRadius() == 0) return null;
if (pathVertex == null) return null;
int radius = getRadius();
diff --git a/src/main/java/net/rptools/maptool/model/drawing/WallTemplate.java b/src/main/java/net/rptools/maptool/model/drawing/WallTemplate.java
index eb0697aaa1..1d9be00232 100644
--- a/src/main/java/net/rptools/maptool/model/drawing/WallTemplate.java
+++ b/src/main/java/net/rptools/maptool/model/drawing/WallTemplate.java
@@ -82,7 +82,7 @@ public void setVertex(ZonePoint vertex) {
* @see net.rptools.maptool.model.drawing.LineTemplate#calcPath()
*/
@Override
- protected List calcPath() {
+ public List calcPath() {
return getPath(); // Do nothing, path is set by tool.
}
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Black.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Black.ttf
new file mode 100644
index 0000000000..84e305641c
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Black.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Bold.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Bold.ttf
new file mode 100644
index 0000000000..0133ddc3cf
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Bold.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-ExtraBold.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-ExtraBold.ttf
new file mode 100644
index 0000000000..24f83dc184
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-ExtraBold.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-ExtraLight.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-ExtraLight.ttf
new file mode 100644
index 0000000000..4ef7591f05
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-ExtraLight.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Light.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Light.ttf
new file mode 100644
index 0000000000..a5ae40b360
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Light.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Medium.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Medium.ttf
new file mode 100644
index 0000000000..b463272e46
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Medium.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Regular.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Regular.ttf
new file mode 100644
index 0000000000..6ab0ca0883
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Regular.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-SemiBold.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-SemiBold.ttf
new file mode 100644
index 0000000000..c2a7838f42
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-SemiBold.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Thin.ttf b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Thin.ttf
new file mode 100644
index 0000000000..1d2d610024
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/fonts/NotoSansSymbols/NotoSansSymbols-Thin.ttf differ
diff --git a/src/main/resources/net/rptools/maptool/client/image/libgdx.png b/src/main/resources/net/rptools/maptool/client/image/libgdx.png
new file mode 100644
index 0000000000..8575f4719f
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/image/libgdx.png differ
diff --git a/src/main/resources/net/rptools/maptool/client/maptool.atlas b/src/main/resources/net/rptools/maptool/client/maptool.atlas
new file mode 100644
index 0000000000..5b810c685c
--- /dev/null
+++ b/src/main/resources/net/rptools/maptool/client/maptool.atlas
@@ -0,0 +1,193 @@
+maptool.png
+size:512,512
+repeat:none
+block_move
+bounds:2,254,256,256
+blueLabelbox
+bounds:121,185,129,21
+split:10,10,10,10
+pad:10,10,10,10
+border/blue/bl
+bounds:383,369,3,3
+border/blue/bottom
+bounds:2,2,52,3
+border/blue/br
+bounds:452,387,3,3
+border/blue/right
+bounds:480,463,3,47
+border/blue/left
+bounds:480,463,3,47
+border/blue/tl
+bounds:485,499,3,3
+border/blue/top
+bounds:234,144,52,3
+border/blue/tr
+bounds:143,117,3,3
+border/fowtools/bl
+bounds:252,192,6,6
+border/fowtools/bottom
+bounds:329,374,52,6
+border/fowtools/br
+bounds:485,504,6,6
+border/fowtools/left
+bounds:121,88,6,52
+border/fowtools/right
+bounds:260,214,6,52
+border/fowtools/tl
+bounds:82,48,6,6
+border/fowtools/top
+bounds:234,149,52,6
+border/fowtools/tr
+bounds:284,313,6,6
+border/gray/bl
+bounds:383,374,5,6
+border/gray/bottom
+bounds:121,149,111,6
+border/gray/br
+bounds:288,149,6,6
+border/gray/left
+bounds:114,53,5,199
+border/gray/right
+bounds:106,53,6,199
+border/gray/tl
+bounds:321,321,5,5
+border/gray/top
+bounds:121,142,111,5
+border/gray/tr
+bounds:252,185,6,5
+border/gray2/bl
+bounds:70,25,3,3
+border/gray2/bottom
+bounds:329,364,52,3
+border/gray2/br
+bounds:493,507,3,3
+border/gray2/left
+bounds:452,392,3,52
+border/gray2/right
+bounds:268,214,3,52
+border/gray2/tl
+bounds:456,446,3,3
+border/gray2/top
+bounds:2,7,52,3
+border/gray2/tr
+bounds:260,201,3,3
+border/green/bl
+bounds:197,137,3,3
+border/green/bottom
+bounds:329,359,52,3
+border/green/br
+bounds:100,69,3,3
+border/green/left
+bounds:274,272,3,47
+border/green/right
+bounds:274,272,3,47
+border/green/tl
+bounds:183,123,3,3
+border/green/top
+bounds:129,132,52,3
+border/green/tr
+bounds:90,51,3,3
+border/highlight/bl
+bounds:456,451,6,6
+border/highlight/bottom
+bounds:390,384,52,6
+border/highlight/br
+bounds:444,384,6,6
+border/highlight/left
+bounds:252,200,6,52
+border/highlight/right
+bounds:321,328,6,52
+border/highlight/tl
+bounds:260,206,6,6
+border/highlight/top
+bounds:2,22,52,6
+border/highlight/tr
+bounds:121,80,6,6
+border/purple/bl
+bounds:292,316,3,3
+border/purple/bottom
+bounds:329,369,52,3
+border/purple/br
+bounds:121,75,3,3
+border/purple/left
+bounds:470,463,3,47
+border/purple/right
+bounds:470,463,3,47
+border/purple/tl
+bounds:268,209,3,3
+border/purple/top
+bounds:2,17,52,3
+border/purple/tr
+bounds:329,340,3,3
+border/red/bl
+bounds:390,379,3,3
+border/red/bottom
+bounds:129,122,52,3
+border/red/br
+bounds:464,454,3,3
+border/red/left
+bounds:475,463,3,47
+border/red/right
+bounds:475,463,3,47
+border/red/tl
+bounds:82,43,3,3
+border/red/top
+bounds:129,137,52,3
+border/red/tr
+bounds:288,144,3,3
+border/shadow/bl
+bounds:183,128,12,12
+border/shadow/bottom
+bounds:121,171,217,12
+border/shadow/br
+bounds:129,108,12,12
+border/shadow/left
+bounds:456,459,12,51
+border/shadow/right
+bounds:260,268,12,51
+border/shadow/tl
+bounds:56,16,12,12
+border/shadow/top
+bounds:121,157,217,12
+border/shadow/tr
+bounds:329,345,12,12
+border/yellow/bl
+bounds:284,308,3,3
+border/yellow/bottom
+bounds:129,127,52,3
+border/yellow/br
+bounds:129,103,3,3
+border/yellow/left
+bounds:279,272,3,47
+border/yellow/right
+bounds:279,272,3,47
+border/yellow/tl
+bounds:296,152,3,3
+border/yellow/top
+bounds:2,12,52,3
+border/yellow/tr
+bounds:343,354,3,3
+broken
+bounds:260,382,128,128
+darkGreyLabelbox
+bounds:121,231,129,21
+split:10,10,10,10
+pad:10,10,10,10
+grayLabelbox
+bounds:121,208,129,21
+split:10,10,10,10
+pad:10,10,10,10
+hexBorder
+bounds:390,392,60,52
+isoBorder
+bounds:2,30,78,42
+lightbulb
+bounds:82,56,16,16
+redDot
+bounds:390,446,64,64
+stack
+bounds:56,2,12,12
+unknown
+bounds:2,74,102,178
+whiteBorder
+bounds:260,321,59,59
diff --git a/src/main/resources/net/rptools/maptool/client/maptool.png b/src/main/resources/net/rptools/maptool/client/maptool.png
new file mode 100644
index 0000000000..816edf917a
Binary files /dev/null and b/src/main/resources/net/rptools/maptool/client/maptool.png differ