diff --git a/build.gradle b/build.gradle index 01cd54792d..f9416ed785 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,9 @@ def javaArgs = [ "-DMAPTOOL_DATADIR=.maptool-" + vendor.toLowerCase(), "-XX:+ShowCodeDetailsInExceptionMessages", "--add-opens=java.desktop/java.awt=ALL-UNNAMED", "--add-opens=java.desktop/java.awt.geom=ALL-UNNAMED", "--add-opens=java.desktop/sun.awt.geom=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED", - "--add-opens=javafx.web/javafx.scene.web=ALL-UNNAMED", "--add-opens=javafx.web/com.sun.webkit=ALL-UNNAMED", "--add-opens=javafx.web/com.sun.webkit.dom=ALL-UNNAMED", + "--add-opens=javafx.web/javafx.scene.web=ALL-UNNAMED", "--add-opens=javafx.web/com.sun.webkit=ALL-UNNAMED", + "--add-opens=javafx.web/com.sun.webkit.dom=ALL-UNNAMED","--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.desktop/sun.awt=ALL-UNNAMED", "--add-opens=java.desktop/sun.java2d=ALL-UNNAMED", "--add-opens=java.desktop/javax.swing=ALL-UNNAMED","--add-opens=java.desktop/sun.awt.shell=ALL-UNNAMED", "--add-opens=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED"] @@ -116,7 +118,7 @@ spotless { java { target project.fileTree(project.projectDir) { include 'src/**/*.java' - exclude '**/JTextAreaAppender.java' + exclude '**/JTextAreaAppender.java', '**/GifDecoder.java' exclude 'src/main/java/net/rptools/maptool/client/ui/themes/Flat*ContrastIJTheme.java' exclude 'src/main/java/net/rptools/maptool/client/ui/themes/Utils.java' } @@ -420,6 +422,17 @@ dependencies { implementation(libs.rptools.maptool.addons) implementation(libs.rptools.dice.roller) implementation(libs.noiselib) + + // libgdx + implementation 'com.github.thelsing:libgdx-jogl-backend:becdde406e' + implementation 'com.badlogicgames.gdx:gdx-backend-lwjgl:1.13.1' + implementation 'com.badlogicgames.gdx:gdx:1.13.1' + implementation 'com.badlogicgames.gdx:gdx-platform:1.13.1:natives-desktop' + implementation "space.earlygrey:shapedrawer:2.6.0" + implementation 'com.badlogicgames.gdx:gdx-freetype:1.13.1' + implementation 'com.badlogicgames.gdx:gdx-freetype-platform:1.13.1:natives-desktop' + implementation "com.badlogicgames.gdx-video:gdx-video:1.3.2-SNAPSHOT" + implementation "com.badlogicgames.gdx-video:gdx-video-lwjgl3:1.3.2-SNAPSHOT" } processResources { diff --git a/buildSrc/shared.gradle b/buildSrc/shared.gradle index 5b08ffd80f..b3dad22bdb 100644 --- a/buildSrc/shared.gradle +++ b/buildSrc/shared.gradle @@ -4,8 +4,10 @@ repositories { mavenCentral() maven { url = 'https://maptool.craigs-stuff.net/repo/' } maven { url = 'https://jitpack.io' } - maven { url "https://www.jetbrains.com/intellij-repository/releases" } - maven { url "https://cache-redirector.jetbrains.com/intellij-dependencies" } + maven { url = "https://www.jetbrains.com/intellij-repository/releases" } + maven { url = "https://cache-redirector.jetbrains.com/intellij-dependencies" } + maven { url = "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url = "https://jogamp.org/deployment/maven" } } apply from: rootProject.file('buildSrc/java-version.gradle') diff --git a/maptool.tpproj b/maptool.tpproj new file mode 100644 index 0000000000..161726318c --- /dev/null +++ b/maptool.tpproj @@ -0,0 +1,47 @@ +name=maptool +filename= +output=src/main/resources/net/rptools/maptool/client + +alias=true +alphaThreshold=0 +debug=false +duplicatePadding=false +edgePadding=true +fast=false +filterMag=Nearest +filterMin=Nearest +ignoreBlankImages=true +maxHeight=2048 +maxWidth=2048 +minHeight=16 +minWidth=16 +paddingX=2 +paddingY=2 +pot=true +mof=false +rotation=false +stripWhitespaceX=false +stripWhitespaceY=false +wrapX=ClampToEdge +wrapY=ClampToEdge +premultiplyAlpha=false +grid=false +square=false +bleed=true +limitMemory=true +useIndexes=true +prettyPrint=false +legacyOutput=false + +scaleFactors=[{suffix:"",factor:1,resampling:bicubic}] +inputFiles=[{path:src/main/resources/net/rptools/maptool/client/image/blueLabelbox.png,type:Input,regionName:null,ninepatch:{splits:[10,10,10,10],pads:[10,10,10,10]}},{path:src/main/resources/net/rptools/maptool/client/image/darkGreyLabelbox.png,type:Input,regionName:null,ninepatch:{splits:[10,10,10,10],pads:[10,10,10,10]}},{path:src/main/resources/net/rptools/maptool/client/image/grayLabelbox.png,type:Input,regionName:null,ninepatch:{splits:[10,10,10,10],pads:[10,10,10,10]}},{path:src/main/resources/net/rptools/maptool/client/image/border,type:Input,dirFilePrefix:border/,recursive:true,flattenPaths:false},{path:src/main/resources/net/rptools/maptool/client/image/stack.png,type:Input,regionName:null},{path:src/main/resources/net/rptools/maptool/client/image/whiteBorder.png,type:Input,regionName:null},{path:src/main/resources/net/rptools/maptool/client/image/hexBorder.png,type:Input,regionName:null},{path:src/main/resources/net/rptools/maptool/client/image/isoBorder.png,type:Input,regionName:null},{path:src/main/resources/net/rptools/maptool/client/image/redDot.png,type:Input,regionName:null},{path:src/main/resources/net/rptools/maptool/client/image/broken.png,type:Input,regionName:null},{path:src/main/resources/net/rptools/maptool/client/image/unknown.png,type:Input,regionName:null},{path:src/main/resources/net/rptools/maptool/client/image/block_move.png,type:Input,regionName:null},{path:src/main/resources/net/rptools/maptool/client/image/lightbulb.png,type:Input,regionName:null}] +keepInputFileExtensions=false + + +-PROJ- + +version=4.12.0 +fileTypeType=png +fileTypeData={encoding:RGBA8888} +previewBackgroundColor=ffffffff +projectSettings={inputFiles:{}} diff --git a/src/main/java/net/rptools/lib/gdx/Earcut.java b/src/main/java/net/rptools/lib/gdx/Earcut.java new file mode 100644 index 0000000000..c1aff2d306 --- /dev/null +++ b/src/main/java/net/rptools/lib/gdx/Earcut.java @@ -0,0 +1,757 @@ +/* + * 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.utils.ShortArray; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Adapted from https://github.com/earcut4j/earcut4j and modified to use float and short instead of + * int and float + */ +public final class Earcut { + + private Earcut() {} + ; + + /** + * Triangulates the given polygon + * + * @param data is a flat array of vertice coordinates like [x0,y0, x1,y1, x2,y2, ...]. + * @return List containing groups of three vertice indices in the resulting array forms a + * triangle. + */ + public static ShortArray earcut(float[] data) { + return earcut(data, (short[]) null, (short) 2); + } + + /** + * Triangulates the given polygon + * + * @param data is a flat array of vertice coordinates like [x0,y0, x1,y1, x2,y2, ...]. + * @param holeIndices is an array of hole indices if any (e.g. [5, 8] for a 12-vertice input would + * mean one hole with vertices 5-7 and another with 8-11). + * @param dim is the number of coordinates per vertice in the input array + * @return List containing groups of three vertice indices in the resulting array forms a + * triangle. + */ + public static ShortArray earcut(float[] data, short[] holeIndices, short dim) { + + boolean hasHoles = holeIndices != null && holeIndices.length > 0; + short outerLen = (short) (hasHoles ? holeIndices[0] * dim : data.length); + + Node outerNode = linkedList(data, (short) 0, outerLen, dim, true); + + ShortArray triangles = new ShortArray(); + + if (outerNode == null || outerNode.next == outerNode.prev) return triangles; + + float minX = 0; + float minY = 0; + float maxX = 0; + float maxY = 0; + float invSize = Float.MIN_VALUE; + + if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim); + + // if the shape is not too simple, we'll use z-order curve hash later; + // calculate polygon bbox + if (data.length > 80 * dim) { + minX = maxX = data[0]; + minY = maxY = data[1]; + + for (int i = dim; i < outerLen; i += dim) { + float x = data[i]; + float y = data[i + 1]; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + // minX, minY and size are later used to transform coords into + // Shorts for z-order calculation + invSize = Math.max(maxX - minX, maxY - minY); + invSize = invSize != 0.0f ? 1.0f / invSize : 0.0f; + } + + earcutLinked(outerNode, triangles, dim, minX, minY, invSize, Short.MIN_VALUE); + + return triangles; + } + + private static void earcutLinked( + Node ear, ShortArray triangles, int dim, float minX, float minY, float invSize, int pass) { + if (ear == null) return; + + // interlink polygon nodes in z-order + if (pass == Short.MIN_VALUE && invSize != Float.MIN_VALUE) indexCurve(ear, minX, minY, invSize); + + Node stop = ear; + + // iterate through ears, slicing them one by one + while (ear.prev != ear.next) { + Node prev = ear.prev; + Node next = ear.next; + + if (invSize != Float.MIN_VALUE ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) { + // cut off the triangle + triangles.add((short) (prev.i / dim)); + triangles.add((short) (ear.i / dim)); + triangles.add((short) (next.i / dim)); + + removeNode(ear); + + // skipping the next vertice leads to less sliver triangles + ear = next.next; + stop = next.next; + + continue; + } + + ear = next; + + // if we looped through the whole remaining polygon and can't find + // any more ears + if (ear == stop) { + // try filtering points and slicing again + if (pass == Short.MIN_VALUE) { + earcutLinked(filterPoints(ear, null), triangles, dim, minX, minY, invSize, 1); + + // if this didn't work, try curing all small + // self-intersections locally + } else if (pass == 1) { + ear = cureLocalIntersections(filterPoints(ear, null), triangles, dim); + earcutLinked(ear, triangles, dim, minX, minY, invSize, 2); + + // as a last resort, try splitting the remaining polygon + // into two + } else if (pass == 2) { + splitEarcut(ear, triangles, dim, minX, minY, invSize); + } + + break; + } + } + } + + private static void splitEarcut( + Node start, ShortArray triangles, int dim, float minX, float minY, float size) { + // look for a valid diagonal that divides the polygon into two + Node a = start; + do { + Node b = a.next.next; + while (b != a.prev) { + if (a.i != b.i && isValidDiagonal(a, b)) { + // split the polygon in two by the diagonal + Node c = splitPolygon(a, b); + + // filter colinear points around the cuts + a = filterPoints(a, a.next); + c = filterPoints(c, c.next); + + // run earcut on each half + earcutLinked(a, triangles, dim, minX, minY, size, Short.MIN_VALUE); + earcutLinked(c, triangles, dim, minX, minY, size, Short.MIN_VALUE); + return; + } + b = b.next; + } + a = a.next; + } while (a != start); + } + + private static boolean isValidDiagonal(Node a, Node b) { + // return a.next.i != b.i && a.prev.i != b.i && !intersectsPolygon(a, b) && locallyInside(a, b) + // && locallyInside(b, a) && middleInside(a, b); + + return a.next.i != b.i + && a.prev.i != b.i + && !intersectsPolygon(a, b) + && // dones't intersect other edges + (locallyInside(a, b) + && locallyInside(b, a) + && middleInside(a, b) + && // locally visible + (area(a.prev, a, b.prev) != 0 || area(a, b.prev, b) != 0) + || // does not create opposite-facing sectors + equals(a, b) + && area(a.prev, a, a.next) > 0 + && area(b.prev, b, b.next) > 0); // special zero-length case + } + + private static boolean middleInside(Node a, Node b) { + Node p = a; + boolean inside = false; + float px = (a.x + b.x) / 2; + float py = (a.y + b.y) / 2; + do { + if (((p.y > py) != (p.next.y > py)) + && (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)) inside = !inside; + p = p.next; + } while (p != a); + + return inside; + } + + private static boolean intersectsPolygon(Node a, Node b) { + Node p = a; + do { + if (p.i != a.i + && p.next.i != a.i + && p.i != b.i + && p.next.i != b.i + && intersects(p, p.next, a, b)) return true; + p = p.next; + } while (p != a); + + return false; + } + + private static boolean intersects(Node p1, Node q1, Node p2, Node q2) { + if ((equals(p1, p2) && equals(q1, q2)) || (equals(p1, q2) && equals(p2, q1))) return true; + float o1 = sign(area(p1, q1, p2)); + float o2 = sign(area(p1, q1, q2)); + float o3 = sign(area(p2, q2, p1)); + float o4 = sign(area(p2, q2, q1)); + + if (o1 != o2 && o3 != o4) return true; // general case + + if (o1 == 0 && onSegment(p1, p2, q1)) + return true; // p1, q1 and p2 are collinear and p2 lies on p1q1 + if (o2 == 0 && onSegment(p1, q2, q1)) + return true; // p1, q1 and q2 are collinear and q2 lies on p1q1 + if (o3 == 0 && onSegment(p2, p1, q2)) + return true; // p2, q2 and p1 are collinear and p1 lies on p2q2 + if (o4 == 0 && onSegment(p2, q1, q2)) + return true; // p2, q2 and q1 are collinear and q1 lies on p2q2 + + return false; + } + + // for collinear points p, q, r, check if point q lies on segment pr + private static boolean onSegment(Node p, Node q, Node r) { + return q.x <= Math.max(p.x, r.x) + && q.x >= Math.min(p.x, r.x) + && q.y <= Math.max(p.y, r.y) + && q.y >= Math.min(p.y, r.y); + } + + private static float sign(float num) { + return num > 0 ? 1 : num < 0 ? -1 : 0; + } + + private static Node cureLocalIntersections(Node start, ShortArray triangles, int dim) { + Node p = start; + do { + Node a = p.prev, b = p.next.next; + + if (!equals(a, b) + && intersects(a, p, p.next, b) + && locallyInside(a, b) + && locallyInside(b, a)) { + + triangles.add((short) (a.i / dim)); + triangles.add((short) (p.i / dim)); + triangles.add((short) (b.i / dim)); + + // remove two nodes involved + removeNode(p); + removeNode(p.next); + + p = start = b; + } + p = p.next; + } while (p != start); + + return filterPoints(p, null); + } + + private static boolean isEar(Node ear) { + Node a = ear.prev, b = ear, c = ear.next; + + if (area(a, b, c) >= 0) return false; // reflex, can't be an ear + + // now make sure we don't have other points inside the potential ear + Node p = ear.next.next; + + while (p != ear.prev) { + if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next) >= 0) + return false; + p = p.next; + } + + return true; + } + + private static boolean isEarHashed(Node ear, float minX, float minY, float invSize) { + Node a = ear.prev; + Node b = ear; + Node c = ear.next; + + if (area(a, b, c) >= 0) return false; // reflex, can't be an ear + + // triangle bbox; min & max are calculated like this for speed + float minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x), + minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y), + maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x), + maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y); + + // z-order range for the current triangle bbox; + float minZ = zOrder(minTX, minTY, minX, minY, invSize); + float maxZ = zOrder(maxTX, maxTY, minX, minY, invSize); + + // first look for points inside the triangle in increasing z-order + Node p = ear.prevZ; + Node n = ear.nextZ; + + while (p != null && p.z >= minZ && n != null && n.z <= maxZ) { + if (p != ear.prev + && p != ear.next + && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) + && area(p.prev, p, p.next) >= 0) return false; + p = p.prevZ; + + if (n != ear.prev + && n != ear.next + && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) + && area(n.prev, n, n.next) >= 0) return false; + n = n.nextZ; + } + + // look for remaining points in decreasing z-order + while (p != null && p.z >= minZ) { + if (p != ear.prev + && p != ear.next + && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) + && area(p.prev, p, p.next) >= 0) return false; + p = p.prevZ; + } + + // look for remaining points in increasing z-order + while (n != null && n.z <= maxZ) { + if (n != ear.prev + && n != ear.next + && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) + && area(n.prev, n, n.next) >= 0) return false; + n = n.nextZ; + } + + return true; + } + + // z-order of a point given coords and inverse of the longer side of data bbox + private static float zOrder(float x, float y, float minX, float minY, float invSize) { + // coords are transformed into non-negative 15-bit Short range + int lx = Float.valueOf(32767 * (x - minX) * invSize).intValue(); + int ly = Float.valueOf(32767 * (y - minY) * invSize).intValue(); + + lx = (lx | (lx << 8)) & 0x00FF00FF; + lx = (lx | (lx << 4)) & 0x0F0F0F0F; + lx = (lx | (lx << 2)) & 0x33333333; + lx = (lx | (lx << 1)) & 0x55555555; + + ly = (ly | (ly << 8)) & 0x00FF00FF; + ly = (ly | (ly << 4)) & 0x0F0F0F0F; + ly = (ly | (ly << 2)) & 0x33333333; + ly = (ly | (ly << 1)) & 0x55555555; + + return lx | (ly << 1); + } + + private static void indexCurve(Node start, float minX, float minY, float invSize) { + Node p = start; + do { + if (p.z == Float.MIN_VALUE) p.z = zOrder(p.x, p.y, minX, minY, invSize); + p.prevZ = p.prev; + p.nextZ = p.next; + p = p.next; + } while (p != start); + + p.prevZ.nextZ = null; + p.prevZ = null; + + sortLinked(p); + } + + private static Node sortLinked(Node list) { + int inSize = 1; + + int numMerges; + do { + Node p = list; + list = null; + Node tail = null; + numMerges = 0; + + while (p != null) { + numMerges++; + Node q = p; + int pSize = 0; + for (int i = 0; i < inSize; i++) { + pSize++; + q = q.nextZ; + if (q == null) break; + } + + int qSize = inSize; + + while (pSize > 0 || (qSize > 0 && q != null)) { + Node e; + if (pSize == 0) { + e = q; + q = q.nextZ; + qSize--; + } else if (qSize == 0 || q == null) { + e = p; + p = p.nextZ; + pSize--; + } else if (p.z <= q.z) { + e = p; + p = p.nextZ; + pSize--; + } else { + e = q; + q = q.nextZ; + qSize--; + } + + if (tail != null) tail.nextZ = e; + else list = e; + + e.prevZ = tail; + tail = e; + } + + p = q; + } + + tail.nextZ = null; + inSize *= 2; + + } while (numMerges > 1); + + return list; + } + + private static Node eliminateHoles(float[] data, short[] holeIndices, Node outerNode, short dim) { + List queue = new ArrayList<>(); + + int len = holeIndices.length; + for (int i = 0; i < len; i++) { + short start = (short) (holeIndices[i] * dim); + short end = (short) (i < len - 1 ? holeIndices[i + 1] * dim : data.length); + Node list = linkedList(data, start, end, dim, false); + if (list == list.next) list.steiner = true; + queue.add(getLeftmost(list)); + } + + queue.sort( + new Comparator() { + + @Override + public int compare(Node o1, Node o2) { + if (o1.x - o2.x > 0) return 1; + else if (o1.x - o2.x < 0) return -2; + return 0; + } + }); + + for (Node node : queue) { + eliminateHole(node, outerNode); + outerNode = filterPoints(outerNode, outerNode.next); + } + + return outerNode; + } + + private static Node filterPoints(Node start, Node end) { + if (start == null) return start; + if (end == null) end = start; + + Node p = start; + boolean again; + + do { + again = false; + + if (!p.steiner && equals(p, p.next) || area(p.prev, p, p.next) == 0) { + removeNode(p); + p = end = p.prev; + if (p == p.next) break; + again = true; + } else { + p = p.next; + } + } while (again || p != end); + + return end; + } + + private static boolean equals(Node p1, Node p2) { + return p1.x == p2.x && p1.y == p2.y; + } + + private static float area(Node p, Node q, Node r) { + return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); + } + + private static void eliminateHole(Node hole, Node outerNode) { + outerNode = findHoleBridge(hole, outerNode); + if (outerNode != null) { + Node b = splitPolygon(outerNode, hole); + + // filter collinear points around the cuts + filterPoints(outerNode, outerNode.next); + filterPoints(b, b.next); + } + } + + private static Node splitPolygon(Node a, Node b) { + Node a2 = new Node(a.i, a.x, a.y); + Node b2 = new Node(b.i, b.x, b.y); + Node an = a.next; + Node bp = b.prev; + + a.next = b; + b.prev = a; + + a2.next = an; + an.prev = a2; + + b2.next = a2; + a2.prev = b2; + + bp.next = b2; + b2.prev = bp; + + return b2; + } + + // David Eberly's algorithm for finding a bridge between hole and outer + // polygon + private static Node findHoleBridge(Node hole, Node outerNode) { + Node p = outerNode; + float hx = hole.x; + float hy = hole.y; + float qx = -Float.MAX_VALUE; + Node m = null; + + // find a segment intersected by a ray from the hole's leftmost point to + // the left; + // segment's endpoint with lesser x will be potential connection point + do { + if (hy <= p.y && hy >= p.next.y) { + float x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y); + if (x <= hx && x > qx) { + qx = x; + if (x == hx) { + if (hy == p.y) return p; + if (hy == p.next.y) return p.next; + } + m = p.x < p.next.x ? p : p.next; + } + } + p = p.next; + } while (p != outerNode); + + if (m == null) return null; + + if (hx == qx) return m; // hole touches outer segment; pick leftmost endpoint + + // look for points inside the triangle of hole point, segment + // intersection and endpoint; + // if there are no points found, we have a valid connection; + // otherwise choose the point of the minimum angle with the ray as + // connection point + + Node stop = m; + float mx = m.x; + float my = m.y; + float tanMin = Float.MAX_VALUE; + float tan; + + p = m; + + do { + if (hx >= p.x + && p.x >= mx + && pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) { + + tan = Math.abs(hy - p.y) / (hx - p.x); // tangential + + if (locallyInside(p, hole) + && (tan < tanMin + || (tan == tanMin && (p.x > m.x || (p.x == m.x && sectorContainsSector(m, p)))))) { + m = p; + tanMin = tan; + } + } + + p = p.next; + } while (p != stop); + + return m; + } + + private static boolean locallyInside(Node a, Node b) { + return area(a.prev, a, a.next) < 0 + ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 + : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; + } + + // whether sector in vertex m contains sector in vertex p in the same + // coordinates + private static boolean sectorContainsSector(Node m, Node p) { + return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0; + } + + private static boolean pointInTriangle( + float ax, float ay, float bx, float by, float cx, float cy, float px, float py) { + return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 + && (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 + && (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0; + } + + private static Node getLeftmost(Node start) { + Node p = start; + Node leftmost = start; + do { + if (p.x < leftmost.x || (p.x == leftmost.x && p.y < leftmost.y)) leftmost = p; + p = p.next; + } while (p != start); + return leftmost; + } + + private static Node linkedList( + float[] data, short start, short end, short dim, boolean clockwise) { + Node last = null; + if (clockwise == (signedArea(data, start, end, dim) > 0)) { + for (short i = start; i < end; i += dim) { + last = insertNode(i, data[i], data[i + 1], last); + } + } else { + for (short i = (short) (end - dim); i >= start; i -= dim) { + last = insertNode(i, data[i], data[i + 1], last); + } + } + + if (last != null && equals(last, last.next)) { + removeNode(last); + last = last.next; + } + return last; + } + + private static void removeNode(Node p) { + p.next.prev = p.prev; + p.prev.next = p.next; + + if (p.prevZ != null) { + p.prevZ.nextZ = p.nextZ; + } + if (p.nextZ != null) { + p.nextZ.prevZ = p.prevZ; + } + } + + private static Node insertNode(short i, float x, float y, Node last) { + Node p = new Node(i, x, y); + + if (last == null) { + p.prev = p; + p.next = p; + } else { + p.next = last.next; + p.prev = last; + last.next.prev = p; + last.next = p; + } + return p; + } + + private static float signedArea(float[] data, int start, int end, int dim) { + float sum = 0; + int j = end - dim; + for (int i = start; i < end; i += dim) { + sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); + j = i; + } + return sum; + } + + private static class Node { + + short i; + float x; + float y; + float z; + boolean steiner; + + Node prev; + Node next; + Node prevZ; + Node nextZ; + + Node(short i, float x, float y) { + // vertice index in coordinates array + this.i = i; + + // vertex coordinates + this.x = x; + this.y = y; + + // previous and next vertice nodes in a polygon ring + this.prev = null; + this.next = null; + + // z-order curve value + this.z = Float.MIN_VALUE; + + // previous and next nodes in z-order + this.prevZ = null; + this.nextZ = null; + + // indicates whether this is a steiner point + this.steiner = false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{i: ") + .append(i) + .append(", x: ") + .append(x) + .append(", y: ") + .append(y) + .append(", prev: ") + .append(toString(prev)) + .append(", next: ") + .append(toString(next)) + .append("}"); + return sb.toString(); + } + + public String toString(Node node) { + if (node == null) { + return "null"; + } + return "{i: " + node.i + ", x: " + node.x + ", y: " + node.y + "}"; + } + } +} diff --git a/src/main/java/net/rptools/lib/gdx/GifDecoder.java b/src/main/java/net/rptools/lib/gdx/GifDecoder.java new file mode 100644 index 0000000000..fe14886d4f --- /dev/null +++ b/src/main/java/net/rptools/lib/gdx/GifDecoder.java @@ -0,0 +1,743 @@ +package net.rptools.lib.gdx; + +import static java.lang.System.arraycopy; + +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Animation; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.utils.Array; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import net.rptools.lib.image.ImageUtil; + +/* + * Copyright 2014 Dhyan Blum + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * A decoder capable of processing a GIF data stream to render the graphics contained in it. This + * implementation follows the official GIF + * specification. + * + *

Example usage: + * + *

+ * + *

+ * final GifImage gifImage = GifDecoder.read(int[] data);
+ * final int width = gifImage.getWidth();
+ * final int height = gifImage.getHeight();
+ * final int frameCount = gifImage.getFrameCount();
+ * for (int i = 0; i < frameCount; i++) {
+ * 	final BufferedImage image = gifImage.getFrame(i);
+ * 	final int delay = gif.getDelay(i);
+ * }
+ * 
+ * + * @author Dhyan Blum + * @version 1.09 November 2017 + */ +public final class GifDecoder { + private GifImage image; + + public GifDecoder(InputStream is) throws IOException { + image = read(is); + } + + public GifDecoder() {} + + static final class BitReader { + private int bitPos; // Next bit to read + private int numBits; // Number of bits to read + private int bitMask; // Use to kill unwanted higher bits + private byte[] in; // Data array + + // To avoid costly bounds checks, 'in' needs 2 more 0-bytes at the end + private final void init(final byte[] in) { + this.in = in; + bitPos = 0; + } + + private final int read() { + // Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2 + int i = bitPos >>> 3; // Byte = bit / 8 + // Bits we'll shift to the right, AND 7 is the same as MODULO 8 + final int rBits = bitPos & 7; + // Byte 0 to 2, AND to get their unsigned values + final int b0 = in[i++] & 0xFF, b1 = in[i++] & 0xFF, b2 = in[i] & 0xFF; + // Glue the bytes together, don't do more shifting than necessary + final int buf = ((b2 << 8 | b1) << 8 | b0) >>> rBits; + bitPos += numBits; + return buf & bitMask; // Kill the unwanted higher bits + } + + private final void setNumBits(final int numBits) { + this.numBits = numBits; + bitMask = (1 << numBits) - 1; + } + } + + static final class CodeTable { + private final int[][] tbl; // Maps codes to lists of colors + private int initTableSize; // Number of colors +2 for CLEAR + EOI + private int initCodeSize; // Initial code size + private int initCodeLimit; // First code limit + private int codeSize; // Current code size, maximum is 12 bits + private int nextCode; // Next available code for a new entry + private int nextCodeLimit; // Increase codeSize when nextCode == limit + private BitReader br; // Notify when code sizes increases + + public CodeTable() { + tbl = new int[4096][1]; + } + + private final int add(final int[] indices) { + if (nextCode < 4096) { + if (nextCode == nextCodeLimit && codeSize < 12) { + codeSize++; // Max code size is 12 + br.setNumBits(codeSize); + nextCodeLimit = (1 << codeSize) - 1; // 2^codeSize - 1 + } + tbl[nextCode++] = indices; + } + return codeSize; + } + + private final int clear() { + codeSize = initCodeSize; + br.setNumBits(codeSize); + nextCodeLimit = initCodeLimit; + nextCode = initTableSize; // Don't recreate table, reset pointer + return codeSize; + } + + private final void init(final GifFrame fr, final int[] activeColTbl, final BitReader br) { + this.br = br; + final int numColors = activeColTbl.length; + initCodeSize = fr.firstCodeSize; + initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1 + initTableSize = fr.endOfInfoCode + 1; + nextCode = initTableSize; + for (int c = numColors - 1; c >= 0; c--) { + tbl[c][0] = activeColTbl[c]; // Translated color + } // A gap may follow with no colors assigned if numCols < CLEAR + tbl[fr.clearCode] = new int[] {fr.clearCode}; // CLEAR + tbl[fr.endOfInfoCode] = new int[] {fr.endOfInfoCode}; // EOI + // Locate transparent color in code table and set to 0 + if (fr.transpColFlag && fr.transpColIndex < numColors) { + tbl[fr.transpColIndex][0] = 0; + } + } + } + + final class GifFrame { + // Graphic control extension (optional) + // Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV + private int disposalMethod; // 0-3 as above, 4-7 undefined + private boolean transpColFlag; // 1 Bit + private int delay; // Unsigned, LSByte first, n * 1/100 * s + private int transpColIndex; // 1 Byte + // Image descriptor + private int x; // Position on the canvas from the left + private int y; // Position on the canvas from the top + private int w; // May be smaller than the base image + private int h; // May be smaller than the base image + private int wh; // width * height + private boolean hasLocColTbl; // Has local color table? 1 Bit + private boolean interlaceFlag; // Is an interlace image? 1 Bit + + @SuppressWarnings("unused") + private boolean sortFlag; // True if local colors are sorted, 1 Bit + + private int sizeOfLocColTbl; // Size of the local color table, 3 Bits + private int[] localColTbl; // Local color table (optional) + // Image data + private int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI + private int clearCode; + private int endOfInfoCode; + private byte[] data; // Holds LZW encoded data + private BufferedImage img; // Full drawn image, not just the frame area + } + + public final class GifImage { + public String header; // Bytes 0-5, GIF87a or GIF89a + private int w; // Unsigned 16 Bit, least significant byte first + private int h; // Unsigned 16 Bit, least significant byte first + private int wh; // Image width * image height + public boolean hasGlobColTbl; // 1 Bit + public int colorResolution; // 3 Bits + public boolean sortFlag; // True if global colors are sorted, 1 Bit + public int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec + public int bgColIndex; // Background color index, 1 Byte + public int pxAspectRatio; // Pixel aspect ratio, 1 Byte + public int[] globalColTbl; // Global color table + private final List frames = new ArrayList(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: + // * + // *

    + // *
  1. Render Hidden-layer tokens + // *
  2. Render Hidden-layer drawables + // *
  3. Render Token-layer drawables + // *
  4. Render Token-layer tokens + // *
+ // * + // * That's fine for players, but clearly wrong if the view is for the GM. We now use: + // * + // *
    + // *
  1. Render Token-layer drawables // Player-drawn images shouldn't obscure GM's + // images? + // *
  2. Render Hidden-layer drawables // GM could always use "View As Player" if needed? + // *
  3. Render Hidden-layer tokens + // *
  4. 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 path = + set.getWalker() != null ? set.getWalker().getPath() : set.getGridlessPath(); + List 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