From 963da8aa2b9bc77659e406e0858d8276d4060717 Mon Sep 17 00:00:00 2001 From: Joacim Breiler Date: Thu, 9 May 2024 15:44:21 +0200 Subject: [PATCH] Feed rate visualization (#2515) * Blends the color for spindle min/max with the color for feed min/max * Add translation to color settings * Attempt to fix intermittently failing test --- .../gcode/GcodePreprocessorUtils.java | 216 +++++++++--------- .../visualizer/GcodeViewParse.java | 7 + .../resources/MessagesBundle_en_US.properties | 6 +- .../AsyncCommunicatorEventDispatcherTest.java | 17 +- .../gcode/GcodePreprocessorUtilsTest.java | 43 +++- .../visualizer/options/VisualizerOptions.java | 6 +- .../renderables/GcodeLineColorizer.java | 75 ++++-- .../visualizer/renderables/GcodeModel.java | 1 + 8 files changed, 235 insertions(+), 136 deletions(-) diff --git a/ugs-core/src/com/willwinder/universalgcodesender/gcode/GcodePreprocessorUtils.java b/ugs-core/src/com/willwinder/universalgcodesender/gcode/GcodePreprocessorUtils.java index 56108116b5..abe8231427 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/gcode/GcodePreprocessorUtils.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/gcode/GcodePreprocessorUtils.java @@ -20,6 +20,10 @@ This file is part of Universal Gcode Sender (UGS). import com.google.common.base.Preconditions; import com.willwinder.universalgcodesender.gcode.util.Code; +import static com.willwinder.universalgcodesender.gcode.util.Code.G0; +import static com.willwinder.universalgcodesender.gcode.util.Code.G1; +import static com.willwinder.universalgcodesender.gcode.util.Code.G53; +import static com.willwinder.universalgcodesender.gcode.util.Code.ModalGroup.Motion; import com.willwinder.universalgcodesender.gcode.util.GcodeParserException; import com.willwinder.universalgcodesender.gcode.util.PlaneFormatter; import com.willwinder.universalgcodesender.i18n.Localization; @@ -30,14 +34,18 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.universalgcodesender.model.UnitUtils; import java.text.DecimalFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import static com.willwinder.universalgcodesender.gcode.util.Code.*; -import static com.willwinder.universalgcodesender.gcode.util.Code.ModalGroup.Motion; - /** * Collection of useful command preprocessor methods. * @@ -221,7 +229,7 @@ static public Position updatePointWithCommand(List commandArgs, Position /** * Update a point given the new coordinates. */ - static public Position updatePointWithCommand(Position initial, double x, double y, double z, double a, double b, double c, boolean absoluteMode) { + public static Position updatePointWithCommand(Position initial, double x, double y, double z, double a, double b, double c, boolean absoluteMode) { Position newPoint = new Position(initial); @@ -246,28 +254,36 @@ static public Position updatePointWithCommand(Position initial, double x, double } } else { if (!Double.isNaN(x)) { - newPoint.x += x; + newPoint.x = addValue(newPoint.x, x); } if (!Double.isNaN(y)) { - newPoint.y += y; + newPoint.y = addValue(newPoint.y, y); } if (!Double.isNaN(z)) { - newPoint.z += z; + newPoint.z = addValue(newPoint.z, z); } if (!Double.isNaN(a)) { - newPoint.a += a; + newPoint.a = addValue(newPoint.a, a); } if (!Double.isNaN(b)) { - newPoint.b += b; + newPoint.b = addValue(newPoint.b, b); } if (!Double.isNaN(c)) { - newPoint.c += c; + newPoint.c = addValue(newPoint.c, c); } } return newPoint; } - + + private static double addValue(double oldValue, double newValue) { + if (Double.isNaN(oldValue)) { + return newValue; + } else { + return oldValue + newValue; + } + } + static public Position updateCenterWithCommand( List commandArgs, Position initial, @@ -275,15 +291,15 @@ static public Position updateCenterWithCommand( boolean absoluteIJKMode, boolean clockwise, PlaneFormatter plane) { - double i = parseCoord(commandArgs, 'I'); - double j = parseCoord(commandArgs, 'J'); - double k = parseCoord(commandArgs, 'K'); + double i = parseCoord(commandArgs, 'I'); + double j = parseCoord(commandArgs, 'J'); + double k = parseCoord(commandArgs, 'K'); double radius = parseCoord(commandArgs, 'R'); - + if (Double.isNaN(i) && Double.isNaN(j) && Double.isNaN(k)) { return GcodePreprocessorUtils.convertRToCenter( - initial, nextPoint, radius, absoluteIJKMode, - clockwise, plane); + initial, nextPoint, radius, absoluteIJKMode, + clockwise, plane); } return updatePointWithCommand(initial, i, j, k, 0, 0, 0, absoluteIJKMode); @@ -299,7 +315,7 @@ static public String generateLineFromPoints(final Code command, final CNCPoint s if (df == null) { df = DEFAULT_FORMATTER; } - + StringBuilder sb = new StringBuilder(); sb.append(command); @@ -319,21 +335,21 @@ static public String generateLineFromPoints(final Code command, final CNCPoint s } else { // calculate offsets. if (!Double.isNaN(end.x)) { sb.append("X"); - sb.append(df.format(end.x-start.x)); + sb.append(df.format(end.x - start.x)); } if (!Double.isNaN(end.y)) { sb.append("Y"); - sb.append(df.format(end.y-start.y)); + sb.append(df.format(end.y - start.y)); } if (!Double.isNaN(end.z)) { sb.append("Z"); - sb.append(df.format(end.z-start.z)); + sb.append(df.format(end.z - start.z)); } } - + return sb.toString(); } - + /** * Splits a gcode command by each word/argument, doesn't care about spaces. * This command is about the same speed as the string.split(" ") command, @@ -341,7 +357,7 @@ static public String generateLineFromPoints(final Code command, final CNCPoint s */ static public List splitCommand(String command) { // Special handling for GRBL system commands which will not be splitted - if(command.startsWith("$")) { + if (command.startsWith("$")) { return Collections.singletonList(command); } @@ -350,8 +366,8 @@ static public List splitCommand(String command) { boolean readLineComment = false; int blockCommentDepth = 0; StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < command.length(); i++){ + + for (int i = 0; i < command.length(); i++) { char c = command.charAt(i); if (c == '(' && !readLineComment) { @@ -371,7 +387,7 @@ static public List splitCommand(String command) { } continue; } else if (c == ';' && !readLineComment && blockCommentDepth == 0) { - if( sb.length() > 0 ){ + if (sb.length() > 0) { l.add(sb.toString()); sb = new StringBuilder(); } @@ -390,37 +406,33 @@ static public List splitCommand(String command) { // character is a letter or whitespace, then we hit a boundary. else if (readNumeric && !Character.isDigit(c) && c != '.') { readNumeric = false; // reset flag. - + l.add(sb.toString()); sb = new StringBuilder(); - + if (Character.isLetter(c)) { sb.append(c); } - } - - else if (Character.isDigit(c) || c == '.' || c == '-') { + } else if (Character.isDigit(c) || c == '.' || c == '-') { sb.append(c); readNumeric = true; - } - - else if (Character.isLetter(c)) { + } else if (Character.isLetter(c)) { sb.append(c); } } - + // Add final one if (sb.length() > 0) { l.add(sb.toString()); } - + return l; } - + // TODO: Replace everything that uses this with a loop that loops through // the string and creates a hash with all the values. static public boolean hasAxisWords(List argList) { - for(String t : argList) { + for (String t : argList) { if (t.length() > 1) { char c = Character.toUpperCase(t.charAt(0)); if (c == 'X' || c == 'Y' || c == 'Z' || c == 'A' || c == 'B' || c == 'C') { @@ -433,15 +445,14 @@ static public boolean hasAxisWords(List argList) { // TODO: Replace everything that uses this with a loop that loops through // the string and creates a hash with all the values. + /** * Pulls out a word, like "F100", "S1300", "T0", "X-0.5" */ static public String extractWord(List argList, char c) { char address = Character.toUpperCase(c); - for(String t : argList) - { - if (Character.toUpperCase(t.charAt(0)) == address) - { + for (String t : argList) { + if (Character.toUpperCase(t.charAt(0)) == address) { return t; } } @@ -450,8 +461,7 @@ static public String extractWord(List argList, char c) { // TODO: Replace everything that uses this with a loop that loops through // the string and creates a hash with all the values. - static public double parseCoord(List argList, char c) - { + static public double parseCoord(List argList, char c) { String word = extractWord(argList, c); if (word != null && word.length() > 1) { try { @@ -462,18 +472,18 @@ static public double parseCoord(List argList, char c) } return Double.NaN; } - + /** * Generates the points along an arc including the start and end points. - * - * @param start start position XYZ and rotations - * @param end end position XYZ and rotations - * @param center center of rotation - * @param clockwise flag indicating clockwise or counter-clockwise - * @param radius radius of the arc in the same units as the given start, end and center position - * @param minArcLengthMM minimum length before expansions are made. + * + * @param start start position XYZ and rotations + * @param end end position XYZ and rotations + * @param center center of rotation + * @param clockwise flag indicating clockwise or counter-clockwise + * @param radius radius of the arc in the same units as the given start, end and center position + * @param minArcLengthMM minimum length before expansions are made. * @param arcSegmentLengthMM length of segments in resulting Positions. - * @param plane helper to select values for arcs across different planes + * @param plane helper to select values for arcs across different planes */ static public List generatePointsAlongArcBDring( final Position start, @@ -488,7 +498,7 @@ static public List generatePointsAlongArcBDring( // Calculate radius if necessary. double r = radius; if (r == 0) { - r = Math.sqrt(Math.pow(plane.axis0(start) - plane.axis0(center),2.0) + Math.pow(plane.axis1(end) - plane.axis1(center), 2.0)); + r = Math.sqrt(Math.pow(plane.axis0(start) - plane.axis0(center), 2.0) + Math.pow(plane.axis1(end) - plane.axis1(center), 2.0)); } double startAngle = GcodePreprocessorUtils.getAngle(center, start, plane); @@ -506,11 +516,11 @@ static public List generatePointsAlongArcBDring( /** * Calculates the number of points to expand an arc into * - * @param radius the radius of the arc - * @param radiusUnits the radius units - * @param minArcLengthMM the minimum arc length + * @param radius the radius of the arc + * @param radiusUnits the radius units + * @param minArcLengthMM the minimum arc length * @param arcSegmentLengthMM the arg length - * @param sweep the angle of the arc + * @param sweep the angle of the arc * @return the number of segments to split the arc into to achieve the given arc segment length */ private static int calculateNumberOfPointsToExpand(double radius, UnitUtils.Units radiusUnits, double minArcLengthMM, double arcSegmentLengthMM, double sweep) { @@ -528,22 +538,23 @@ private static int calculateNumberOfPointsToExpand(double radius, UnitUtils.Unit int numPoints = 20; if (arcSegmentLengthMM > 0) { - numPoints = (int)Math.ceil(arcLengthMM/ arcSegmentLengthMM); + numPoints = (int) Math.ceil(arcLengthMM / arcSegmentLengthMM); } return numPoints; } /** * Generates the points along an arc including the start and end points. - * @param p1 start position XYZ and rotations - * @param p2 end position XYZ and rotations - * @param center center of rotation - * @param isCw flag indicating clockwise or counter-clockwise - * @param radius radius of the arc + * + * @param p1 start position XYZ and rotations + * @param p2 end position XYZ and rotations + * @param center center of rotation + * @param isCw flag indicating clockwise or counter-clockwise + * @param radius radius of the arc * @param startAngle beginning angle of arc - * @param sweep sweep length in radians - * @param numPoints number of points to generate - * @param plane helper to select values for arcs across different planes + * @param sweep sweep length in radians + * @param numPoints number of points to generate + * @param plane helper to select values for arcs across different planes */ public static List generatePointsAlongArcBDring( final Position p1, @@ -573,12 +584,11 @@ public static List generatePointsAlongArcBDring( double bIncrement = (p2.b - p1.b) / numPoints; double cIncrement = (p2.c - p1.c) / numPoints; - for(int i=0; i= Math.PI * 2) { @@ -599,7 +609,7 @@ public static List generatePointsAlongArcBDring( segments.add(new Position(nextPoint)); } - + segments.add(new Position(p2)); return segments; @@ -618,7 +628,7 @@ static private Position convertRToCenter( boolean clockwise, PlaneFormatter plane) { Position center = new Position(start.getUnits()); - + // This math is copied from GRBL in gcode.c double x = plane.axis0(end) - plane.axis0(start); double y = plane.axis1(end) - plane.axis1(start); @@ -637,8 +647,8 @@ static private Position convertRToCenter( h_x2_div_d = -h_x2_div_d; } - double offsetX = 0.5*(x-(y*h_x2_div_d)); - double offsetY = 0.5*(y+(x*h_x2_div_d)); + double offsetX = 0.5 * (x - (y * h_x2_div_d)); + double offsetY = 0.5 * (y + (x * h_x2_div_d)); if (!absoluteIJK) { plane.setAxis0(center, plane.axis0(start) + offsetX); @@ -665,16 +675,15 @@ static public double getAngle(final Position start, final Position end, PlaneFor if (deltaX != 0) { // prevent div by 0 // it helps to know what quadrant you are in if (deltaX > 0 && deltaY >= 0) { // 0 - 90 - angle = Math.atan(deltaY/deltaX); + angle = Math.atan(deltaY / deltaX); } else if (deltaX < 0 && deltaY >= 0) { // 90 to 180 - angle = Math.PI - Math.abs(Math.atan(deltaY/deltaX)); + angle = Math.PI - Math.abs(Math.atan(deltaY / deltaX)); } else if (deltaX < 0 && deltaY < 0) { // 180 - 270 - angle = Math.PI + Math.abs(Math.atan(deltaY/deltaX)); + angle = Math.PI + Math.abs(Math.atan(deltaY / deltaX)); } else if (deltaX > 0 && deltaY < 0) { // 270 - 360 - angle = Math.PI * 2 - Math.abs(Math.atan(deltaY/deltaX)); + angle = Math.PI * 2 - Math.abs(Math.atan(deltaY / deltaX)); } - } - else { + } else { // 90 deg if (deltaY > 0) { angle = Math.PI / 2.0; @@ -684,12 +693,13 @@ static public double getAngle(final Position start, final Position end, PlaneFor angle = Math.PI * 3.0 / 2.0; } } - + return angle; } /** * Helper method for arc calculation to calculate sweep from two angles. + * * @return sweep in radians. */ static private double calculateSweep(double startAngle, double endAngle, boolean isCw) { @@ -734,23 +744,18 @@ static public Set getCodes(List args, Character letter) { .collect(Collectors.toCollection(LinkedHashSet::new)); } - public static class SplitCommand { - public String extracted; - public String remainder; - } - public static boolean isMotionWord(char character) { char c = Character.toUpperCase(character); - return + return c == 'X' || c == 'Y' || c == 'Z' - || c == 'U' || c == 'V' || c == 'W' - || c == 'I' || c == 'J' || c == 'K' - || c == 'R'; + || c == 'U' || c == 'V' || c == 'W' + || c == 'I' || c == 'J' || c == 'K' + || c == 'R'; } /** * Return extracted motion words and remainder words. - * + *

* If the code is implicit, like the command "X0Y0", we'll still extract "X0Y0". * If the code is G0 or G1 and G53 is found, it will also be extracted: * http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g53 @@ -758,7 +763,7 @@ public static boolean isMotionWord(char character) { public static SplitCommand extractMotion(Code code, String command) { List args = splitCommand(command); if (args.isEmpty()) return null; - + StringBuilder extracted = new StringBuilder(); StringBuilder remainder = new StringBuilder(); @@ -777,7 +782,7 @@ public static SplitCommand extractMotion(Code code, String command) { if (extracted.length() == 0) return null; SplitCommand sc = new SplitCommand(); - sc.extracted= extracted.toString(); + sc.extracted = extracted.toString(); sc.remainder = remainder.toString(); return sc; @@ -785,17 +790,17 @@ public static SplitCommand extractMotion(Code code, String command) { /** * Normalize a command by adding in implicit state. - * + *

* For example given the following program: - * G20 - * G0 X10 F25 - * Y10 - * + * G20 + * G0 X10 F25 + * Y10 + *

* The third command would be normalized to: - * G0 Y10 F25 + * G0 Y10 F25 * * @param command a command string to normalize. - * @param state the machine state before the command. + * @param state the machine state before the command. * @return normalized command. */ public static String normalizeCommand(String command, GcodeState state) throws GcodeParserException { @@ -835,4 +840,9 @@ public static String normalizeCommand(String command, GcodeState state) throws G return result.toString(); } + + public static class SplitCommand { + public String extracted; + public String remainder; + } } diff --git a/ugs-core/src/com/willwinder/universalgcodesender/visualizer/GcodeViewParse.java b/ugs-core/src/com/willwinder/universalgcodesender/visualizer/GcodeViewParse.java index a38353f4c0..3f489f547a 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/visualizer/GcodeViewParse.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/visualizer/GcodeViewParse.java @@ -42,9 +42,11 @@ public class GcodeViewParse { private final Position max; private final List lines; private double maxSpindleSpeed; + private double maxFeedRate; public GcodeViewParse() { maxSpindleSpeed = 0; + maxFeedRate = 0; min = new Position(UnitUtils.Units.MM); max = new Position(UnitUtils.Units.MM); lines = new ArrayList<>(); @@ -72,6 +74,10 @@ public double getMaxSpindleSpeed() { return maxSpindleSpeed; } + public double getMaxFeedRate() { + return maxFeedRate; + } + /** * Test a point and update min/max coordinates if appropriate. */ @@ -153,6 +159,7 @@ private void recalculateBoundaries() { testExtremes(lineSegment.getStart()); testExtremes(lineSegment.getEnd()); maxSpindleSpeed = Math.max(lineSegment.getSpindleSpeed(), maxSpindleSpeed); + maxFeedRate = Math.max(lineSegment.getFeedRate(), maxFeedRate); }); } diff --git a/ugs-core/src/resources/MessagesBundle_en_US.properties b/ugs-core/src/resources/MessagesBundle_en_US.properties index d203aca492..69eb8ba46b 100644 --- a/ugs-core/src/resources/MessagesBundle_en_US.properties +++ b/ugs-core/src/resources/MessagesBundle_en_US.properties @@ -342,8 +342,10 @@ platform.visualizer.dowel.preview.desc = Show dowel preview platform.visualizer.highlight = Show highlighted lines platform.visualizer.highlight.desc = Highlights the selected lines from the editor platform.visualizer.color.highlight = Editor Line Highlight Color -platform.visualizer.color.linear = Linear Movement Color (G1) max speed -platform.visualizer.color.linear.min.speed = Linear Movement Color (G1) min speed +platform.visualizer.color.linear = Linear Movement Color (G1) feed max +platform.visualizer.color.linear.min.speed = Linear Movement Color (G1) feed min +platform.visualizer.color.spindle.max.speed = Linear Movement Color (G1) spindle max +platform.visualizer.color.spindle.min.speed = Linear Movement Color (G1) spindle min platform.visualizer.color.rapid = Rapid Movement Color (G0) platform.visualizer.color.arc = Arc Movement Color (G2/G3) platform.visualizer.color.plunge = Plunge/Raise Movement Color diff --git a/ugs-core/test/com/willwinder/universalgcodesender/communicator/event/AsyncCommunicatorEventDispatcherTest.java b/ugs-core/test/com/willwinder/universalgcodesender/communicator/event/AsyncCommunicatorEventDispatcherTest.java index 68456f99f5..623353bdba 100644 --- a/ugs-core/test/com/willwinder/universalgcodesender/communicator/event/AsyncCommunicatorEventDispatcherTest.java +++ b/ugs-core/test/com/willwinder/universalgcodesender/communicator/event/AsyncCommunicatorEventDispatcherTest.java @@ -1,16 +1,12 @@ package com.willwinder.universalgcodesender.communicator.event; import com.willwinder.universalgcodesender.communicator.ICommunicatorListener; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - import static com.willwinder.universalgcodesender.utils.ThreadHelper.waitUntil; +import org.junit.After; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; @@ -18,6 +14,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + public class AsyncCommunicatorEventDispatcherTest { private AsyncCommunicatorEventDispatcher eventDispatcher; @@ -32,13 +31,13 @@ public void tearDown() { } @Test - public void dispatchShouldStartWorkerThread() throws TimeoutException, InterruptedException { + public void dispatchShouldStartWorkerThread() throws TimeoutException { ICommunicatorListener listener = mock(ICommunicatorListener.class); eventDispatcher.addListener(listener); eventDispatcher.communicatorPausedOnError(); - waitUntil(() -> eventDispatcher.getEventCount() == 0, 1100, TimeUnit.MILLISECONDS); + waitUntil(() -> eventDispatcher.getEventCount() == 0, 2000, TimeUnit.MILLISECONDS); verify(listener, times(1)).communicatorPausedOnError(); } diff --git a/ugs-core/test/com/willwinder/universalgcodesender/gcode/GcodePreprocessorUtilsTest.java b/ugs-core/test/com/willwinder/universalgcodesender/gcode/GcodePreprocessorUtilsTest.java index 5481f4e70a..5b495a73cf 100644 --- a/ugs-core/test/com/willwinder/universalgcodesender/gcode/GcodePreprocessorUtilsTest.java +++ b/ugs-core/test/com/willwinder/universalgcodesender/gcode/GcodePreprocessorUtilsTest.java @@ -20,24 +20,27 @@ This file is part of Universal Gcode Sender (UGS). import com.google.common.collect.ImmutableList; import com.willwinder.universalgcodesender.gcode.util.Code; +import static com.willwinder.universalgcodesender.gcode.util.Code.G1; +import static com.willwinder.universalgcodesender.gcode.util.Code.G2; +import static com.willwinder.universalgcodesender.gcode.util.Code.G3; +import static com.willwinder.universalgcodesender.gcode.util.Code.G38_2; +import static com.willwinder.universalgcodesender.gcode.util.Code.G92_1; import com.willwinder.universalgcodesender.gcode.util.Plane; import com.willwinder.universalgcodesender.gcode.util.PlaneFormatter; import com.willwinder.universalgcodesender.model.PartialPosition; import com.willwinder.universalgcodesender.model.Position; import com.willwinder.universalgcodesender.model.UnitUtils; +import static com.willwinder.universalgcodesender.model.UnitUtils.Units.INCH; +import static com.willwinder.universalgcodesender.model.UnitUtils.Units.MM; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import org.junit.Test; import java.util.ArrayList; import java.util.List; import java.util.Set; -import static com.willwinder.universalgcodesender.gcode.util.Code.*; -import static com.willwinder.universalgcodesender.gcode.util.Code.G1; -import static com.willwinder.universalgcodesender.model.UnitUtils.Units.INCH; -import static com.willwinder.universalgcodesender.model.UnitUtils.Units.MM; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.*; - /** * @author Joacim Breiler */ @@ -335,6 +338,32 @@ public void overridePositionShouldAddMissingCoordinatesToCommand() { assertEquals("G0X1Y2Z3A4B5C6", newCommand); } + @Test + public void updatePointWithCommandShouldSetPositionIfOriginalIsNaN() { + Position position = new Position(Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN, MM); + Position newPosition = GcodePreprocessorUtils.updatePointWithCommand(position, 1, 2, 3, 4, 5,6, false); + assertEquals(1, newPosition.getX(), 0.01); + assertEquals(2, newPosition.getY(), 0.01); + assertEquals(3, newPosition.getZ(), 0.01); + assertEquals(4, newPosition.getA(), 0.01); + assertEquals(5, newPosition.getB(), 0.01); + assertEquals(6, newPosition.getC(), 0.01); + assertEquals(MM, newPosition.getUnits()); + } + + @Test + public void updatePointWithCommandShouldAddToPosition() { + Position position = new Position(1, 2, 3, 4, 5, 6, MM); + Position newPosition = GcodePreprocessorUtils.updatePointWithCommand(position, 1, 2, 3, 4, 5,6, false); + assertEquals(2, newPosition.getX(), 0.01); + assertEquals(4, newPosition.getY(), 0.01); + assertEquals(6, newPosition.getZ(), 0.01); + assertEquals(8, newPosition.getA(), 0.01); + assertEquals(10, newPosition.getB(), 0.01); + assertEquals(12, newPosition.getC(), 0.01); + assertEquals(MM, newPosition.getUnits()); + } + static void assertThatPointsAreWithinBoundary(Position start, Position end, double radius, List points) { assertTrue(points.stream().allMatch(p -> p.x >= start.x)); assertTrue(points.stream().allMatch(p -> p.x <= end.x)); diff --git a/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/options/VisualizerOptions.java b/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/options/VisualizerOptions.java index 6542a97c70..346b24a838 100644 --- a/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/options/VisualizerOptions.java +++ b/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/options/VisualizerOptions.java @@ -43,6 +43,8 @@ public class VisualizerOptions extends ArrayList> { public static String VISUALIZER_OPTION_MODEL_DESC = "platform.visualizer.model.desc"; public static String VISUALIZER_OPTION_LINEAR = "platform.visualizer.color.linear"; public static String VISUALIZER_OPTION_LINEAR_MIN_SPEED = "platform.visualizer.color.linear.min.speed"; + public static String VISUALIZER_OPTION_SPINDLE_MAX_SPEED = "platform.visualizer.color.spindle.max.speed"; + public static String VISUALIZER_OPTION_SPINDLE_MIN_SPEED = "platform.visualizer.color.spindle.min.speed"; public static String VISUALIZER_OPTION_RAPID = "platform.visualizer.color.rapid"; public static String VISUALIZER_OPTION_ARC = "platform.visualizer.color.arc"; public static String VISUALIZER_OPTION_PLUNGE = "platform.visualizer.color.plunge"; @@ -114,7 +116,9 @@ public VisualizerOptions() { // GcodeModel renderable add(getOption(VISUALIZER_OPTION_MODEL, Localization.getString(VISUALIZER_OPTION_MODEL_DESC), true)); add(getOption(VISUALIZER_OPTION_LINEAR, "", new Color(0,0,158))); - add(getOption(VISUALIZER_OPTION_LINEAR_MIN_SPEED, "", new Color(0,0,158, 125))); + add(getOption(VISUALIZER_OPTION_LINEAR_MIN_SPEED, "", new Color(204,255,255))); + add(getOption(VISUALIZER_OPTION_SPINDLE_MAX_SPEED, "", new Color(0,0,158))); + add(getOption(VISUALIZER_OPTION_SPINDLE_MIN_SPEED, "", new Color(204,255,255))); add(getOption(VISUALIZER_OPTION_RAPID, "", new Color(204,204,0))); add(getOption(VISUALIZER_OPTION_ARC, "", new Color(178,34,34))); add(getOption(VISUALIZER_OPTION_PLUNGE, "", new Color(0,100,0))); diff --git a/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/renderables/GcodeLineColorizer.java b/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/renderables/GcodeLineColorizer.java index 408a3ccb85..cf7a144c2d 100644 --- a/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/renderables/GcodeLineColorizer.java +++ b/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/renderables/GcodeLineColorizer.java @@ -19,16 +19,17 @@ This file is part of Universal Gcode Sender (UGS). package com.willwinder.ugs.nbm.visualizer.renderables; import com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions; -import com.willwinder.universalgcodesender.visualizer.LineSegment; - -import java.awt.Color; - import static com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions.VISUALIZER_OPTION_ARC; import static com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions.VISUALIZER_OPTION_COMPLETE; import static com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions.VISUALIZER_OPTION_LINEAR; import static com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions.VISUALIZER_OPTION_LINEAR_MIN_SPEED; import static com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions.VISUALIZER_OPTION_PLUNGE; import static com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions.VISUALIZER_OPTION_RAPID; +import static com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions.VISUALIZER_OPTION_SPINDLE_MAX_SPEED; +import static com.willwinder.ugs.nbm.visualizer.options.VisualizerOptions.VISUALIZER_OPTION_SPINDLE_MIN_SPEED; +import com.willwinder.universalgcodesender.visualizer.LineSegment; + +import java.awt.Color; /** * Generates a color based on the current line segment @@ -37,8 +38,11 @@ This file is part of Universal Gcode Sender (UGS). */ public class GcodeLineColorizer { private double maxSpindleSpeed = 0; + private double maxFeedRate = 0; private Color feedMinColor = Color.BLACK; private Color feedMaxColor = Color.BLACK; + private Color spindleMinColor = Color.BLACK; + private Color spindleMaxColor = Color.BLACK; private Color rapidColor = Color.BLACK; private Color arcColor = Color.BLACK; private Color plungeColor = Color.BLACK; @@ -47,6 +51,8 @@ public class GcodeLineColorizer { public void reloadPreferences(VisualizerOptions vo) { feedMaxColor = vo.getOptionForKey(VISUALIZER_OPTION_LINEAR).value; feedMinColor = vo.getOptionForKey(VISUALIZER_OPTION_LINEAR_MIN_SPEED).value; + spindleMaxColor = vo.getOptionForKey(VISUALIZER_OPTION_SPINDLE_MAX_SPEED).value; + spindleMinColor = vo.getOptionForKey(VISUALIZER_OPTION_SPINDLE_MIN_SPEED).value; rapidColor = vo.getOptionForKey(VISUALIZER_OPTION_RAPID).value; arcColor = vo.getOptionForKey(VISUALIZER_OPTION_ARC).value; plungeColor = vo.getOptionForKey(VISUALIZER_OPTION_PLUNGE).value; @@ -63,26 +69,67 @@ public Color getColor(LineSegment lineSegment, long currentCommandNumber) { } else if (lineSegment.isZMovement()) { return plungeColor; } else { - return getFeedColor(lineSegment.getSpindleSpeed()); + return getFeedColor(lineSegment.getFeedRate(), lineSegment.getSpindleSpeed()); } } - private Color getFeedColor(double spindleSpeed) { - if (maxSpindleSpeed == 0) { - return feedMaxColor; - } - + private Color getFeedColor(double feedRate, double spindleSpeed) { double currentSpindleSpeed = Math.max(spindleSpeed, 0.1); + double currentFeedRate = Math.max(feedRate, 0.1); + + double feedRatePercent = currentFeedRate / maxFeedRate; + Color feedColor = maxFeedRate < 0.01 ? feedMaxColor : getColor(feedMinColor, feedMaxColor, feedRatePercent); double speedPercent = currentSpindleSpeed / maxSpindleSpeed; - int red = Math.min(255, feedMinColor.getRed() + (int) (speedPercent * (feedMaxColor.getRed() - feedMinColor.getRed()))); - int green = Math.min(255, feedMinColor.getGreen() + (int) (speedPercent * (feedMaxColor.getGreen() - feedMinColor.getGreen()))); - int blue = Math.min(255, feedMinColor.getBlue() + (int) (speedPercent * (feedMaxColor.getBlue() - feedMinColor.getBlue()))); - int alpha = Math.min(255, feedMinColor.getAlpha() + (int) (speedPercent * (feedMaxColor.getAlpha() - feedMinColor.getAlpha()))); + Color speedColor = maxSpindleSpeed < 0.1 ? spindleMaxColor : getColor(spindleMinColor, spindleMaxColor, speedPercent); + return blend(speedColor, feedColor); + } + + /** + * Blends multiple colors with the equal amount to one color + * + * @param colors a list of colors to blend + * @return a blended color + */ + public static Color blend(Color... colors) { + if (colors == null || colors.length == 0) { + return null; + } + float ratio = 1f / (colors.length); + + int a = 0; + int r = 0; + int g = 0; + int b = 0; + + for (Color color : colors) { + int rgb = color.getRGB(); + int a1 = (rgb >> 24 & 0xff); + int r1 = ((rgb & 0xff0000) >> 16); + int g1 = ((rgb & 0xff00) >> 8); + int b1 = (rgb & 0xff); + a += a1 * ratio; + r += r1 * ratio; + g += g1 * ratio; + b += b1 * ratio; + } + + return new Color(a << 24 | r << 16 | g << 8 | b); + } + + private Color getColor(Color minColor, Color maxColor, double percent) { + int red = Math.min(255, minColor.getRed() + (int) (percent * (maxColor.getRed() - minColor.getRed()))); + int green = Math.min(255, minColor.getGreen() + (int) (percent * (maxColor.getGreen() - minColor.getGreen()))); + int blue = Math.min(255, minColor.getBlue() + (int) (percent * (maxColor.getBlue() - minColor.getBlue()))); + int alpha = Math.min(255, minColor.getAlpha() + (int) (percent * (maxColor.getAlpha() - minColor.getAlpha()))); return new Color(red, green, blue, alpha); } public void setMaxSpindleSpeed(double maxSpindleSpeed) { this.maxSpindleSpeed = maxSpindleSpeed; } + + public void setMaxFeedRate(double maxFeedRate) { + this.maxFeedRate = maxFeedRate; + } } diff --git a/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/renderables/GcodeModel.java b/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/renderables/GcodeModel.java index 1f1376af68..7e47868ac0 100644 --- a/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/renderables/GcodeModel.java +++ b/ugs-platform/ugs-platform-visualizer/src/main/java/com/willwinder/ugs/nbm/visualizer/renderables/GcodeModel.java @@ -227,6 +227,7 @@ private boolean generateObject() { this.objectMin = gcvp.getMinimumExtremes(); this.objectMax = gcvp.getMaximumExtremes(); this.colorizer.setMaxSpindleSpeed(gcvp.getMaxSpindleSpeed()); + this.colorizer.setMaxFeedRate(gcvp.getMaxFeedRate()); if (gcodeLineList.isEmpty()) { return false;