From 0b2565d981fd98dd0bf5446e0134927f20ce1284 Mon Sep 17 00:00:00 2001 From: James Ball Date: Sat, 24 Oct 2020 13:05:55 +0100 Subject: [PATCH 01/26] Add XML Util class --- src/parser/SvgParser.java | 3 +++ src/parser/XmlUtil.java | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/parser/XmlUtil.java diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index 72b31c4..cb25b81 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -8,6 +8,7 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; +import org.w3c.dom.Node; import org.xml.sax.SAXException; import shapes.Shape; @@ -33,6 +34,8 @@ public class SvgParser extends FileParser { DocumentBuilder builder = factory.newDocumentBuilder(); File file = new File(path); Document doc = builder.parse(file); + + for (Node elem : doc.getElementsByTagName("path")) } @Override diff --git a/src/parser/XmlUtil.java b/src/parser/XmlUtil.java new file mode 100644 index 0000000..8d87a51 --- /dev/null +++ b/src/parser/XmlUtil.java @@ -0,0 +1,30 @@ +package parser; + +import java.util.AbstractList; +import java.util.Collections; +import java.util.List; +import java.util.RandomAccess; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public final class XmlUtil { + private XmlUtil(){} + + public static List asList(NodeList n) { + return n.getLength()==0? + Collections.emptyList(): new NodeListWrapper(n); + } + static final class NodeListWrapper extends AbstractList + implements RandomAccess { + private final NodeList list; + NodeListWrapper(NodeList l) { + list=l; + } + public Node get(int index) { + return list.item(index); + } + public int size() { + return list.getLength(); + } + } +} From 9361e8bdeababc646fd542e4060035d26fdba6dd Mon Sep 17 00:00:00 2001 From: James Ball Date: Sat, 24 Oct 2020 16:54:38 +0100 Subject: [PATCH 02/26] Implement SVG pre-processing --- src/parser/SvgParser.java | 53 +++++++++++++++++++++++--- src/parser/XmlUtil.java | 1 + test/{TestSuite.java => LineTest.java} | 2 +- 3 files changed, 50 insertions(+), 6 deletions(-) rename test/{TestSuite.java => LineTest.java} (96%) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index cb25b81..cd9f898 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -1,8 +1,11 @@ package parser; +import static parser.XmlUtil.asList; + import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -25,17 +28,57 @@ public class SvgParser extends FileParser { shapes = new ArrayList<>(); } - @Override - protected void parseFile(String path) - throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { + private Document getSvgDocument(String path) + throws IOException, SAXException, ParserConfigurationException { + // opens XML reader for svg file. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(true); factory.setIgnoringElementContentWhitespace(true); DocumentBuilder builder = factory.newDocumentBuilder(); File file = new File(path); - Document doc = builder.parse(file); - for (Node elem : doc.getElementsByTagName("path")) + return builder.parse(file); + } + + private String[] preProcessPath(String path) throws IllegalArgumentException { + // Replace all commas with spaces and then remove unnecessary whitespace + path = path.replace(',', ' '); + path = path.replaceAll("\\s+", " "); + path = path.replaceAll("(^\\s|\\s$)", ""); + + // If there are any characters in the path that are illegal + if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) { + throw new IllegalArgumentException("Illegal characters in SVG path."); + // If there are more than 1 letters or delimiters next to one another + } else if (path.matches("[a-zA-Z.\\-]{2,}")) { + throw new IllegalArgumentException("Multiple letters or delimiters found next to one another in SVG path."); + // First character in path must be a command + } else if (path.matches("^[a-zA-Z]")) { + throw new IllegalArgumentException("Start of SVG path is not a letter."); + } + + // Split on SVG path characters to get a list of instructions, keeping the SVG commands + return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])"); + } + + private List getSvgPathAttributes(Document svg) { + List paths = new ArrayList<>(); + + for (Node elem : asList(svg.getElementsByTagName("path"))) { + paths.add(elem.getAttributes().getNamedItem("d").getNodeValue()); + } + + return paths; + } + + @Override + protected void parseFile(String filePath) + throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { + Document svg = getSvgDocument(filePath); + + for (String path : getSvgPathAttributes(svg)) { + preProcessPath(path); + } } @Override diff --git a/src/parser/XmlUtil.java b/src/parser/XmlUtil.java index 8d87a51..576ba06 100644 --- a/src/parser/XmlUtil.java +++ b/src/parser/XmlUtil.java @@ -14,6 +14,7 @@ public final class XmlUtil { return n.getLength()==0? Collections.emptyList(): new NodeListWrapper(n); } + static final class NodeListWrapper extends AbstractList implements RandomAccess { private final NodeList list; diff --git a/test/TestSuite.java b/test/LineTest.java similarity index 96% rename from test/TestSuite.java rename to test/LineTest.java index 5758768..0994029 100644 --- a/test/TestSuite.java +++ b/test/LineTest.java @@ -3,7 +3,7 @@ import shapes.Line; import static org.junit.Assert.*; -public class TestSuite { +public class LineTest { // TODO: Create tests for shapes.Shapes class. @Test From 70112fa3a96dff08a5178953ec1b2b44cd5a6e9d Mon Sep 17 00:00:00 2001 From: James Ball Date: Sat, 24 Oct 2020 19:31:04 +0100 Subject: [PATCH 03/26] Create main SVG parser functions --- src/parser/FileParser.java | 2 +- src/parser/SvgParser.java | 127 +++++++++++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/parser/FileParser.java b/src/parser/FileParser.java index 73180aa..c3a706d 100644 --- a/src/parser/FileParser.java +++ b/src/parser/FileParser.java @@ -31,5 +31,5 @@ public abstract class FileParser { protected abstract void parseFile(String path) throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException; - public abstract List getShapes(); + public abstract List getShapes(); } diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index cd9f898..e0e4155 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -6,7 +6,11 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -17,10 +21,108 @@ import shapes.Shape; public class SvgParser extends FileParser { - private final List shapes; + private final List shapes; + private static final Map, List>> commandMap = new HashMap<>(); static { fileExtension = "svg"; + + commandMap.put('M', SvgParser::parseMoveToAbsolute); + commandMap.put('m', SvgParser::parseMoveToRelative); + commandMap.put('L', SvgParser::parseLineToAbsolute); + commandMap.put('l', SvgParser::parseLineToRelative); + commandMap.put('H', SvgParser::parseHorizontalLineToAbsolute); + commandMap.put('h', SvgParser::parseHorizontalLineToRelative); + commandMap.put('V', SvgParser::parseVerticalLineToAbsolute); + commandMap.put('v', SvgParser::parseVerticalLineToRelative); + commandMap.put('C', SvgParser::parseCurveToAbsolute); + commandMap.put('c', SvgParser::parseCurveToRelative); + commandMap.put('S', SvgParser::parseSmoothCurveToAbsolute); + commandMap.put('s', SvgParser::parseSmoothCurveToRelative); + commandMap.put('Q', SvgParser::parseQuadraticCurveToAbsolute); + commandMap.put('q', SvgParser::parseQuadraticCurveToRelative); + commandMap.put('T', SvgParser::parseSmoothQuadraticCurveToAbsolute); + commandMap.put('t', SvgParser::parseSmoothQuadraticCurveToRelative); + commandMap.put('A', SvgParser::parseEllipticalArcAbsolute); + commandMap.put('a', SvgParser::parseEllipticalArcRelative); + commandMap.put('Z', SvgParser::parseClosePath); + commandMap.put('z', SvgParser::parseClosePath); + } + + private static List parseMoveToAbsolute(List args) { + return null; + } + + private static List parseMoveToRelative(List args) { + return null; + } + + private static List parseClosePath(List args) { + return null; + } + + private static List parseLineToAbsolute(List args) { + return null; + } + + private static List parseLineToRelative(List args) { + return null; + } + + private static List parseHorizontalLineToAbsolute(List args) { + return null; + } + + private static List parseHorizontalLineToRelative(List args) { + return null; + } + + private static List parseVerticalLineToAbsolute(List args) { + return null; + } + + private static List parseVerticalLineToRelative(List args) { + return null; + } + + private static List parseCurveToAbsolute(List args) { + return null; + } + + private static List parseCurveToRelative(List args) { + return null; + } + + private static List parseSmoothCurveToAbsolute(List args) { + return null; + } + + private static List parseSmoothCurveToRelative(List args) { + return null; + } + + private static List parseQuadraticCurveToAbsolute(List args) { + return null; + } + + private static List parseQuadraticCurveToRelative(List args) { + return null; + } + + private static List parseSmoothQuadraticCurveToAbsolute(List args) { + return null; + } + + private static List parseSmoothQuadraticCurveToRelative(List args) { + return null; + } + + private static List parseEllipticalArcAbsolute(List args) { + return null; + } + + private static List parseEllipticalArcRelative(List args) { + return null; } public SvgParser(String path) throws IOException, SAXException, ParserConfigurationException { @@ -49,10 +151,11 @@ public class SvgParser extends FileParser { // If there are any characters in the path that are illegal if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) { throw new IllegalArgumentException("Illegal characters in SVG path."); - // If there are more than 1 letters or delimiters next to one another + // If there are more than 1 letters or delimiters next to one another } else if (path.matches("[a-zA-Z.\\-]{2,}")) { - throw new IllegalArgumentException("Multiple letters or delimiters found next to one another in SVG path."); - // First character in path must be a command + throw new IllegalArgumentException( + "Multiple letters or delimiters found next to one another in SVG path."); + // First character in path must be a command } else if (path.matches("^[a-zA-Z]")) { throw new IllegalArgumentException("Start of SVG path is not a letter."); } @@ -76,13 +179,25 @@ public class SvgParser extends FileParser { throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { Document svg = getSvgDocument(filePath); + // Get all d attributes within path elements in the SVG file. for (String path : getSvgPathAttributes(svg)) { - preProcessPath(path); + String[] commands = preProcessPath(path); + + for (String command : commands) { + // Split the command into number strings and convert them into floats. + List nums = Arrays.stream(command.substring(1).split(" ")) + .map(Float::parseFloat) + .collect(Collectors.toList()); + + // Use the nums to get a list of shapes, using the first character in the command to specify + // the function to use. + shapes.addAll(commandMap.get(command.charAt(0)).apply(nums)); + } } } @Override - public List getShapes() { + public List getShapes() { return shapes; } } From ea2f299eaa41530b64e0fbdc29fb013e495c476d Mon Sep 17 00:00:00 2001 From: James Ball Date: Sat, 24 Oct 2020 19:43:50 +0100 Subject: [PATCH 04/26] Fix implementation of SVG parser --- src/parser/FileParser.java | 7 +- src/parser/SvgParser.java | 207 +++++++++++++++++++------------------ 2 files changed, 108 insertions(+), 106 deletions(-) diff --git a/src/parser/FileParser.java b/src/parser/FileParser.java index c3a706d..66e8092 100644 --- a/src/parser/FileParser.java +++ b/src/parser/FileParser.java @@ -15,12 +15,7 @@ public abstract class FileParser { return fileExtension; } - public FileParser(String path) throws IOException, SAXException, ParserConfigurationException { - checkFileExtension(path); - parseFile(path); - } - - private static void checkFileExtension(String path) throws IllegalArgumentException { + protected static void checkFileExtension(String path) throws IllegalArgumentException { Pattern pattern = Pattern.compile("\\." + getFileExtension() + "$"); if (!pattern.matcher(path).find()) { throw new IllegalArgumentException( diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index e0e4155..42591df 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -18,116 +18,116 @@ import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.SAXException; import shapes.Shape; +import shapes.Vector2; public class SvgParser extends FileParser { private final List shapes; - private static final Map, List>> commandMap = new HashMap<>(); + private final Map, List>> commandMap; + + private Vector2 pos; static { fileExtension = "svg"; - - commandMap.put('M', SvgParser::parseMoveToAbsolute); - commandMap.put('m', SvgParser::parseMoveToRelative); - commandMap.put('L', SvgParser::parseLineToAbsolute); - commandMap.put('l', SvgParser::parseLineToRelative); - commandMap.put('H', SvgParser::parseHorizontalLineToAbsolute); - commandMap.put('h', SvgParser::parseHorizontalLineToRelative); - commandMap.put('V', SvgParser::parseVerticalLineToAbsolute); - commandMap.put('v', SvgParser::parseVerticalLineToRelative); - commandMap.put('C', SvgParser::parseCurveToAbsolute); - commandMap.put('c', SvgParser::parseCurveToRelative); - commandMap.put('S', SvgParser::parseSmoothCurveToAbsolute); - commandMap.put('s', SvgParser::parseSmoothCurveToRelative); - commandMap.put('Q', SvgParser::parseQuadraticCurveToAbsolute); - commandMap.put('q', SvgParser::parseQuadraticCurveToRelative); - commandMap.put('T', SvgParser::parseSmoothQuadraticCurveToAbsolute); - commandMap.put('t', SvgParser::parseSmoothQuadraticCurveToRelative); - commandMap.put('A', SvgParser::parseEllipticalArcAbsolute); - commandMap.put('a', SvgParser::parseEllipticalArcRelative); - commandMap.put('Z', SvgParser::parseClosePath); - commandMap.put('z', SvgParser::parseClosePath); - } - - private static List parseMoveToAbsolute(List args) { - return null; - } - - private static List parseMoveToRelative(List args) { - return null; - } - - private static List parseClosePath(List args) { - return null; - } - - private static List parseLineToAbsolute(List args) { - return null; - } - - private static List parseLineToRelative(List args) { - return null; - } - - private static List parseHorizontalLineToAbsolute(List args) { - return null; - } - - private static List parseHorizontalLineToRelative(List args) { - return null; - } - - private static List parseVerticalLineToAbsolute(List args) { - return null; - } - - private static List parseVerticalLineToRelative(List args) { - return null; - } - - private static List parseCurveToAbsolute(List args) { - return null; - } - - private static List parseCurveToRelative(List args) { - return null; - } - - private static List parseSmoothCurveToAbsolute(List args) { - return null; - } - - private static List parseSmoothCurveToRelative(List args) { - return null; - } - - private static List parseQuadraticCurveToAbsolute(List args) { - return null; - } - - private static List parseQuadraticCurveToRelative(List args) { - return null; - } - - private static List parseSmoothQuadraticCurveToAbsolute(List args) { - return null; - } - - private static List parseSmoothQuadraticCurveToRelative(List args) { - return null; - } - - private static List parseEllipticalArcAbsolute(List args) { - return null; - } - - private static List parseEllipticalArcRelative(List args) { - return null; } public SvgParser(String path) throws IOException, SAXException, ParserConfigurationException { - super(path); + FileParser.checkFileExtension(path); shapes = new ArrayList<>(); + + commandMap = new HashMap<>(); + commandMap.put('M', this::parseMoveToAbsolute); + commandMap.put('m', this::parseMoveToRelative); + commandMap.put('L', this::parseLineToAbsolute); + commandMap.put('l', this::parseLineToRelative); + commandMap.put('H', this::parseHorizontalLineToAbsolute); + commandMap.put('h', this::parseHorizontalLineToRelative); + commandMap.put('V', this::parseVerticalLineToAbsolute); + commandMap.put('v', this::parseVerticalLineToRelative); + commandMap.put('C', this::parseCurveToAbsolute); + commandMap.put('c', this::parseCurveToRelative); + commandMap.put('S', this::parseSmoothCurveToAbsolute); + commandMap.put('s', this::parseSmoothCurveToRelative); + commandMap.put('Q', this::parseQuadraticCurveToAbsolute); + commandMap.put('q', this::parseQuadraticCurveToRelative); + commandMap.put('T', this::parseSmoothQuadraticCurveToAbsolute); + commandMap.put('t', this::parseSmoothQuadraticCurveToRelative); + commandMap.put('A', this::parseEllipticalArcAbsolute); + commandMap.put('a', this::parseEllipticalArcRelative); + + parseFile(path); + } + + private List parseMoveToAbsolute(List args) { + return new ArrayList<>(); + } + + private List parseMoveToRelative(List args) { + return null; + } + + private List parseLineToAbsolute(List args) { + return null; + } + + private List parseLineToRelative(List args) { + return null; + } + + private List parseHorizontalLineToAbsolute(List args) { + return null; + } + + private List parseHorizontalLineToRelative(List args) { + return null; + } + + private List parseVerticalLineToAbsolute(List args) { + return null; + } + + private List parseVerticalLineToRelative(List args) { + return null; + } + + private List parseCurveToAbsolute(List args) { + return null; + } + + private List parseCurveToRelative(List args) { + return null; + } + + private List parseSmoothCurveToAbsolute(List args) { + return null; + } + + private List parseSmoothCurveToRelative(List args) { + return null; + } + + private List parseQuadraticCurveToAbsolute(List args) { + return null; + } + + private List parseQuadraticCurveToRelative(List args) { + return null; + } + + private List parseSmoothQuadraticCurveToAbsolute(List args) { + return null; + } + + private List parseSmoothQuadraticCurveToRelative(List args) { + return null; + } + + private List parseEllipticalArcAbsolute(List args) { + return null; + } + + private List parseEllipticalArcRelative(List args) { + return null; } private Document getSvgDocument(String path) @@ -181,9 +181,16 @@ public class SvgParser extends FileParser { // Get all d attributes within path elements in the SVG file. for (String path : getSvgPathAttributes(svg)) { + pos = new Vector2(); String[] commands = preProcessPath(path); for (String command : commands) { + char commandChar = command.charAt(0); + + if (commandChar == 'z' || commandChar == 'Z') { + break; + } + // Split the command into number strings and convert them into floats. List nums = Arrays.stream(command.substring(1).split(" ")) .map(Float::parseFloat) @@ -191,7 +198,7 @@ public class SvgParser extends FileParser { // Use the nums to get a list of shapes, using the first character in the command to specify // the function to use. - shapes.addAll(commandMap.get(command.charAt(0)).apply(nums)); + shapes.addAll(commandMap.get(commandChar).apply(nums)); } } } From 3bb97209511665e4882d33aba381c483d933ec1b Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 25 Oct 2020 14:49:11 +0000 Subject: [PATCH 05/26] Complete svg-parser initial implementation --- src/parser/SvgParser.java | 270 +++++++++++++++++++++++++++++++++++--- src/shapes/Vector2.java | 12 ++ 2 files changed, 262 insertions(+), 20 deletions(-) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index 42591df..ccc75d1 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -2,6 +2,7 @@ package parser; import static parser.XmlUtil.asList; +import java.awt.font.ShapeGraphicAttribute; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -17,6 +18,9 @@ import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.SAXException; +import shapes.CubicBezierCurve; +import shapes.Line; +import shapes.QuadraticBezierCurve; import shapes.Shape; import shapes.Vector2; @@ -25,7 +29,10 @@ public class SvgParser extends FileParser { private final List shapes; private final Map, List>> commandMap; - private Vector2 pos; + private Vector2 currPoint; + private Vector2 initialPoint; + private Vector2 prevCubicControlPoint; + private Vector2 prevQuadraticControlPoint; static { fileExtension = "svg"; @@ -54,80 +61,301 @@ public class SvgParser extends FileParser { commandMap.put('t', this::parseSmoothQuadraticCurveToRelative); commandMap.put('A', this::parseEllipticalArcAbsolute); commandMap.put('a', this::parseEllipticalArcRelative); + commandMap.put('Z', this::parseClosePath); + commandMap.put('z', this::parseClosePath); parseFile(path); } private List parseMoveToAbsolute(List args) { - return new ArrayList<>(); + return parseMoveTo(args, true); } private List parseMoveToRelative(List args) { - return null; + return parseMoveTo(args, false); + } + + private List parseMoveTo(List args, boolean isAbsolute) { + if (args.size() % 2 != 0 || args.size() < 2) { + throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments."); + } + + Vector2 vec = new Vector2(args.get(0), args.get(1)); + + if (isAbsolute) { + currPoint = vec; + initialPoint = currPoint; + if (args.size() > 2) { + return parseLineToAbsolute(args.subList(2, args.size() - 1)); + } + } else { + currPoint = currPoint.translate(vec); + initialPoint = currPoint; + if (args.size() > 2) { + return parseLineToRelative(args.subList(2, args.size() - 1)); + } + } + + return new ArrayList<>(); + } + + private List parseClosePath(List args) { + Line line = new Line(currPoint, initialPoint); + currPoint = initialPoint; + return List.of(line); } private List parseLineToAbsolute(List args) { - return null; + return parseLineTo(args, true); } private List parseLineToRelative(List args) { - return null; + return parseLineTo(args, false); + } + + private List parseLineTo(List args, boolean isAbsolute) { + if (args.size() % 2 != 0 || args.size() < 2) { + throw new IllegalArgumentException("SVG lineto command has incorrect number of arguments."); + } + + List lines = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += 2) { + Vector2 newPoint = new Vector2(args.get(i), args.get(i + 1)); + + if (!isAbsolute) { + newPoint = currPoint.translate(newPoint); + } + + lines.add(new Line(currPoint, newPoint)); + currPoint = newPoint; + } + + prevCubicControlPoint = null; + prevQuadraticControlPoint = null; + + return lines; } private List parseHorizontalLineToAbsolute(List args) { - return null; + return parseHorizontalLineTo(args, true); } private List parseHorizontalLineToRelative(List args) { - return null; + return parseHorizontalLineTo(args, false); + } + + private List parseHorizontalLineTo(List args, boolean isAbsolute) { + List lines = new ArrayList<>(); + + for (Float point : args) { + Vector2 newPoint; + + if (isAbsolute) { + newPoint = new Vector2(point, currPoint.getY()); + } else { + newPoint = currPoint.translate(new Vector2(point, 0)); + } + + lines.add(new Line(currPoint, newPoint)); + currPoint = newPoint; + } + + prevCubicControlPoint = null; + prevQuadraticControlPoint = null; + + return lines; } private List parseVerticalLineToAbsolute(List args) { - return null; + return parseVerticalLineTo(args, true); } private List parseVerticalLineToRelative(List args) { - return null; + return parseVerticalLineTo(args, false); + } + + private List parseVerticalLineTo(List args, boolean isAbsolute) { + List lines = new ArrayList<>(); + + for (Float point : args) { + Vector2 newPoint; + + if (isAbsolute) { + newPoint = new Vector2(currPoint.getX(), point); + } else { + newPoint = currPoint.translate(new Vector2(0, point)); + } + + lines.add(new Line(currPoint, newPoint)); + currPoint = newPoint; + } + + prevCubicControlPoint = null; + prevQuadraticControlPoint = null; + + return lines; } private List parseCurveToAbsolute(List args) { - return null; + return parseCurveTo(args, true); } private List parseCurveToRelative(List args) { - return null; + return parseCurveTo(args, false); + } + + private List parseCurveTo(List args, boolean isAbsolute) { + if (args.size() % 6 != 0 || args.size() < 6) { + throw new IllegalArgumentException("SVG curveto command has incorrect number of arguments."); + } + + List curves = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += 6) { + Vector2 controlPoint1 = new Vector2(args.get(i), args.get(i + 1)); + Vector2 controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3)); + Vector2 newPoint = new Vector2(args.get(i + 4), args.get(i + 5)); + + if (!isAbsolute) { + controlPoint1 = currPoint.translate(controlPoint1); + controlPoint2 = currPoint.translate(controlPoint2); + newPoint = currPoint.translate(newPoint); + } + + curves.add(new CubicBezierCurve(currPoint, controlPoint1, controlPoint2, newPoint)); + currPoint = newPoint; + prevCubicControlPoint = controlPoint2; + } + + prevQuadraticControlPoint = null; + + return curves; } private List parseSmoothCurveToAbsolute(List args) { - return null; + return parseSmoothCurveTo(args, true); } private List parseSmoothCurveToRelative(List args) { - return null; + return parseSmoothCurveTo(args, false); + } + + private List parseSmoothCurveTo(List args, boolean isAbsolute) { + if (args.size() % 4 != 0 || args.size() < 4) { + throw new IllegalArgumentException("SVG smooth curveto command has incorrect number of arguments."); + } + + List curves = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += 4) { + Vector2 controlPoint1 = prevCubicControlPoint == null ? currPoint : prevCubicControlPoint.reflectRelativeToVector(currPoint); + Vector2 controlPoint2 = new Vector2(args.get(i), args.get(i + 1)); + Vector2 newPoint = new Vector2(args.get(i + 2), args.get(i + 3)); + + if (!isAbsolute) { + controlPoint2 = currPoint.translate(controlPoint2); + newPoint = currPoint.translate(newPoint); + } + + curves.add(new CubicBezierCurve(currPoint, controlPoint1, controlPoint2, newPoint)); + currPoint = newPoint; + prevCubicControlPoint = controlPoint2; + } + + prevQuadraticControlPoint = null; + + return curves; } private List parseQuadraticCurveToAbsolute(List args) { - return null; + return parseQuadraticCurveTo(args, true); } private List parseQuadraticCurveToRelative(List args) { - return null; + return parseQuadraticCurveTo(args, false); + } + + private List parseQuadraticCurveTo(List args, boolean isAbsolute) { + if (args.size() % 4 != 0 || args.size() < 4) { + throw new IllegalArgumentException("SVG quadratic curveto command has incorrect number of arguments."); + } + + List curves = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += 4) { + Vector2 controlPoint = new Vector2(args.get(i), args.get(i + 1)); + Vector2 newPoint = new Vector2(args.get(i + 2), args.get(i + 3)); + + if (!isAbsolute) { + controlPoint = currPoint.translate(controlPoint); + newPoint = currPoint.translate(newPoint); + } + + curves.add(new QuadraticBezierCurve(currPoint, controlPoint, newPoint)); + currPoint = newPoint; + prevQuadraticControlPoint = controlPoint; + } + + prevCubicControlPoint = null; + + return curves; } private List parseSmoothQuadraticCurveToAbsolute(List args) { - return null; + return parseSmoothQuadraticCurveTo(args, true); } private List parseSmoothQuadraticCurveToRelative(List args) { - return null; + return parseSmoothQuadraticCurveTo(args, false); + } + + private List parseSmoothQuadraticCurveTo(List args, boolean isAbsolute) { + if (args.size() % 2 != 0 || args.size() < 2) { + throw new IllegalArgumentException("SVG quadratic smooth curveto command has incorrect number of arguments."); + } + + List curves = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += 2) { + Vector2 controlPoint = prevQuadraticControlPoint == null ? currPoint : prevQuadraticControlPoint.reflectRelativeToVector(currPoint); + Vector2 newPoint = new Vector2(args.get(i), args.get(i + 1)); + + if (!isAbsolute) { + newPoint = currPoint.translate(newPoint); + } + + curves.add(new QuadraticBezierCurve(currPoint, controlPoint, newPoint)); + currPoint = newPoint; + prevQuadraticControlPoint = controlPoint; + } + + prevCubicControlPoint = null; + + return curves; } private List parseEllipticalArcAbsolute(List args) { - return null; + return parseEllipticalArc(args, true); } private List parseEllipticalArcRelative(List args) { - return null; + return parseEllipticalArc(args, false); + } + + private List parseEllipticalArc(List args, boolean isAbsolute) { + if (args.size() % 7 != 0 || args.size() < 7) { + throw new IllegalArgumentException("SVG elliptical arc command has incorrect number of arguments."); + } + + List lineToArgs = new ArrayList<>(); + + for (int i = 0; i < args.size(); i += 7) { + lineToArgs.add(args.get(i + 5)); + lineToArgs.add(args.get(i + 6)); + } + + return parseLineTo(lineToArgs, isAbsolute); } private Document getSvgDocument(String path) @@ -181,7 +409,9 @@ public class SvgParser extends FileParser { // Get all d attributes within path elements in the SVG file. for (String path : getSvgPathAttributes(svg)) { - pos = new Vector2(); + currPoint = new Vector2(); + prevCubicControlPoint = null; + prevQuadraticControlPoint = null; String[] commands = preProcessPath(path); for (String command : commands) { diff --git a/src/shapes/Vector2.java b/src/shapes/Vector2.java index 67b0a4e..75749ce 100644 --- a/src/shapes/Vector2.java +++ b/src/shapes/Vector2.java @@ -41,6 +41,18 @@ public final class Vector2 extends Shape { return new Vector2(x, y); } + public Vector2 add(Vector2 vector) { + return translate(vector); + } + + public Vector2 sub(Vector2 vector) { + return new Vector2(getX() - vector.getX(), getY() - vector.getY()); + } + + public Vector2 reflectRelativeToVector(Vector2 vector) { + return translate(vector.sub(this).scale(2)); + } + @Override public float nextX(double drawingProgress) { return (float) getX(); From 7cd2900756966a81fd3488684952d9f1766baeab Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 25 Oct 2020 16:41:00 +0000 Subject: [PATCH 06/26] Refactor curveto commands in parser --- src/parser/SvgParser.java | 203 ++++++++++++++------------------------ 1 file changed, 73 insertions(+), 130 deletions(-) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index ccc75d1..c571ffe 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -2,7 +2,6 @@ package parser; import static parser.XmlUtil.asList; -import java.awt.font.ShapeGraphicAttribute; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -51,8 +50,8 @@ public class SvgParser extends FileParser { commandMap.put('h', this::parseHorizontalLineToRelative); commandMap.put('V', this::parseVerticalLineToAbsolute); commandMap.put('v', this::parseVerticalLineToRelative); - commandMap.put('C', this::parseCurveToAbsolute); - commandMap.put('c', this::parseCurveToRelative); + commandMap.put('C', this::parseCubicCurveToAbsolute); + commandMap.put('c', this::parseCubicCurveToRelative); commandMap.put('S', this::parseSmoothCurveToAbsolute); commandMap.put('s', this::parseSmoothCurveToRelative); commandMap.put('Q', this::parseQuadraticCurveToAbsolute); @@ -131,9 +130,6 @@ public class SvgParser extends FileParser { currPoint = newPoint; } - prevCubicControlPoint = null; - prevQuadraticControlPoint = null; - return lines; } @@ -161,9 +157,6 @@ public class SvgParser extends FileParser { currPoint = newPoint; } - prevCubicControlPoint = null; - prevQuadraticControlPoint = null; - return lines; } @@ -191,31 +184,72 @@ public class SvgParser extends FileParser { currPoint = newPoint; } - prevCubicControlPoint = null; - prevQuadraticControlPoint = null; - return lines; } - private List parseCurveToAbsolute(List args) { - return parseCurveTo(args, true); + private List parseCubicCurveToAbsolute(List args) { + return parseCurveTo(args, true, true, false); } - private List parseCurveToRelative(List args) { - return parseCurveTo(args, false); + private List parseCubicCurveToRelative(List args) { + return parseCurveTo(args, false, true, false); } - private List parseCurveTo(List args, boolean isAbsolute) { - if (args.size() % 6 != 0 || args.size() < 6) { + private List parseSmoothCurveToAbsolute(List args) { + return parseCurveTo(args, true, true, true); + } + + private List parseSmoothCurveToRelative(List args) { + return parseCurveTo(args, false, true, true); + } + + private List parseQuadraticCurveToAbsolute(List args) { + return parseCurveTo(args, true, false, false); + } + + private List parseQuadraticCurveToRelative(List args) { + return parseCurveTo(args, false, false, false); + } + + private List parseSmoothQuadraticCurveToAbsolute(List args) { + return parseCurveTo(args, true, false, true); + } + + private List parseSmoothQuadraticCurveToRelative(List args) { + return parseCurveTo(args, false, false, true); + } + + private List parseCurveTo(List args, boolean isAbsolute, boolean isCubic, boolean isSmooth) { + int expectedArgs = isCubic ? 4 : 2; + if (!isSmooth) { + expectedArgs += 2; + } + + if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) { throw new IllegalArgumentException("SVG curveto command has incorrect number of arguments."); } - List curves = new ArrayList<>(); + List curves = new ArrayList<>(); - for (int i = 0; i < args.size(); i += 6) { - Vector2 controlPoint1 = new Vector2(args.get(i), args.get(i + 1)); - Vector2 controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3)); - Vector2 newPoint = new Vector2(args.get(i + 4), args.get(i + 5)); + for (int i = 0; i < args.size(); i += expectedArgs) { + Vector2 controlPoint1; + Vector2 controlPoint2 = new Vector2(); + + if (isSmooth) { + if (isCubic) { + controlPoint1 = prevCubicControlPoint == null ? currPoint : prevCubicControlPoint.reflectRelativeToVector(currPoint); + } else { + controlPoint1 = prevQuadraticControlPoint == null ? currPoint : prevQuadraticControlPoint.reflectRelativeToVector(currPoint); + } + } else { + controlPoint1 = new Vector2(args.get(i), args.get(i + 1)); + } + + if (isCubic) { + controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3)); + } + + Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2), args.get(i + expectedArgs - 1)); if (!isAbsolute) { controlPoint1 = currPoint.translate(controlPoint1); @@ -223,115 +257,17 @@ public class SvgParser extends FileParser { newPoint = currPoint.translate(newPoint); } - curves.add(new CubicBezierCurve(currPoint, controlPoint1, controlPoint2, newPoint)); - currPoint = newPoint; - prevCubicControlPoint = controlPoint2; - } - - prevQuadraticControlPoint = null; - - return curves; - } - - private List parseSmoothCurveToAbsolute(List args) { - return parseSmoothCurveTo(args, true); - } - - private List parseSmoothCurveToRelative(List args) { - return parseSmoothCurveTo(args, false); - } - - private List parseSmoothCurveTo(List args, boolean isAbsolute) { - if (args.size() % 4 != 0 || args.size() < 4) { - throw new IllegalArgumentException("SVG smooth curveto command has incorrect number of arguments."); - } - - List curves = new ArrayList<>(); - - for (int i = 0; i < args.size(); i += 4) { - Vector2 controlPoint1 = prevCubicControlPoint == null ? currPoint : prevCubicControlPoint.reflectRelativeToVector(currPoint); - Vector2 controlPoint2 = new Vector2(args.get(i), args.get(i + 1)); - Vector2 newPoint = new Vector2(args.get(i + 2), args.get(i + 3)); - - if (!isAbsolute) { - controlPoint2 = currPoint.translate(controlPoint2); - newPoint = currPoint.translate(newPoint); + if (isCubic) { + curves.add(new CubicBezierCurve(currPoint, controlPoint1, controlPoint2, newPoint)); + currPoint = newPoint; + prevCubicControlPoint = controlPoint2; + } else { + curves.add(new QuadraticBezierCurve(currPoint, controlPoint1, newPoint)); + currPoint = newPoint; + prevQuadraticControlPoint = controlPoint1; } - - curves.add(new CubicBezierCurve(currPoint, controlPoint1, controlPoint2, newPoint)); - currPoint = newPoint; - prevCubicControlPoint = controlPoint2; } - prevQuadraticControlPoint = null; - - return curves; - } - - private List parseQuadraticCurveToAbsolute(List args) { - return parseQuadraticCurveTo(args, true); - } - - private List parseQuadraticCurveToRelative(List args) { - return parseQuadraticCurveTo(args, false); - } - - private List parseQuadraticCurveTo(List args, boolean isAbsolute) { - if (args.size() % 4 != 0 || args.size() < 4) { - throw new IllegalArgumentException("SVG quadratic curveto command has incorrect number of arguments."); - } - - List curves = new ArrayList<>(); - - for (int i = 0; i < args.size(); i += 4) { - Vector2 controlPoint = new Vector2(args.get(i), args.get(i + 1)); - Vector2 newPoint = new Vector2(args.get(i + 2), args.get(i + 3)); - - if (!isAbsolute) { - controlPoint = currPoint.translate(controlPoint); - newPoint = currPoint.translate(newPoint); - } - - curves.add(new QuadraticBezierCurve(currPoint, controlPoint, newPoint)); - currPoint = newPoint; - prevQuadraticControlPoint = controlPoint; - } - - prevCubicControlPoint = null; - - return curves; - } - - private List parseSmoothQuadraticCurveToAbsolute(List args) { - return parseSmoothQuadraticCurveTo(args, true); - } - - private List parseSmoothQuadraticCurveToRelative(List args) { - return parseSmoothQuadraticCurveTo(args, false); - } - - private List parseSmoothQuadraticCurveTo(List args, boolean isAbsolute) { - if (args.size() % 2 != 0 || args.size() < 2) { - throw new IllegalArgumentException("SVG quadratic smooth curveto command has incorrect number of arguments."); - } - - List curves = new ArrayList<>(); - - for (int i = 0; i < args.size(); i += 2) { - Vector2 controlPoint = prevQuadraticControlPoint == null ? currPoint : prevQuadraticControlPoint.reflectRelativeToVector(currPoint); - Vector2 newPoint = new Vector2(args.get(i), args.get(i + 1)); - - if (!isAbsolute) { - newPoint = currPoint.translate(newPoint); - } - - curves.add(new QuadraticBezierCurve(currPoint, controlPoint, newPoint)); - currPoint = newPoint; - prevQuadraticControlPoint = controlPoint; - } - - prevCubicControlPoint = null; - return curves; } @@ -418,7 +354,7 @@ public class SvgParser extends FileParser { char commandChar = command.charAt(0); if (commandChar == 'z' || commandChar == 'Z') { - break; + continue; } // Split the command into number strings and convert them into floats. @@ -429,6 +365,13 @@ public class SvgParser extends FileParser { // Use the nums to get a list of shapes, using the first character in the command to specify // the function to use. shapes.addAll(commandMap.get(commandChar).apply(nums)); + + if (!String.valueOf(commandChar).matches("[csCS]")) { + prevCubicControlPoint = null; + } + if (!String.valueOf(commandChar).matches("[qtQT]")) { + prevQuadraticControlPoint = null; + } } } } From eb76441a05eee907b6d3b27a9aa6e322fb6d4f5a Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 25 Oct 2020 17:29:25 +0000 Subject: [PATCH 07/26] Refactor function definitions and merge repetitive functions --- src/parser/SvgParser.java | 192 +++++++++++--------------------------- 1 file changed, 53 insertions(+), 139 deletions(-) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index c571ffe..b5db85f 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -42,38 +42,30 @@ public class SvgParser extends FileParser { shapes = new ArrayList<>(); commandMap = new HashMap<>(); - commandMap.put('M', this::parseMoveToAbsolute); - commandMap.put('m', this::parseMoveToRelative); - commandMap.put('L', this::parseLineToAbsolute); - commandMap.put('l', this::parseLineToRelative); - commandMap.put('H', this::parseHorizontalLineToAbsolute); - commandMap.put('h', this::parseHorizontalLineToRelative); - commandMap.put('V', this::parseVerticalLineToAbsolute); - commandMap.put('v', this::parseVerticalLineToRelative); - commandMap.put('C', this::parseCubicCurveToAbsolute); - commandMap.put('c', this::parseCubicCurveToRelative); - commandMap.put('S', this::parseSmoothCurveToAbsolute); - commandMap.put('s', this::parseSmoothCurveToRelative); - commandMap.put('Q', this::parseQuadraticCurveToAbsolute); - commandMap.put('q', this::parseQuadraticCurveToRelative); - commandMap.put('T', this::parseSmoothQuadraticCurveToAbsolute); - commandMap.put('t', this::parseSmoothQuadraticCurveToRelative); - commandMap.put('A', this::parseEllipticalArcAbsolute); - commandMap.put('a', this::parseEllipticalArcRelative); + commandMap.put('M', (args) -> parseMoveTo(args, true)); + commandMap.put('m', (args) -> parseMoveTo(args, false)); + commandMap.put('L', (args) -> parseLineTo(args, true, true, true)); + commandMap.put('l', (args) -> parseLineTo(args, false, true, true)); + commandMap.put('H', (args) -> parseLineTo(args, true, true, false)); + commandMap.put('h', (args) -> parseLineTo(args, false, true, false)); + commandMap.put('V', (args) -> parseLineTo(args, true, false, true)); + commandMap.put('v', (args) -> parseLineTo(args, false, false, true)); + commandMap.put('C', (args) -> parseCurveTo(args, true, true, false)); + commandMap.put('c', (args) -> parseCurveTo(args, false, true, false)); + commandMap.put('S', (args) -> parseCurveTo(args, true, true, true)); + commandMap.put('s', (args) -> parseCurveTo(args, false, true, true)); + commandMap.put('Q', (args) -> parseCurveTo(args, true, false, false)); + commandMap.put('q', (args) -> parseCurveTo(args, false, false, false)); + commandMap.put('T', (args) -> parseCurveTo(args, true, false, true)); + commandMap.put('t', (args) -> parseCurveTo(args, false, false, true)); + commandMap.put('A', (args) -> parseEllipticalArc(args, true)); + commandMap.put('a', (args) -> parseEllipticalArc(args, false)); commandMap.put('Z', this::parseClosePath); commandMap.put('z', this::parseClosePath); parseFile(path); } - private List parseMoveToAbsolute(List args) { - return parseMoveTo(args, true); - } - - private List parseMoveToRelative(List args) { - return parseMoveTo(args, false); - } - private List parseMoveTo(List args, boolean isAbsolute) { if (args.size() % 2 != 0 || args.size() < 2) { throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments."); @@ -85,13 +77,13 @@ public class SvgParser extends FileParser { currPoint = vec; initialPoint = currPoint; if (args.size() > 2) { - return parseLineToAbsolute(args.subList(2, args.size() - 1)); + return parseLineTo(args.subList(2, args.size() - 1), true, true, true); } } else { currPoint = currPoint.translate(vec); initialPoint = currPoint; if (args.size() > 2) { - return parseLineToRelative(args.subList(2, args.size() - 1)); + return parseLineTo(args.subList(2, args.size() - 1), false, true, true); } } @@ -104,53 +96,35 @@ public class SvgParser extends FileParser { return List.of(line); } - private List parseLineToAbsolute(List args) { - return parseLineTo(args, true); - } + private List parseLineTo(List args, boolean isAbsolute, + boolean isHorizontal, boolean isVertical) { + int expectedArgs = isHorizontal && isVertical ? 2 : 1; - private List parseLineToRelative(List args) { - return parseLineTo(args, false); - } - - private List parseLineTo(List args, boolean isAbsolute) { - if (args.size() % 2 != 0 || args.size() < 2) { + if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) { throw new IllegalArgumentException("SVG lineto command has incorrect number of arguments."); } List lines = new ArrayList<>(); - for (int i = 0; i < args.size(); i += 2) { - Vector2 newPoint = new Vector2(args.get(i), args.get(i + 1)); - - if (!isAbsolute) { - newPoint = currPoint.translate(newPoint); - } - - lines.add(new Line(currPoint, newPoint)); - currPoint = newPoint; - } - - return lines; - } - - private List parseHorizontalLineToAbsolute(List args) { - return parseHorizontalLineTo(args, true); - } - - private List parseHorizontalLineToRelative(List args) { - return parseHorizontalLineTo(args, false); - } - - private List parseHorizontalLineTo(List args, boolean isAbsolute) { - List lines = new ArrayList<>(); - - for (Float point : args) { - Vector2 newPoint; + for (int i = 0; i < args.size(); i += expectedArgs) { + Vector2 newPoint = currPoint; if (isAbsolute) { - newPoint = new Vector2(point, currPoint.getY()); + if (isHorizontal && isVertical) { + newPoint = new Vector2(args.get(i), args.get(i + 1)); + } else if (isHorizontal) { + newPoint = new Vector2(args.get(i), currPoint.getY()); + } else if (isVertical) { + newPoint = new Vector2(currPoint.getX(), args.get(i)); + } } else { - newPoint = currPoint.translate(new Vector2(point, 0)); + if (isHorizontal && isVertical) { + newPoint = currPoint.translate(newPoint); + } else if (isHorizontal) { + newPoint = currPoint.translate(new Vector2(args.get(i), 0)); + } else if (isVertical) { + newPoint = currPoint.translate(new Vector2(0, args.get(i))); + } } lines.add(new Line(currPoint, newPoint)); @@ -160,66 +134,8 @@ public class SvgParser extends FileParser { return lines; } - private List parseVerticalLineToAbsolute(List args) { - return parseVerticalLineTo(args, true); - } - - private List parseVerticalLineToRelative(List args) { - return parseVerticalLineTo(args, false); - } - - private List parseVerticalLineTo(List args, boolean isAbsolute) { - List lines = new ArrayList<>(); - - for (Float point : args) { - Vector2 newPoint; - - if (isAbsolute) { - newPoint = new Vector2(currPoint.getX(), point); - } else { - newPoint = currPoint.translate(new Vector2(0, point)); - } - - lines.add(new Line(currPoint, newPoint)); - currPoint = newPoint; - } - - return lines; - } - - private List parseCubicCurveToAbsolute(List args) { - return parseCurveTo(args, true, true, false); - } - - private List parseCubicCurveToRelative(List args) { - return parseCurveTo(args, false, true, false); - } - - private List parseSmoothCurveToAbsolute(List args) { - return parseCurveTo(args, true, true, true); - } - - private List parseSmoothCurveToRelative(List args) { - return parseCurveTo(args, false, true, true); - } - - private List parseQuadraticCurveToAbsolute(List args) { - return parseCurveTo(args, true, false, false); - } - - private List parseQuadraticCurveToRelative(List args) { - return parseCurveTo(args, false, false, false); - } - - private List parseSmoothQuadraticCurveToAbsolute(List args) { - return parseCurveTo(args, true, false, true); - } - - private List parseSmoothQuadraticCurveToRelative(List args) { - return parseCurveTo(args, false, false, true); - } - - private List parseCurveTo(List args, boolean isAbsolute, boolean isCubic, boolean isSmooth) { + private List parseCurveTo(List args, boolean isAbsolute, boolean isCubic, + boolean isSmooth) { int expectedArgs = isCubic ? 4 : 2; if (!isSmooth) { expectedArgs += 2; @@ -237,9 +153,11 @@ public class SvgParser extends FileParser { if (isSmooth) { if (isCubic) { - controlPoint1 = prevCubicControlPoint == null ? currPoint : prevCubicControlPoint.reflectRelativeToVector(currPoint); + controlPoint1 = prevCubicControlPoint == null ? currPoint + : prevCubicControlPoint.reflectRelativeToVector(currPoint); } else { - controlPoint1 = prevQuadraticControlPoint == null ? currPoint : prevQuadraticControlPoint.reflectRelativeToVector(currPoint); + controlPoint1 = prevQuadraticControlPoint == null ? currPoint + : prevQuadraticControlPoint.reflectRelativeToVector(currPoint); } } else { controlPoint1 = new Vector2(args.get(i), args.get(i + 1)); @@ -249,7 +167,8 @@ public class SvgParser extends FileParser { controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3)); } - Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2), args.get(i + expectedArgs - 1)); + Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2), + args.get(i + expectedArgs - 1)); if (!isAbsolute) { controlPoint1 = currPoint.translate(controlPoint1); @@ -271,17 +190,12 @@ public class SvgParser extends FileParser { return curves; } - private List parseEllipticalArcAbsolute(List args) { - return parseEllipticalArc(args, true); - } - - private List parseEllipticalArcRelative(List args) { - return parseEllipticalArc(args, false); - } - private List parseEllipticalArc(List args, boolean isAbsolute) { + // TODO: Properly implement + if (args.size() % 7 != 0 || args.size() < 7) { - throw new IllegalArgumentException("SVG elliptical arc command has incorrect number of arguments."); + throw new IllegalArgumentException( + "SVG elliptical arc command has incorrect number of arguments."); } List lineToArgs = new ArrayList<>(); @@ -291,7 +205,7 @@ public class SvgParser extends FileParser { lineToArgs.add(args.get(i + 6)); } - return parseLineTo(lineToArgs, isAbsolute); + return parseLineTo(lineToArgs, isAbsolute, true, true); } private Document getSvgDocument(String path) From 4fe27f2dca3e744d6d45fb0ec0724ff2c0d8d4ac Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 25 Oct 2020 17:41:00 +0000 Subject: [PATCH 08/26] Correct sub-path closing --- src/parser/SvgParser.java | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index b5db85f..278c783 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -28,6 +28,7 @@ public class SvgParser extends FileParser { private final List shapes; private final Map, List>> commandMap; + private Vector2 currPoint; private Vector2 initialPoint; private Vector2 prevCubicControlPoint; @@ -91,9 +92,13 @@ public class SvgParser extends FileParser { } private List parseClosePath(List args) { - Line line = new Line(currPoint, initialPoint); - currPoint = initialPoint; - return List.of(line); + if (!currPoint.equals(initialPoint)) { + Line line = new Line(currPoint, initialPoint); + currPoint = initialPoint; + return List.of(line); + } else { + return List.of(); + } } private List parseLineTo(List args, boolean isAbsolute, @@ -266,16 +271,15 @@ public class SvgParser extends FileParser { for (String command : commands) { char commandChar = command.charAt(0); + List nums = null; - if (commandChar == 'z' || commandChar == 'Z') { - continue; + if (commandChar != 'z' && commandChar != 'Z') { + // Split the command into number strings and convert them into floats. + nums = Arrays.stream(command.substring(1).split(" ")) + .map(Float::parseFloat) + .collect(Collectors.toList()); } - // Split the command into number strings and convert them into floats. - List nums = Arrays.stream(command.substring(1).split(" ")) - .map(Float::parseFloat) - .collect(Collectors.toList()); - // Use the nums to get a list of shapes, using the first character in the command to specify // the function to use. shapes.addAll(commandMap.get(commandChar).apply(nums)); From 26ae41a3af955b15825dce0d7c0bd5b085c30287 Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 25 Oct 2020 17:55:47 +0000 Subject: [PATCH 09/26] Add comments to SVG parser --- src/parser/SvgParser.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index 278c783..0acc0f9 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -43,6 +43,7 @@ public class SvgParser extends FileParser { shapes = new ArrayList<>(); commandMap = new HashMap<>(); + // Map command chars to function calls. commandMap.put('M', (args) -> parseMoveTo(args, true)); commandMap.put('m', (args) -> parseMoveTo(args, false)); commandMap.put('L', (args) -> parseLineTo(args, true, true, true)); @@ -67,6 +68,7 @@ public class SvgParser extends FileParser { parseFile(path); } + // Parses moveto commands (M and m commands) private List parseMoveTo(List args, boolean isAbsolute) { if (args.size() % 2 != 0 || args.size() < 2) { throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments."); @@ -91,6 +93,7 @@ public class SvgParser extends FileParser { return new ArrayList<>(); } + // Parses close path commands (Z and z commands) private List parseClosePath(List args) { if (!currPoint.equals(initialPoint)) { Line line = new Line(currPoint, initialPoint); @@ -101,6 +104,10 @@ public class SvgParser extends FileParser { } } + // Parses lineto commands (L, l, H, h, V, and v commands) + // isHorizontal and isVertical should be true for parsing L and l commands + // Only isHorizontal should be true for parsing H and h commands + // Only isVertical should be true for parsing V and v commands private List parseLineTo(List args, boolean isAbsolute, boolean isHorizontal, boolean isVertical) { int expectedArgs = isHorizontal && isVertical ? 2 : 1; @@ -139,6 +146,11 @@ public class SvgParser extends FileParser { return lines; } + // Parses curveto commands (C, c, S, s, Q, q, T, and t commands) + // isCubic should be true for parsing C, c, S, and s commands + // isCubic should be false for parsing Q, q, T, and t commands + // isSmooth should be true for parsing S, s, T, and t commands + // isSmooth should be false for parsing C, c, Q, and q commands private List parseCurveTo(List args, boolean isAbsolute, boolean isCubic, boolean isSmooth) { int expectedArgs = isCubic ? 4 : 2; @@ -225,6 +237,7 @@ public class SvgParser extends FileParser { return builder.parse(file); } + // Does error checking against SVG path and returns array of SVG commands and arguments private String[] preProcessPath(String path) throws IllegalArgumentException { // Replace all commas with spaces and then remove unnecessary whitespace path = path.replace(',', ' '); @@ -247,6 +260,7 @@ public class SvgParser extends FileParser { return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])"); } + // Returns list of SVG path data attributes private List getSvgPathAttributes(Document svg) { List paths = new ArrayList<>(); From dde6cf8aac28210dd41eccd17b588e979435eacf Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 25 Oct 2020 17:59:32 +0000 Subject: [PATCH 10/26] Reorder functions in SvgParser --- src/parser/SvgParser.java | 176 +++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index 0acc0f9..23c6910 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -68,6 +68,94 @@ public class SvgParser extends FileParser { parseFile(path); } + private Document getSvgDocument(String path) + throws IOException, SAXException, ParserConfigurationException { + // opens XML reader for svg file. + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(true); + factory.setIgnoringElementContentWhitespace(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + File file = new File(path); + + return builder.parse(file); + } + + // Does error checking against SVG path and returns array of SVG commands and arguments + private String[] preProcessPath(String path) throws IllegalArgumentException { + // Replace all commas with spaces and then remove unnecessary whitespace + path = path.replace(',', ' '); + path = path.replaceAll("\\s+", " "); + path = path.replaceAll("(^\\s|\\s$)", ""); + + // If there are any characters in the path that are illegal + if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) { + throw new IllegalArgumentException("Illegal characters in SVG path."); + // If there are more than 1 letters or delimiters next to one another + } else if (path.matches("[a-zA-Z.\\-]{2,}")) { + throw new IllegalArgumentException( + "Multiple letters or delimiters found next to one another in SVG path."); + // First character in path must be a command + } else if (path.matches("^[a-zA-Z]")) { + throw new IllegalArgumentException("Start of SVG path is not a letter."); + } + + // Split on SVG path characters to get a list of instructions, keeping the SVG commands + return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])"); + } + + // Returns list of SVG path data attributes + private List getSvgPathAttributes(Document svg) { + List paths = new ArrayList<>(); + + for (Node elem : asList(svg.getElementsByTagName("path"))) { + paths.add(elem.getAttributes().getNamedItem("d").getNodeValue()); + } + + return paths; + } + + @Override + protected void parseFile(String filePath) + throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { + Document svg = getSvgDocument(filePath); + + // Get all d attributes within path elements in the SVG file. + for (String path : getSvgPathAttributes(svg)) { + currPoint = new Vector2(); + prevCubicControlPoint = null; + prevQuadraticControlPoint = null; + String[] commands = preProcessPath(path); + + for (String command : commands) { + char commandChar = command.charAt(0); + List nums = null; + + if (commandChar != 'z' && commandChar != 'Z') { + // Split the command into number strings and convert them into floats. + nums = Arrays.stream(command.substring(1).split(" ")) + .map(Float::parseFloat) + .collect(Collectors.toList()); + } + + // Use the nums to get a list of shapes, using the first character in the command to specify + // the function to use. + shapes.addAll(commandMap.get(commandChar).apply(nums)); + + if (!String.valueOf(commandChar).matches("[csCS]")) { + prevCubicControlPoint = null; + } + if (!String.valueOf(commandChar).matches("[qtQT]")) { + prevQuadraticControlPoint = null; + } + } + } + } + + @Override + public List getShapes() { + return shapes; + } + // Parses moveto commands (M and m commands) private List parseMoveTo(List args, boolean isAbsolute) { if (args.size() % 2 != 0 || args.size() < 2) { @@ -224,92 +312,4 @@ public class SvgParser extends FileParser { return parseLineTo(lineToArgs, isAbsolute, true, true); } - - private Document getSvgDocument(String path) - throws IOException, SAXException, ParserConfigurationException { - // opens XML reader for svg file. - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setValidating(true); - factory.setIgnoringElementContentWhitespace(true); - DocumentBuilder builder = factory.newDocumentBuilder(); - File file = new File(path); - - return builder.parse(file); - } - - // Does error checking against SVG path and returns array of SVG commands and arguments - private String[] preProcessPath(String path) throws IllegalArgumentException { - // Replace all commas with spaces and then remove unnecessary whitespace - path = path.replace(',', ' '); - path = path.replaceAll("\\s+", " "); - path = path.replaceAll("(^\\s|\\s$)", ""); - - // If there are any characters in the path that are illegal - if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) { - throw new IllegalArgumentException("Illegal characters in SVG path."); - // If there are more than 1 letters or delimiters next to one another - } else if (path.matches("[a-zA-Z.\\-]{2,}")) { - throw new IllegalArgumentException( - "Multiple letters or delimiters found next to one another in SVG path."); - // First character in path must be a command - } else if (path.matches("^[a-zA-Z]")) { - throw new IllegalArgumentException("Start of SVG path is not a letter."); - } - - // Split on SVG path characters to get a list of instructions, keeping the SVG commands - return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])"); - } - - // Returns list of SVG path data attributes - private List getSvgPathAttributes(Document svg) { - List paths = new ArrayList<>(); - - for (Node elem : asList(svg.getElementsByTagName("path"))) { - paths.add(elem.getAttributes().getNamedItem("d").getNodeValue()); - } - - return paths; - } - - @Override - protected void parseFile(String filePath) - throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { - Document svg = getSvgDocument(filePath); - - // Get all d attributes within path elements in the SVG file. - for (String path : getSvgPathAttributes(svg)) { - currPoint = new Vector2(); - prevCubicControlPoint = null; - prevQuadraticControlPoint = null; - String[] commands = preProcessPath(path); - - for (String command : commands) { - char commandChar = command.charAt(0); - List nums = null; - - if (commandChar != 'z' && commandChar != 'Z') { - // Split the command into number strings and convert them into floats. - nums = Arrays.stream(command.substring(1).split(" ")) - .map(Float::parseFloat) - .collect(Collectors.toList()); - } - - // Use the nums to get a list of shapes, using the first character in the command to specify - // the function to use. - shapes.addAll(commandMap.get(commandChar).apply(nums)); - - if (!String.valueOf(commandChar).matches("[csCS]")) { - prevCubicControlPoint = null; - } - if (!String.valueOf(commandChar).matches("[qtQT]")) { - prevQuadraticControlPoint = null; - } - } - } - } - - @Override - public List getShapes() { - return shapes; - } } From e8f03e6f684d549b06dee66c326300bca4108b00 Mon Sep 17 00:00:00 2001 From: James Ball Date: Sun, 25 Oct 2020 19:00:15 +0000 Subject: [PATCH 11/26] Add test for checking closing of paths --- test/SvgParserTest.java | 25 +++++++++++++++++++++++++ test/images/sine-wave.svg | 13 +++++++++++++ test/images/square-relative.svg | 6 ++++++ 3 files changed, 44 insertions(+) create mode 100644 test/SvgParserTest.java create mode 100644 test/images/sine-wave.svg create mode 100644 test/images/square-relative.svg diff --git a/test/SvgParserTest.java b/test/SvgParserTest.java new file mode 100644 index 0000000..6a904a9 --- /dev/null +++ b/test/SvgParserTest.java @@ -0,0 +1,25 @@ +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.List; +import javax.xml.parsers.ParserConfigurationException; +import org.junit.Test; +import org.xml.sax.SAXException; +import parser.SvgParser; +import shapes.Line; +import shapes.Shape; +import shapes.Vector2; + +public class SvgParserTest { + + @Test + public void closingASubPathDrawsLineToInitialPoint() + throws ParserConfigurationException, SAXException, IOException { + SvgParser svgParser = new SvgParser("test/images/square-relative.svg"); + List shapes = svgParser.getShapes(); + assertEquals(shapes.get(0), new Line(new Vector2(10, 10), new Vector2(12, 10))); + assertEquals(shapes.get(1), new Line(new Vector2(12, 10), new Vector2(12, 12))); + assertEquals(shapes.get(2), new Line(new Vector2(12, 12), new Vector2(10, 12))); + assertEquals(shapes.get(3), new Line(new Vector2(10, 12), new Vector2(10, 10))); + } +} diff --git a/test/images/sine-wave.svg b/test/images/sine-wave.svg new file mode 100644 index 0000000..1f75e31 --- /dev/null +++ b/test/images/sine-wave.svg @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/test/images/square-relative.svg b/test/images/square-relative.svg new file mode 100644 index 0000000..3ff451f --- /dev/null +++ b/test/images/square-relative.svg @@ -0,0 +1,6 @@ + \ No newline at end of file From 797f1fe42558233014a1db3d557429d46f8c5819 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 26 Oct 2020 17:25:50 +0000 Subject: [PATCH 12/26] Refactor parseLineTo and scale coordinates by viewBox attribute --- src/parser/SvgParser.java | 58 +++++++++++++------ test/SvgParserTest.java | 15 +++-- ...quare-relative.svg => closing-subpath.svg} | 10 ++-- 3 files changed, 55 insertions(+), 28 deletions(-) rename test/images/{square-relative.svg => closing-subpath.svg} (78%) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index 23c6910..b00add9 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -9,6 +9,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Vector; import java.util.function.Function; import java.util.stream.Collectors; import javax.xml.parsers.DocumentBuilder; @@ -28,6 +29,8 @@ public class SvgParser extends FileParser { private final List shapes; private final Map, List>> commandMap; + private float width; + private float height; private Vector2 currPoint; private Vector2 initialPoint; @@ -104,7 +107,7 @@ public class SvgParser extends FileParser { } // Returns list of SVG path data attributes - private List getSvgPathAttributes(Document svg) { + private static List getSvgPathAttributes(Document svg) { List paths = new ArrayList<>(); for (Node elem : asList(svg.getElementsByTagName("path"))) { @@ -114,10 +117,31 @@ public class SvgParser extends FileParser { return paths; } + // Returns the width and height of the viewBox attribute + private static List getViewBoxDimensions(Node svgElem) { + return Arrays.stream( + svgElem.getAttributes() + .getNamedItem("viewBox") + .getNodeValue() + .split(" ")) + .map(Float::parseFloat) + .skip(2) + .collect(Collectors.toList()); + } + @Override protected void parseFile(String filePath) throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { Document svg = getSvgDocument(filePath); + List svgElem = asList(svg.getElementsByTagName("svg")); + + if (svgElem.size() != 1) { + throw new IllegalArgumentException("SVG has either zero or more than one svg element."); + } + + List dimensions = getViewBoxDimensions(svgElem.get(0)); + width = dimensions.get(0); + height = dimensions.get(1); // Get all d attributes within path elements in the SVG file. for (String path : getSvgPathAttributes(svg)) { @@ -162,7 +186,7 @@ public class SvgParser extends FileParser { throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments."); } - Vector2 vec = new Vector2(args.get(0), args.get(1)); + Vector2 vec = new Vector2(args.get(0) / width, args.get(1) / height); if (isAbsolute) { currPoint = vec; @@ -207,24 +231,22 @@ public class SvgParser extends FileParser { List lines = new ArrayList<>(); for (int i = 0; i < args.size(); i += expectedArgs) { - Vector2 newPoint = currPoint; + Vector2 newPoint; - if (isAbsolute) { - if (isHorizontal && isVertical) { - newPoint = new Vector2(args.get(i), args.get(i + 1)); - } else if (isHorizontal) { - newPoint = new Vector2(args.get(i), currPoint.getY()); - } else if (isVertical) { - newPoint = new Vector2(currPoint.getX(), args.get(i)); - } + if (expectedArgs == 1) { + newPoint = new Vector2(args.get(i) / width, args.get(i) / height); } else { - if (isHorizontal && isVertical) { - newPoint = currPoint.translate(newPoint); - } else if (isHorizontal) { - newPoint = currPoint.translate(new Vector2(args.get(i), 0)); - } else if (isVertical) { - newPoint = currPoint.translate(new Vector2(0, args.get(i))); - } + newPoint = new Vector2(args.get(i) / width, args.get(i + 1) / height); + } + + if (isHorizontal && !isVertical) { + newPoint = isAbsolute ? newPoint.setY(currPoint.getY()) : newPoint.setY(0); + } else if (isVertical && !isHorizontal) { + newPoint = isAbsolute ? newPoint.setX(currPoint.getX()) : newPoint.setX(0); + } + + if (!isAbsolute) { + newPoint = currPoint.translate(newPoint); } lines.add(new Line(currPoint, newPoint)); diff --git a/test/SvgParserTest.java b/test/SvgParserTest.java index 6a904a9..515b070 100644 --- a/test/SvgParserTest.java +++ b/test/SvgParserTest.java @@ -12,14 +12,19 @@ import shapes.Vector2; public class SvgParserTest { + @Test + public void lineToGeneratesALineShape() { + + } + @Test public void closingASubPathDrawsLineToInitialPoint() throws ParserConfigurationException, SAXException, IOException { - SvgParser svgParser = new SvgParser("test/images/square-relative.svg"); + SvgParser svgParser = new SvgParser("test/images/closing-subpath.svg"); List shapes = svgParser.getShapes(); - assertEquals(shapes.get(0), new Line(new Vector2(10, 10), new Vector2(12, 10))); - assertEquals(shapes.get(1), new Line(new Vector2(12, 10), new Vector2(12, 12))); - assertEquals(shapes.get(2), new Line(new Vector2(12, 12), new Vector2(10, 12))); - assertEquals(shapes.get(3), new Line(new Vector2(10, 12), new Vector2(10, 10))); + assertEquals(shapes.get(0), new Line(new Vector2(0.5, 0.5), new Vector2(0.75, 0.5))); + assertEquals(shapes.get(1), new Line(new Vector2(0.75, 0.5), new Vector2(0.75, 0.75))); + assertEquals(shapes.get(2), new Line(new Vector2(0.75, 0.75), new Vector2(0.5, 0.75))); + assertEquals(shapes.get(3), new Line(new Vector2(0.5, 0.75), new Vector2(0.5, 0.5))); } } diff --git a/test/images/square-relative.svg b/test/images/closing-subpath.svg similarity index 78% rename from test/images/square-relative.svg rename to test/images/closing-subpath.svg index 3ff451f..ca601be 100644 --- a/test/images/square-relative.svg +++ b/test/images/closing-subpath.svg @@ -1,6 +1,6 @@ - \ No newline at end of file From 4ff80b2ad0c7a86ec2ff3297027a51317961e49a Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 26 Oct 2020 17:34:45 +0000 Subject: [PATCH 13/26] Pass lineToGeneratesALineShape --- src/shapes/Line.java | 8 ++++++++ src/shapes/Vector2.java | 8 ++++++++ test/SvgParserTest.java | 9 +++++++-- test/images/line-to.svg | 5 +++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 test/images/line-to.svg diff --git a/src/shapes/Line.java b/src/shapes/Line.java index 91d5f82..b090b83 100644 --- a/src/shapes/Line.java +++ b/src/shapes/Line.java @@ -114,4 +114,12 @@ public final class Line extends Shape { return false; } } + + @Override + public String toString() { + return "Line{" + + "a=" + a + + ", b=" + b + + '}'; + } } diff --git a/src/shapes/Vector2.java b/src/shapes/Vector2.java index 75749ce..324ce93 100644 --- a/src/shapes/Vector2.java +++ b/src/shapes/Vector2.java @@ -114,4 +114,12 @@ public final class Vector2 extends Shape { return (double) Math.round(value) / factor; } + + @Override + public String toString() { + return "Vector2{" + + "x=" + x + + ", y=" + y + + '}'; + } } diff --git a/test/SvgParserTest.java b/test/SvgParserTest.java index 515b070..410fd1a 100644 --- a/test/SvgParserTest.java +++ b/test/SvgParserTest.java @@ -13,8 +13,13 @@ import shapes.Vector2; public class SvgParserTest { @Test - public void lineToGeneratesALineShape() { - + public void lineToGeneratesALineShape() + throws ParserConfigurationException, SAXException, IOException { + SvgParser svgParser = new SvgParser("test/images/line-to.svg"); + List shapes = svgParser.getShapes(); + assertEquals(shapes.get(0), new Line(new Vector2(0.5, 0.5), new Vector2(0.75, 1))); + assertEquals(shapes.get(1), new Line(new Vector2(0.75, 1), new Vector2(0, 0))); + assertEquals(shapes.get(2), new Line(new Vector2(0, 0), new Vector2(0.5, 0.5))); } @Test diff --git a/test/images/line-to.svg b/test/images/line-to.svg new file mode 100644 index 0000000..ec08c84 --- /dev/null +++ b/test/images/line-to.svg @@ -0,0 +1,5 @@ + \ No newline at end of file From 6f076b9cdef5be53baa5716c5c018b40047a4110 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 26 Oct 2020 17:50:20 +0000 Subject: [PATCH 14/26] Add helper function to Line for generating paths and pass horizontalLineToGeneratesAHorizontalLineShape --- src/shapes/Line.java | 17 +++++++++++++++++ test/SvgParserTest.java | 17 ++++++++++------- test/images/horizontal-line-to.svg | 5 +++++ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 test/images/horizontal-line-to.svg diff --git a/src/shapes/Line.java b/src/shapes/Line.java index b090b83..df0c0d6 100644 --- a/src/shapes/Line.java +++ b/src/shapes/Line.java @@ -1,5 +1,8 @@ package shapes; +import java.util.ArrayList; +import java.util.List; + public final class Line extends Shape { private final Vector2 a; @@ -97,6 +100,20 @@ public final class Line extends Shape { return new Line(getX1(), getY1(), getX2(), y2); } + public static List pathToLines(double... path) { + List lines = new ArrayList<>(); + + Vector2 prev = new Vector2(path[0], path[1]); + + for (int i = 2; i < path.length; i += 2) { + Vector2 dest = new Vector2(path[i], path[i + 1]); + lines.add(new Line(prev, dest)); + prev = dest; + } + + return lines; + } + @Override public Line setWeight(double weight) { return new Line(getX1(), getY1(), getX2(), getY2(), weight); diff --git a/test/SvgParserTest.java b/test/SvgParserTest.java index 410fd1a..9bb8954 100644 --- a/test/SvgParserTest.java +++ b/test/SvgParserTest.java @@ -17,9 +17,15 @@ public class SvgParserTest { throws ParserConfigurationException, SAXException, IOException { SvgParser svgParser = new SvgParser("test/images/line-to.svg"); List shapes = svgParser.getShapes(); - assertEquals(shapes.get(0), new Line(new Vector2(0.5, 0.5), new Vector2(0.75, 1))); - assertEquals(shapes.get(1), new Line(new Vector2(0.75, 1), new Vector2(0, 0))); - assertEquals(shapes.get(2), new Line(new Vector2(0, 0), new Vector2(0.5, 0.5))); + assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.75, 1, 0, 0, 0.5, 0.5)); + } + + @Test + public void horizontalLineToGeneratesAHorizontalLineShape() + throws ParserConfigurationException, SAXException, IOException { + SvgParser svgParser = new SvgParser("test/images/horizontal-line-to.svg"); + List shapes = svgParser.getShapes(); + assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5, 0.5)); } @Test @@ -27,9 +33,6 @@ public class SvgParserTest { throws ParserConfigurationException, SAXException, IOException { SvgParser svgParser = new SvgParser("test/images/closing-subpath.svg"); List shapes = svgParser.getShapes(); - assertEquals(shapes.get(0), new Line(new Vector2(0.5, 0.5), new Vector2(0.75, 0.5))); - assertEquals(shapes.get(1), new Line(new Vector2(0.75, 0.5), new Vector2(0.75, 0.75))); - assertEquals(shapes.get(2), new Line(new Vector2(0.75, 0.75), new Vector2(0.5, 0.75))); - assertEquals(shapes.get(3), new Line(new Vector2(0.5, 0.75), new Vector2(0.5, 0.5))); + assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0.75, 0.75, 0.5, 0.75, 0.5, 0.5)); } } diff --git a/test/images/horizontal-line-to.svg b/test/images/horizontal-line-to.svg new file mode 100644 index 0000000..3e8892d --- /dev/null +++ b/test/images/horizontal-line-to.svg @@ -0,0 +1,5 @@ + \ No newline at end of file From 34e7229a8bea49bf5a049347202e4d36c3df0ed4 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 26 Oct 2020 17:53:04 +0000 Subject: [PATCH 15/26] Pass verticalLineToGeneratesAVerticalLineShape --- test/SvgParserTest.java | 8 ++++++++ test/images/vertical-line-to.svg | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 test/images/vertical-line-to.svg diff --git a/test/SvgParserTest.java b/test/SvgParserTest.java index 9bb8954..e75ecce 100644 --- a/test/SvgParserTest.java +++ b/test/SvgParserTest.java @@ -28,6 +28,14 @@ public class SvgParserTest { assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5, 0.5)); } + @Test + public void verticalLineToGeneratesAVerticalLineShape() + throws ParserConfigurationException, SAXException, IOException { + SvgParser svgParser = new SvgParser("test/images/vertical-line-to.svg"); + List shapes = svgParser.getShapes(); + assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5)); + } + @Test public void closingASubPathDrawsLineToInitialPoint() throws ParserConfigurationException, SAXException, IOException { diff --git a/test/images/vertical-line-to.svg b/test/images/vertical-line-to.svg new file mode 100644 index 0000000..47ed56e --- /dev/null +++ b/test/images/vertical-line-to.svg @@ -0,0 +1,5 @@ + \ No newline at end of file From af70109e4f565527a81a4c3af1c49bbc6154c391 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 26 Oct 2020 20:59:05 +0000 Subject: [PATCH 16/26] Fix SVG scaling and split numbers with multiple decimal points correctly --- src/parser/SvgParser.java | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index b00add9..325ab03 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.Vector; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -87,6 +88,7 @@ public class SvgParser extends FileParser { private String[] preProcessPath(String path) throws IllegalArgumentException { // Replace all commas with spaces and then remove unnecessary whitespace path = path.replace(',', ' '); + path = path.replace("-", " -"); path = path.replaceAll("\\s+", " "); path = path.replaceAll("(^\\s|\\s$)", ""); @@ -129,6 +131,28 @@ public class SvgParser extends FileParser { .collect(Collectors.toList()); } + private static List splitCommand(String command) { + List nums = new ArrayList<>(); + String[] decimalSplit = command.split("\\."); + + try { + if (decimalSplit.length == 1) { + nums.add(Float.parseFloat(decimalSplit[0])); + } else { + nums.add(Float.parseFloat(decimalSplit[0] + "." + decimalSplit[1])); + + for (int i = 2; i < decimalSplit.length; i++) { + nums.add(Float.parseFloat("." + decimalSplit[i])); + } + } + } catch (Exception e) { + System.out.println(Arrays.toString(decimalSplit)); + System.out.println(command); + } + + return nums; + } + @Override protected void parseFile(String filePath) throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { @@ -157,7 +181,8 @@ public class SvgParser extends FileParser { if (commandChar != 'z' && commandChar != 'Z') { // Split the command into number strings and convert them into floats. nums = Arrays.stream(command.substring(1).split(" ")) - .map(Float::parseFloat) + .filter(Predicate.not(String::isBlank)) + .flatMap((numString) -> splitCommand(numString).stream()) .collect(Collectors.toList()); } @@ -287,15 +312,15 @@ public class SvgParser extends FileParser { : prevQuadraticControlPoint.reflectRelativeToVector(currPoint); } } else { - controlPoint1 = new Vector2(args.get(i), args.get(i + 1)); + controlPoint1 = new Vector2(args.get(i) / width, args.get(i + 1) / height); } if (isCubic) { - controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3)); + controlPoint2 = new Vector2(args.get(i + 2) / width, args.get(i + 3) / height); } - Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2), - args.get(i + expectedArgs - 1)); + Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2) / width, + args.get(i + expectedArgs - 1) / height); if (!isAbsolute) { controlPoint1 = currPoint.translate(controlPoint1); From 6c1a09fb3e66bb6853ad751160f4b18539c7291c Mon Sep 17 00:00:00 2001 From: James Ball Date: Wed, 28 Oct 2020 17:05:15 +0000 Subject: [PATCH 17/26] Reduce repeated argument scaling and add vector scaling to shapes --- src/parser/SvgParser.java | 17 ++++++++++------- src/shapes/CubicBezierCurve.java | 13 +++++++++---- src/shapes/Ellipse.java | 5 +++++ src/shapes/Line.java | 5 +++++ src/shapes/Shape.java | 2 ++ src/shapes/Vector2.java | 5 +++++ 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index 325ab03..2bff2be 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -205,13 +205,17 @@ public class SvgParser extends FileParser { return shapes; } + private Vector2 scaledArguments(float arg1, float arg2) { + return new Vector2(arg1 / width, arg2 / height); + } + // Parses moveto commands (M and m commands) private List parseMoveTo(List args, boolean isAbsolute) { if (args.size() % 2 != 0 || args.size() < 2) { throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments."); } - Vector2 vec = new Vector2(args.get(0) / width, args.get(1) / height); + Vector2 vec = scaledArguments(args.get(0), args.get(1)); if (isAbsolute) { currPoint = vec; @@ -259,9 +263,9 @@ public class SvgParser extends FileParser { Vector2 newPoint; if (expectedArgs == 1) { - newPoint = new Vector2(args.get(i) / width, args.get(i) / height); + newPoint = scaledArguments(args.get(i), args.get(i)); } else { - newPoint = new Vector2(args.get(i) / width, args.get(i + 1) / height); + newPoint = scaledArguments(args.get(i), args.get(i + 1)); } if (isHorizontal && !isVertical) { @@ -312,15 +316,14 @@ public class SvgParser extends FileParser { : prevQuadraticControlPoint.reflectRelativeToVector(currPoint); } } else { - controlPoint1 = new Vector2(args.get(i) / width, args.get(i + 1) / height); + controlPoint1 = scaledArguments(args.get(i), args.get(i + 1)); } if (isCubic) { - controlPoint2 = new Vector2(args.get(i + 2) / width, args.get(i + 3) / height); + controlPoint2 = scaledArguments(args.get(i + 2), args.get(i + 3)); } - Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2) / width, - args.get(i + expectedArgs - 1) / height); + Vector2 newPoint = scaledArguments(args.get(i + expectedArgs - 2), args.get(i + expectedArgs - 1)); if (!isAbsolute) { controlPoint1 = currPoint.translate(controlPoint1); diff --git a/src/shapes/CubicBezierCurve.java b/src/shapes/CubicBezierCurve.java index a2a1f91..ae2a881 100644 --- a/src/shapes/CubicBezierCurve.java +++ b/src/shapes/CubicBezierCurve.java @@ -52,22 +52,27 @@ public class CubicBezierCurve extends Shape { } @Override - public Shape rotate(double theta) { + public CubicBezierCurve rotate(double theta) { return this; } @Override - public Shape scale(double factor) { + public CubicBezierCurve scale(double factor) { return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation); } @Override - public Shape translate(Vector2 vector) { + public CubicBezierCurve scale(Vector2 vector) { + return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), p3.scale(vector), weight, translation); + } + + @Override + public CubicBezierCurve translate(Vector2 vector) { return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation.translate(vector)); } @Override - public Shape setWeight(double weight) { + public CubicBezierCurve setWeight(double weight) { return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation); } } diff --git a/src/shapes/Ellipse.java b/src/shapes/Ellipse.java index 694c4e6..4ac2d5f 100644 --- a/src/shapes/Ellipse.java +++ b/src/shapes/Ellipse.java @@ -53,6 +53,11 @@ public final class Ellipse extends Shape { return new Ellipse(a * factor, b * factor, weight, rotation, position.scale(factor)); } + @Override + public Ellipse scale(Vector2 vector) { + return new Ellipse(a * vector.getX(), b * vector.getY(), weight, rotation, position.scale(vector)); + } + @Override public Ellipse translate(Vector2 vector) { return new Ellipse(a, b, weight, rotation, position.translate(vector)); diff --git a/src/shapes/Line.java b/src/shapes/Line.java index df0c0d6..60c760b 100644 --- a/src/shapes/Line.java +++ b/src/shapes/Line.java @@ -46,6 +46,11 @@ public final class Line extends Shape { return new Line(a.scale(factor), b.scale(factor), weight); } + @Override + public Line scale(Vector2 vector) { + return new Line(a.scale(vector), b.scale(vector), weight); + } + public Line copy() { return new Line(a.copy(), b.copy(), weight); } diff --git a/src/shapes/Shape.java b/src/shapes/Shape.java index 10b1046..04e7419 100644 --- a/src/shapes/Shape.java +++ b/src/shapes/Shape.java @@ -15,6 +15,8 @@ public abstract class Shape { public abstract Shape scale(double factor); + public abstract Shape scale(Vector2 vector); + public abstract Shape translate(Vector2 vector); public abstract Shape setWeight(double weight); diff --git a/src/shapes/Vector2.java b/src/shapes/Vector2.java index 324ce93..0106d9e 100644 --- a/src/shapes/Vector2.java +++ b/src/shapes/Vector2.java @@ -76,6 +76,11 @@ public final class Vector2 extends Shape { return new Vector2(getX() * factor, getY() * factor); } + @Override + public Vector2 scale(Vector2 vector) { + return new Vector2(getX() * vector.getX(), getY() * vector.getY()); + } + @Override public Vector2 translate(Vector2 vector) { return new Vector2(getX() + vector.getX(), getY() + vector.getY()); From 0c65eb2c670932eb187ded003da1f4b3ca718845 Mon Sep 17 00:00:00 2001 From: James Ball Date: Wed, 28 Oct 2020 18:27:09 +0000 Subject: [PATCH 18/26] Fix scaling with cubic curves and translate svg to middle of display --- src/parser/SvgParser.java | 35 +++++++++------- src/parser/XmlUtil.java | 4 ++ src/shapes/CubicBezierCurve.java | 45 +++++++------------- src/shapes/QuadraticBezierCurve.java | 63 ++++++++++++++++++++++------ 4 files changed, 90 insertions(+), 57 deletions(-) diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index 2bff2be..4446302 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -1,6 +1,7 @@ package parser; import static parser.XmlUtil.asList; +import static parser.XmlUtil.getNodeValue; import java.io.File; import java.io.IOException; @@ -9,10 +10,10 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Vector; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -30,6 +31,8 @@ public class SvgParser extends FileParser { private final List shapes; private final Map, List>> commandMap; + private float viewBoxWidth; + private float viewBoxHeight; private float width; private float height; @@ -76,7 +79,6 @@ public class SvgParser extends FileParser { throws IOException, SAXException, ParserConfigurationException { // opens XML reader for svg file. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setValidating(true); factory.setIgnoringElementContentWhitespace(true); DocumentBuilder builder = factory.newDocumentBuilder(); File file = new File(path); @@ -113,22 +115,24 @@ public class SvgParser extends FileParser { List paths = new ArrayList<>(); for (Node elem : asList(svg.getElementsByTagName("path"))) { - paths.add(elem.getAttributes().getNamedItem("d").getNodeValue()); + paths.add(getNodeValue(elem, "d")); } return paths; } // Returns the width and height of the viewBox attribute - private static List getViewBoxDimensions(Node svgElem) { - return Arrays.stream( - svgElem.getAttributes() - .getNamedItem("viewBox") - .getNodeValue() - .split(" ")) + private void getSvgDimensions(Node svgElem) { + List viewBox = Arrays.stream(getNodeValue(svgElem, "viewBox").split(" ")) .map(Float::parseFloat) .skip(2) .collect(Collectors.toList()); + + viewBoxWidth = viewBox.get(0); + viewBoxHeight = viewBox.get(1); + + width = Float.parseFloat(getNodeValue(svgElem, "width")); + height = Float.parseFloat(getNodeValue(svgElem, "height")); } private static List splitCommand(String command) { @@ -163,9 +167,7 @@ public class SvgParser extends FileParser { throw new IllegalArgumentException("SVG has either zero or more than one svg element."); } - List dimensions = getViewBoxDimensions(svgElem.get(0)); - width = dimensions.get(0); - height = dimensions.get(1); + getSvgDimensions(svgElem.get(0)); // Get all d attributes within path elements in the SVG file. for (String path : getSvgPathAttributes(svg)) { @@ -206,7 +208,11 @@ public class SvgParser extends FileParser { } private Vector2 scaledArguments(float arg1, float arg2) { - return new Vector2(arg1 / width, arg2 / height); + return new Vector2(arg1, arg2) + .scale(new Vector2((width / viewBoxWidth) / viewBoxWidth, + -(height / viewBoxHeight) / viewBoxHeight) + ).translate(new Vector2(-0.5, 0.5)) + .scale(2); } // Parses moveto commands (M and m commands) @@ -323,7 +329,8 @@ public class SvgParser extends FileParser { controlPoint2 = scaledArguments(args.get(i + 2), args.get(i + 3)); } - Vector2 newPoint = scaledArguments(args.get(i + expectedArgs - 2), args.get(i + expectedArgs - 1)); + Vector2 newPoint = scaledArguments(args.get(i + expectedArgs - 2), + args.get(i + expectedArgs - 1)); if (!isAbsolute) { controlPoint1 = currPoint.translate(controlPoint1); diff --git a/src/parser/XmlUtil.java b/src/parser/XmlUtil.java index 576ba06..86b162c 100644 --- a/src/parser/XmlUtil.java +++ b/src/parser/XmlUtil.java @@ -28,4 +28,8 @@ public final class XmlUtil { return list.getLength(); } } + + public static String getNodeValue(Node node, String namedItem) { + return node.getAttributes().getNamedItem(namedItem).getNodeValue(); + } } diff --git a/src/shapes/CubicBezierCurve.java b/src/shapes/CubicBezierCurve.java index ae2a881..a712aac 100644 --- a/src/shapes/CubicBezierCurve.java +++ b/src/shapes/CubicBezierCurve.java @@ -6,49 +6,34 @@ public class CubicBezierCurve extends Shape { private final Vector2 p1; private final Vector2 p2; private final Vector2 p3; - private final double factor; - private final Vector2 translation; - public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight, - double factor, Vector2 translation) { + public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight) { this.p0 = p0; this.p1 = p1; this.p2 = p2; this.p3 = p3; this.weight = weight; - this.factor = factor; - this.translation = translation; - Line temp = new Line(p0, p3); - this.length = temp.length; - } - - public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double factor, - Vector2 translation) { - this(p0, p1, p2, p3, DEFAULT_WEIGHT, factor, translation); - } - - public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double factor) { - this(p0, p1, p2, p3, DEFAULT_WEIGHT, factor, new Vector2()); + this.length = new Line(p0, p3).length; } public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) { - this(p0, p1, p2, p3, DEFAULT_WEIGHT, 1, new Vector2()); + this(p0, p1, p2, p3, DEFAULT_WEIGHT); } @Override public float nextX(double t) { - return (float) (Math.pow(1 - t, 3) * factor * p0.getX() - + 3 * Math.pow(1 - t, 2) * t * factor * p1.getX() - + 3 * (1 - t) * Math.pow(t, 2) * factor * p2.getX() - + Math.pow(t, 3) * factor * p3.getX()); + return (float) (Math.pow(1 - t, 3) * p0.getX() + + 3 * Math.pow(1 - t, 2) * t * p1.getX() + + 3 * (1 - t) * Math.pow(t, 2) * p2.getX() + + Math.pow(t, 3) * p3.getX()); } @Override public float nextY(double t) { - return (float) (Math.pow(1 - t, 3) * factor * p0.getY() - + 3 * Math.pow(1 - t, 2) * t * factor * p1.getY() - + 3 * (1 - t) * Math.pow(t, 2) * factor * p2.getY() - + Math.pow(t, 3) * factor * p3.getY()); + return (float) (Math.pow(1 - t, 3) * p0.getY() + + 3 * Math.pow(1 - t, 2) * t * p1.getY() + + 3 * (1 - t) * Math.pow(t, 2) * p2.getY() + + Math.pow(t, 3) * p3.getY()); } @Override @@ -58,21 +43,21 @@ public class CubicBezierCurve extends Shape { @Override public CubicBezierCurve scale(double factor) { - return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation); + return new CubicBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor), p3.scale(factor), weight); } @Override public CubicBezierCurve scale(Vector2 vector) { - return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), p3.scale(vector), weight, translation); + return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), p3.scale(vector), weight); } @Override public CubicBezierCurve translate(Vector2 vector) { - return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation.translate(vector)); + return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector), p3.translate(vector), weight); } @Override public CubicBezierCurve setWeight(double weight) { - return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation); + return new CubicBezierCurve(p0, p1, p2, p3, weight); } } diff --git a/src/shapes/QuadraticBezierCurve.java b/src/shapes/QuadraticBezierCurve.java index 4839e37..a41f883 100644 --- a/src/shapes/QuadraticBezierCurve.java +++ b/src/shapes/QuadraticBezierCurve.java @@ -1,22 +1,59 @@ package shapes; -public class QuadraticBezierCurve extends CubicBezierCurve { +public class QuadraticBezierCurve extends Shape { - public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight, - double factor, Vector2 translation) { - super(p0, p1, p1, p2, weight, factor, translation); - } + private final Vector2 p0; + private final Vector2 p1; + private final Vector2 p2; - public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double factor, - Vector2 translation) { - super(p0, p1, p1, p2, factor, translation); - } - - public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double factor) { - super(p0, p1, p1, p2, factor); + public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight) { + this.p0 = p0; + this.p1 = p1; + this.p2 = p2; + this.weight = weight; + this.length = new Line(p0, p2).length; } public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) { - super(p0, p1, p1, p2); + this(p0, p1, p2, DEFAULT_WEIGHT); + } + + @Override + public float nextX(double t) { + return (float) nextVector(t).getX(); + } + + @Override + public float nextY(double t) { + return (float) nextVector(t).getY(); + } + + private Vector2 nextVector(double t) { + return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2))).add(p2.sub(p1).scale(Math.pow(t, 2))); + } + + @Override + public QuadraticBezierCurve rotate(double theta) { + return this; + } + + @Override + public QuadraticBezierCurve scale(double factor) { + return new QuadraticBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor), weight); + } + + @Override + public QuadraticBezierCurve scale(Vector2 vector) { + return new QuadraticBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), weight); + } + + @Override + public QuadraticBezierCurve translate(Vector2 vector) { + return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector), weight); + } + + @Override + public QuadraticBezierCurve setWeight(double weight) { + return new QuadraticBezierCurve(p0, p1, p2, weight); } } From defc6173b1adeb39aeff0ba5d4e5d503014391a9 Mon Sep 17 00:00:00 2001 From: James Ball Date: Thu, 29 Oct 2020 11:00:12 +0000 Subject: [PATCH 19/26] Fix test svg width and height --- test/SvgParserTest.java | 1 - test/images/closing-subpath.svg | 2 +- test/images/horizontal-line-to.svg | 2 +- test/images/line-to.svg | 2 +- test/images/vertical-line-to.svg | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/test/SvgParserTest.java b/test/SvgParserTest.java index e75ecce..8b58889 100644 --- a/test/SvgParserTest.java +++ b/test/SvgParserTest.java @@ -8,7 +8,6 @@ import org.xml.sax.SAXException; import parser.SvgParser; import shapes.Line; import shapes.Shape; -import shapes.Vector2; public class SvgParserTest { diff --git a/test/images/closing-subpath.svg b/test/images/closing-subpath.svg index ca601be..e390a20 100644 --- a/test/images/closing-subpath.svg +++ b/test/images/closing-subpath.svg @@ -1,4 +1,4 @@ - Date: Mon, 2 Nov 2020 21:18:13 +0000 Subject: [PATCH 22/26] Clean shape code --- src/shapes/CubicBezierCurve.java | 11 +++++++---- src/shapes/Ellipse.java | 2 +- src/shapes/QuadraticBezierCurve.java | 8 +++++--- src/shapes/Vector2.java | 6 +++++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/shapes/CubicBezierCurve.java b/src/shapes/CubicBezierCurve.java index bcac45b..cd6bc63 100644 --- a/src/shapes/CubicBezierCurve.java +++ b/src/shapes/CubicBezierCurve.java @@ -30,22 +30,25 @@ public class CubicBezierCurve extends Shape { @Override public CubicBezierCurve rotate(double theta) { - return this; + return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), + p3.rotate(theta), weight); } @Override public CubicBezierCurve scale(double factor) { - return new CubicBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor), p3.scale(factor), weight); + return scale(new Vector2(factor)); } @Override public CubicBezierCurve scale(Vector2 vector) { - return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), p3.scale(vector), weight); + return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), + p3.scale(vector), weight); } @Override public CubicBezierCurve translate(Vector2 vector) { - return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector), p3.translate(vector), weight); + return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector), + p3.translate(vector), weight); } @Override diff --git a/src/shapes/Ellipse.java b/src/shapes/Ellipse.java index 05675d7..4ce3a53 100644 --- a/src/shapes/Ellipse.java +++ b/src/shapes/Ellipse.java @@ -45,7 +45,7 @@ public final class Ellipse extends Shape { @Override public Ellipse scale(double factor) { - return new Ellipse(a * factor, b * factor, weight, rotation, position.scale(factor)); + return scale(new Vector2(factor)); } @Override diff --git a/src/shapes/QuadraticBezierCurve.java b/src/shapes/QuadraticBezierCurve.java index 6cf0891..b3edd5b 100644 --- a/src/shapes/QuadraticBezierCurve.java +++ b/src/shapes/QuadraticBezierCurve.java @@ -20,12 +20,13 @@ public class QuadraticBezierCurve extends Shape { @Override public Vector2 nextVector(double t) { - return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2))).add(p2.sub(p1).scale(Math.pow(t, 2))); + return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2))) + .add(p2.sub(p1).scale(Math.pow(t, 2))); } @Override public QuadraticBezierCurve rotate(double theta) { - return this; + return new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), weight); } @Override @@ -40,7 +41,8 @@ public class QuadraticBezierCurve extends Shape { @Override public QuadraticBezierCurve translate(Vector2 vector) { - return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector), weight); + return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector), + p2.translate(vector), weight); } @Override diff --git a/src/shapes/Vector2.java b/src/shapes/Vector2.java index ef40bea..6b1c598 100644 --- a/src/shapes/Vector2.java +++ b/src/shapes/Vector2.java @@ -17,6 +17,10 @@ public final class Vector2 extends Shape { this(x, y, Shape.DEFAULT_WEIGHT); } + public Vector2(double xy) { + this(xy, xy, Shape.DEFAULT_WEIGHT); + } + public Vector2() { this(0, 0); } @@ -68,7 +72,7 @@ public final class Vector2 extends Shape { @Override public Vector2 scale(double factor) { - return new Vector2(getX() * factor, getY() * factor); + return scale(new Vector2(factor)); } @Override From 1081562cfb22172128582c6ab78bbaf5029c3894 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 2 Nov 2020 22:03:58 +0000 Subject: [PATCH 23/26] Reformat and simplify code --- src/audio/AudioClient.java | 3 +- src/engine/WorldObject.java | 69 ++++++++++++++++++------------------- src/parser/FileParser.java | 8 ++--- src/parser/SvgParser.java | 23 +++++++------ src/parser/XmlUtil.java | 14 +++++--- 5 files changed, 60 insertions(+), 57 deletions(-) diff --git a/src/audio/AudioClient.java b/src/audio/AudioClient.java index 7eabc60..53a8d87 100644 --- a/src/audio/AudioClient.java +++ b/src/audio/AudioClient.java @@ -3,6 +3,7 @@ package audio; import engine.Camera; import engine.Vector3; import engine.WorldObject; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -31,7 +32,7 @@ public class AudioClient { // // example: // osci-render models/cube.obj 3 - public static void main(String[] programArgs) { + public static void main(String[] programArgs) throws IOException { // TODO: Calculate weight of lines using depth. // Reduce weight of lines drawn multiple times. // Find intersections of lines to (possibly) improve line cleanup. diff --git a/src/engine/WorldObject.java b/src/engine/WorldObject.java index 368f4de..30e8412 100644 --- a/src/engine/WorldObject.java +++ b/src/engine/WorldObject.java @@ -3,6 +3,7 @@ package engine; import com.mokiat.data.front.parser.*; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -15,24 +16,6 @@ public class WorldObject { private Vector3 position; private Vector3 rotation; - public WorldObject(String filename) { - this.vertices = new ArrayList<>(); - this.edgeData = new ArrayList<>(); - this.position = new Vector3(); - this.rotation = new Vector3(); - - loadFromFile(filename); - } - - public WorldObject(String filename, Vector3 position, Vector3 rotation) { - this.vertices = new ArrayList<>(); - this.edgeData = new ArrayList<>(); - this.position = position; - this.rotation = rotation; - - loadFromFile(filename); - } - public WorldObject(List vertices, List edgeData, Vector3 position, Vector3 rotation) { this.vertices = vertices; @@ -41,6 +24,16 @@ public class WorldObject { this.rotation = rotation; } + public WorldObject(String filename, Vector3 position, Vector3 rotation) throws IOException { + this(new ArrayList<>(), new ArrayList<>(), position, rotation); + loadFromFile(filename); + } + + public WorldObject(String filename) throws IOException { + this(filename, new Vector3(), new Vector3()); + } + + public void rotate(Vector3 theta) { rotation = rotation.add(theta); } @@ -49,6 +42,14 @@ public class WorldObject { rotation = new Vector3(); } + public void move(Vector3 translation) { + position = position.add(translation); + } + + public void resetPosition() { + position = new Vector3(); + } + public List getVertices() { List newVertices = new ArrayList<>(); @@ -63,30 +64,26 @@ public class WorldObject { return edgeData; } - private void loadFromFile(String filename) { - try (InputStream in = new FileInputStream(filename)) { - final IOBJParser parser = new OBJParser(); - final OBJModel model = parser.parse(in); + private void loadFromFile(String filename) throws IOException { + InputStream in = new FileInputStream(filename); + final IOBJParser parser = new OBJParser(); + final OBJModel model = parser.parse(in); - for (OBJVertex vertex : model.getVertices()) { - vertices.add(new Vector3(vertex.x, vertex.y, vertex.z)); - } + for (OBJVertex vertex : model.getVertices()) { + vertices.add(new Vector3(vertex.x, vertex.y, vertex.z)); + } - for (OBJObject object : model.getObjects()) { - for (OBJMesh mesh : object.getMeshes()) { - for (OBJFace face : mesh.getFaces()) { - List references = face.getReferences(); + for (OBJObject object : model.getObjects()) { + for (OBJMesh mesh : object.getMeshes()) { + for (OBJFace face : mesh.getFaces()) { + List references = face.getReferences(); - for (int i = 0; i < references.size(); i++) { - edgeData.add(references.get(i).vertexIndex); - edgeData.add(references.get((i + 1) % references.size()).vertexIndex); - } + for (int i = 0; i < references.size(); i++) { + edgeData.add(references.get(i).vertexIndex); + edgeData.add(references.get((i + 1) % references.size()).vertexIndex); } } } - } catch (IOException e) { - e.printStackTrace(); - throw new IllegalArgumentException("Cannot load mesh data from: " + filename); } } diff --git a/src/parser/FileParser.java b/src/parser/FileParser.java index 66e8092..273426a 100644 --- a/src/parser/FileParser.java +++ b/src/parser/FileParser.java @@ -9,13 +9,9 @@ import shapes.Shape; public abstract class FileParser { - public static String fileExtension; + public abstract String getFileExtension(); - public static String getFileExtension() { - return fileExtension; - } - - protected static void checkFileExtension(String path) throws IllegalArgumentException { + protected void checkFileExtension(String path) throws IllegalArgumentException { Pattern pattern = Pattern.compile("\\." + getFileExtension() + "$"); if (!pattern.matcher(path).find()) { throw new IllegalArgumentException( diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index dcc8e48..dfa4af6 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -41,16 +41,16 @@ public class SvgParser extends FileParser { private Vector2 prevCubicControlPoint; private Vector2 prevQuadraticControlPoint; - static { - fileExtension = "svg"; + public SvgParser(String path) throws IOException, SAXException, ParserConfigurationException { + checkFileExtension(path); + shapes = new ArrayList<>(); + commandMap = new HashMap<>(); + initialiseCommandMap(); + parseFile(path); } - public SvgParser(String path) throws IOException, SAXException, ParserConfigurationException { - FileParser.checkFileExtension(path); - shapes = new ArrayList<>(); - - commandMap = new HashMap<>(); - // Map command chars to function calls. + // Map command chars to function calls. + private void initialiseCommandMap() { commandMap.put('M', (args) -> parseMoveTo(args, true)); commandMap.put('m', (args) -> parseMoveTo(args, false)); commandMap.put('L', (args) -> parseLineTo(args, true, true, true)); @@ -71,8 +71,6 @@ public class SvgParser extends FileParser { commandMap.put('a', (args) -> parseEllipticalArc(args, false)); commandMap.put('Z', this::parseClosePath); commandMap.put('z', this::parseClosePath); - - parseFile(path); } private Document getSvgDocument(String path) @@ -157,6 +155,11 @@ public class SvgParser extends FileParser { return nums; } + @Override + public String getFileExtension() { + return "svg"; + } + @Override protected void parseFile(String filePath) throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException { diff --git a/src/parser/XmlUtil.java b/src/parser/XmlUtil.java index 86b162c..d051b6f 100644 --- a/src/parser/XmlUtil.java +++ b/src/parser/XmlUtil.java @@ -8,22 +8,28 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; public final class XmlUtil { - private XmlUtil(){} + + private XmlUtil() { + } public static List asList(NodeList n) { - return n.getLength()==0? - Collections.emptyList(): new NodeListWrapper(n); + return n.getLength() == 0 ? + Collections.emptyList() : new NodeListWrapper(n); } static final class NodeListWrapper extends AbstractList implements RandomAccess { + private final NodeList list; + NodeListWrapper(NodeList l) { - list=l; + list = l; } + public Node get(int index) { return list.item(index); } + public int size() { return list.getLength(); } From cd2d1925b18d82f62a58fe77a6f29ee392345471 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 2 Nov 2020 22:11:23 +0000 Subject: [PATCH 24/26] Simplify constructors and improve constants --- src/engine/Camera.java | 7 ++++--- src/engine/Vector3.java | 14 ++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/engine/Camera.java b/src/engine/Camera.java index db29eb6..c5f5279 100644 --- a/src/engine/Camera.java +++ b/src/engine/Camera.java @@ -15,9 +15,10 @@ public class Camera { private static final double VERTEX_VALUE_THRESHOLD = 1; private static final double CAMERA_MOVE_INCREMENT = -0.1; private static final int SAMPLE_RENDER_SAMPLES = 50; + private static final double EPSILON = 0.001; + + private final double focalLength; - private double focalLength; - private double clipping = 0.001; private Vector3 pos; public Camera(double focalLength, Vector3 pos) { @@ -100,7 +101,7 @@ public class Camera { } private Vector2 project(Vector3 vertex) { - if (vertex.getZ() - pos.getZ() < clipping) { + if (vertex.getZ() - pos.getZ() < EPSILON) { return new Vector2(); } diff --git a/src/engine/Vector3.java b/src/engine/Vector3.java index 6a449bb..a71fe8c 100644 --- a/src/engine/Vector3.java +++ b/src/engine/Vector3.java @@ -6,18 +6,20 @@ public final class Vector3 { private final double x, y, z; - public Vector3() { - this.x = 0; - this.y = 0; - this.z = 0; - } - public Vector3(double x, double y, double z) { this.x = x; this.y = y; this.z = z; } + public Vector3(double xyz) { + this(xyz, xyz, xyz); + } + + public Vector3() { + this(0, 0, 0); + } + public double getX() { return x; } From ef63e47b94cbeb9fa0dc17382af7b19d9db27b9b Mon Sep 17 00:00:00 2001 From: James Ball Date: Thu, 5 Nov 2020 21:03:48 +0000 Subject: [PATCH 25/26] Add shape normalising function for correct rendering on oscilloscope --- src/audio/AudioClient.java | 11 +++++++++-- src/parser/SvgParser.java | 21 ++++++--------------- src/shapes/Shapes.java | 27 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/audio/AudioClient.java b/src/audio/AudioClient.java index 53a8d87..608805f 100644 --- a/src/audio/AudioClient.java +++ b/src/audio/AudioClient.java @@ -8,6 +8,9 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; +import javax.xml.parsers.ParserConfigurationException; +import org.xml.sax.SAXException; +import parser.SvgParser; import shapes.Shape; import shapes.Shapes; import shapes.Vector2; @@ -32,7 +35,8 @@ public class AudioClient { // // example: // osci-render models/cube.obj 3 - public static void main(String[] programArgs) throws IOException { + public static void main(String[] programArgs) + throws IOException, ParserConfigurationException, SAXException { // TODO: Calculate weight of lines using depth. // Reduce weight of lines drawn multiple times. // Find intersections of lines to (possibly) improve line cleanup. @@ -53,7 +57,10 @@ public class AudioClient { Vector3 rotation = new Vector3(0, OBJ_ROTATE_SPEED, OBJ_ROTATE_SPEED); System.out.println("Begin pre-render..."); - List> frames = preRender(object, rotation, camera); + //List> frames = preRender(object, rotation, camera); + List> frames = new ArrayList<>(); + List frame = Shapes.normalizeShapes(new SvgParser("test/images/sine-wave.svg").getShapes()); + frames.add(frame); System.out.println("Finish pre-render"); System.out.println("Connecting to audio player"); AudioPlayer player = new AudioPlayer(SAMPLE_RATE, frames, ROTATE_SPEED, TRANSLATION_SPEED, diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index dfa4af6..d8332bc 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -210,22 +210,13 @@ public class SvgParser extends FileParser { return shapes; } - private Vector2 scaledArguments(float arg1, float arg2) { - return new Vector2(arg1, arg2); -// return new Vector2(arg1, arg2) -// .scale(new Vector2((width / viewBoxWidth) / viewBoxWidth, -// -(height / viewBoxHeight) / viewBoxHeight) -// ).translate(new Vector2(-0.5, 0.5)) -// .scale(2); - } - // Parses moveto commands (M and m commands) private List parseMoveTo(List args, boolean isAbsolute) { if (args.size() % 2 != 0 || args.size() < 2) { throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments."); } - Vector2 vec = scaledArguments(args.get(0), args.get(1)); + Vector2 vec = new Vector2(args.get(0), args.get(1)); if (isAbsolute) { currPoint = vec; @@ -273,9 +264,9 @@ public class SvgParser extends FileParser { Vector2 newPoint; if (expectedArgs == 1) { - newPoint = scaledArguments(args.get(i), args.get(i)); + newPoint = new Vector2(args.get(i), args.get(i)); } else { - newPoint = scaledArguments(args.get(i), args.get(i + 1)); + newPoint = new Vector2(args.get(i), args.get(i + 1)); } if (isHorizontal && !isVertical) { @@ -326,14 +317,14 @@ public class SvgParser extends FileParser { : prevQuadraticControlPoint.reflectRelativeToVector(currPoint); } } else { - controlPoint1 = scaledArguments(args.get(i), args.get(i + 1)); + controlPoint1 = new Vector2(args.get(i), args.get(i + 1)); } if (isCubic) { - controlPoint2 = scaledArguments(args.get(i + 2), args.get(i + 3)); + controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3)); } - Vector2 newPoint = scaledArguments(args.get(i + expectedArgs - 2), + Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2), args.get(i + expectedArgs - 1)); if (!isAbsolute) { diff --git a/src/shapes/Shapes.java b/src/shapes/Shapes.java index 1232612..bfdf141 100644 --- a/src/shapes/Shapes.java +++ b/src/shapes/Shapes.java @@ -12,8 +12,35 @@ import org.jgrapht.graph.AsSubgraph; import org.jgrapht.graph.DefaultUndirectedWeightedGraph; import org.jgrapht.graph.DefaultWeightedEdge; +// Helper functions for the Shape interface. public class Shapes { + // Normalises shapes between the coords -1 and 1 for proper scaling on an oscilloscope. May not + // work perfectly with curves that heavily deviate from their start and end points. + public static List normalizeShapes(List shapes) { + double maxVertex = 0; + + for (Shape shape : shapes) { + Vector2 startVector = shape.nextVector(0); + Vector2 endVector = shape.nextVector(1); + + double maxX = Math.max(Math.abs(startVector.getX()), Math.abs(endVector.getX())); + double maxY = Math.max(Math.abs(startVector.getY()), Math.abs(endVector.getY())); + + maxVertex = Math.max(Math.max(maxX, maxY), maxVertex); + } + + double factor = 2 / maxVertex; + + for (int i = 0; i < shapes.size(); i++) { + shapes.set(i, shapes.get(i) + .scale(new Vector2(factor, -factor)) + .translate(new Vector2(-1, 1))); + } + + return shapes; + } + public static List generatePolygram(int sides, int angleJump, Vector2 start, double weight) { List polygon = new ArrayList<>(); From d80a7aadf8199841e09d3c41658d3baff1a59e12 Mon Sep 17 00:00:00 2001 From: James Ball Date: Fri, 6 Nov 2020 21:35:00 +0000 Subject: [PATCH 26/26] Complete initial working implementation of SvgParser with argument passing --- {test/images => images}/sine-wave.svg | 0 src/audio/AudioArgs.java | 25 +++++-- src/audio/AudioClient.java | 57 +--------------- src/audio/AudioPlayer.java | 6 +- src/parser/FileParser.java | 12 ++-- src/parser/ObjParser.java | 95 +++++++++++++++++++++++++++ src/parser/SvgParser.java | 30 ++------- src/shapes/Shapes.java | 28 ++++---- test/SvgParserTest.java | 16 ++--- 9 files changed, 157 insertions(+), 112 deletions(-) rename {test/images => images}/sine-wave.svg (100%) create mode 100644 src/parser/ObjParser.java diff --git a/test/images/sine-wave.svg b/images/sine-wave.svg similarity index 100% rename from test/images/sine-wave.svg rename to images/sine-wave.svg diff --git a/src/audio/AudioArgs.java b/src/audio/AudioArgs.java index 84901b8..301ae82 100644 --- a/src/audio/AudioArgs.java +++ b/src/audio/AudioArgs.java @@ -1,11 +1,20 @@ package audio; import engine.Camera; +import java.io.IOException; +import java.util.List; +import javax.xml.parsers.ParserConfigurationException; +import org.xml.sax.SAXException; +import parser.FileParser; +import parser.ObjParser; +import parser.SvgParser; +import shapes.Shape; +import shapes.Shapes; // Helper class for AudioClient that deals with optional program arguments. final class AudioArgs { - final String objFilePath; + final String filePath; final float[] optionalArgs; AudioArgs(String[] args) throws IllegalAudioArgumentException { @@ -13,7 +22,7 @@ final class AudioArgs { throw new IllegalAudioArgumentException(); } - objFilePath = args[0]; + filePath = args[0]; optionalArgs = new float[args.length - 1]; for (int i = 0; i < optionalArgs.length; i++) { @@ -21,8 +30,16 @@ final class AudioArgs { } } - String objFilePath() { - return objFilePath; + List> getFramesFromFile() throws IOException, ParserConfigurationException, SAXException { + if (filePath.matches(".*\\.obj")) { + return new ObjParser(filePath, rotateSpeed(), cameraX(), cameraY(), cameraZ(), focalLength(), + isDefaultPosition()).getShapes(); + } else if (filePath.matches(".*\\.svg")) { + return Shapes.normalize(new SvgParser(filePath).getShapes()); + } else { + throw new IllegalArgumentException( + "Provided file extension in file " + filePath + " not supported."); + } } float rotateSpeed() { diff --git a/src/audio/AudioClient.java b/src/audio/AudioClient.java index 608805f..7aa3be1 100644 --- a/src/audio/AudioClient.java +++ b/src/audio/AudioClient.java @@ -1,24 +1,15 @@ package audio; -import engine.Camera; -import engine.Vector3; -import engine.WorldObject; import java.io.IOException; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.IntStream; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; -import parser.SvgParser; import shapes.Shape; -import shapes.Shapes; import shapes.Vector2; public class AudioClient { private static final int SAMPLE_RATE = 192000; - private static double OBJ_ROTATE_SPEED = Math.PI / 1000; private static final float ROTATE_SPEED = 0; private static final float TRANSLATION_SPEED = 0; private static final Vector2 TRANSLATION = new Vector2(0.3, 0.3); @@ -26,7 +17,7 @@ public class AudioClient { private static final float WEIGHT = Shape.DEFAULT_WEIGHT; // args: - // args[0] - path of .obj file + // args[0] - path of .obj or .svg file // args[1] - rotation speed of object // args[2] - focal length of camera // args[3] - x position of camera @@ -44,23 +35,8 @@ public class AudioClient { AudioArgs args = new AudioArgs(programArgs); - OBJ_ROTATE_SPEED *= args.rotateSpeed(); - - Vector3 cameraPos = new Vector3(args.cameraX(), args.cameraY(), args.cameraZ()); - WorldObject object = new WorldObject(args.objFilePath()); - - // If camera position arguments haven't been specified, automatically work out the position of - // the camera based on the size of the object in the camera's view. - Camera camera = args.isDefaultPosition() ? new Camera(args.focalLength(), object) - : new Camera(args.focalLength(), cameraPos); - - Vector3 rotation = new Vector3(0, OBJ_ROTATE_SPEED, OBJ_ROTATE_SPEED); - System.out.println("Begin pre-render..."); - //List> frames = preRender(object, rotation, camera); - List> frames = new ArrayList<>(); - List frame = Shapes.normalizeShapes(new SvgParser("test/images/sine-wave.svg").getShapes()); - frames.add(frame); + List> frames = args.getFramesFromFile(); System.out.println("Finish pre-render"); System.out.println("Connecting to audio player"); AudioPlayer player = new AudioPlayer(SAMPLE_RATE, frames, ROTATE_SPEED, TRANSLATION_SPEED, @@ -68,33 +44,4 @@ public class AudioClient { System.out.println("Starting audio stream"); player.play(); } - - private static List> preRender(WorldObject object, Vector3 rotation, - Camera camera) { - List> preRenderedFrames = new ArrayList<>(); - // Number of frames it will take to render a full rotation of the object. - int numFrames = (int) (2 * Math.PI / OBJ_ROTATE_SPEED); - - for (int i = 0; i < numFrames; i++) { - preRenderedFrames.add(new ArrayList<>()); - } - - AtomicInteger renderedFrames = new AtomicInteger(); - - // pre-renders the WorldObject in parallel - IntStream.range(0, numFrames).parallel().forEach((frameNum) -> { - WorldObject clone = object.clone(); - clone.rotate(rotation.scale(frameNum)); - // Finds all lines to draw the object from the camera's view and then 'sorts' them by finding - // a hamiltonian path, which dramatically helps with rendering a clean image. - preRenderedFrames.set(frameNum, Shapes.sortLines(camera.draw(clone))); - int numRendered = renderedFrames.getAndIncrement(); - - if (numRendered % 50 == 0) { - System.out.println("Rendered " + numRendered + " frames of " + (numFrames + 1) + " total"); - } - }); - - return preRenderedFrames; - } } \ No newline at end of file diff --git a/src/audio/AudioPlayer.java b/src/audio/AudioPlayer.java index f42abee..b5a1ed4 100644 --- a/src/audio/AudioPlayer.java +++ b/src/audio/AudioPlayer.java @@ -18,7 +18,7 @@ public class AudioPlayer { private final XtFormat FORMAT; - private final List> frames; + private final List> frames; private int currentFrame = 0; private int currentShape = 0; private int audioFramesDrawn = 0; @@ -33,12 +33,12 @@ public class AudioPlayer { private volatile boolean stopped; - public AudioPlayer(int sampleRate, List> frames) { + public AudioPlayer(int sampleRate, List> frames) { this.FORMAT = new XtFormat(new XtMix(sampleRate, XtSample.FLOAT32), 0, 0, 2, 0); this.frames = frames; } - public AudioPlayer(int sampleRate, List> frames, float rotateSpeed, + public AudioPlayer(int sampleRate, List> frames, float rotateSpeed, float translateSpeed, Vector2 translateVector, float scale, float weight) { this(sampleRate, frames); setRotateSpeed(rotateSpeed); diff --git a/src/parser/FileParser.java b/src/parser/FileParser.java index 273426a..274b878 100644 --- a/src/parser/FileParser.java +++ b/src/parser/FileParser.java @@ -2,25 +2,27 @@ package parser; import java.io.IOException; import java.util.List; -import java.util.regex.Pattern; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; import shapes.Shape; public abstract class FileParser { - public abstract String getFileExtension(); + protected abstract String getFileExtension(); protected void checkFileExtension(String path) throws IllegalArgumentException { - Pattern pattern = Pattern.compile("\\." + getFileExtension() + "$"); - if (!pattern.matcher(path).find()) { + if (!hasCorrectFileExtension(path)) { throw new IllegalArgumentException( "File to parse is not a ." + getFileExtension() + " file."); } } + public boolean hasCorrectFileExtension(String path) { + return path.matches(".*\\." + getFileExtension()); + } + protected abstract void parseFile(String path) throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException; - public abstract List getShapes(); + public abstract List> getShapes(); } diff --git a/src/parser/ObjParser.java b/src/parser/ObjParser.java new file mode 100644 index 0000000..3973a0f --- /dev/null +++ b/src/parser/ObjParser.java @@ -0,0 +1,95 @@ +package parser; + +import engine.Camera; +import engine.Vector3; +import engine.WorldObject; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; +import shapes.Shape; +import shapes.Shapes; + +public class ObjParser extends FileParser { + + private static double OBJ_ROTATE_SPEED = Math.PI / 1000; + + private List> shapes; + + private final float rotateSpeed; + private final float cameraX; + private final float cameraY; + private final float cameraZ; + private final float focalLength; + private final boolean isDefaultPosition; + + public ObjParser(String path, float rotateSpeed, float cameraX, float cameraY, float cameraZ, + float focalLength, boolean isDefaultPosition) throws IOException { + checkFileExtension(path); + shapes = new ArrayList<>(); + this.rotateSpeed = rotateSpeed; + this.cameraX = cameraX; + this.cameraY = cameraY; + this.cameraZ = cameraZ; + this.focalLength = focalLength; + this.isDefaultPosition = isDefaultPosition; + parseFile(path); + } + + @Override + protected String getFileExtension() { + return "obj"; + } + + @Override + protected void parseFile(String path) throws IllegalArgumentException, IOException { + OBJ_ROTATE_SPEED *= rotateSpeed; + + Vector3 cameraPos = new Vector3(cameraX, cameraY, cameraZ); + WorldObject object = new WorldObject(path); + + // If camera position arguments haven't been specified, automatically work out the position of + // the camera based on the size of the object in the camera's view. + Camera camera = isDefaultPosition ? new Camera(focalLength, object) + : new Camera(focalLength, cameraPos); + + Vector3 rotation = new Vector3(0, OBJ_ROTATE_SPEED, OBJ_ROTATE_SPEED); + + shapes = preRender(object, rotation, camera); + } + + @Override + public List> getShapes() { + return shapes; + } + + private static List> preRender(WorldObject object, Vector3 rotation, + Camera camera) { + List> preRenderedFrames = new ArrayList<>(); + // Number of frames it will take to render a full rotation of the object. + int numFrames = (int) (2 * Math.PI / OBJ_ROTATE_SPEED); + + for (int i = 0; i < numFrames; i++) { + preRenderedFrames.add(new ArrayList<>()); + } + + AtomicInteger renderedFrames = new AtomicInteger(); + + // pre-renders the WorldObject in parallel + IntStream.range(0, numFrames).parallel().forEach((frameNum) -> { + WorldObject clone = object.clone(); + clone.rotate(rotation.scale(frameNum)); + // Finds all lines to draw the object from the camera's view and then 'sorts' them by finding + // a hamiltonian path, which dramatically helps with rendering a clean image. + preRenderedFrames.set(frameNum, Shapes.sortLines(camera.draw(clone))); + int numRendered = renderedFrames.getAndIncrement(); + + if (numRendered % 50 == 0) { + System.out.println("Rendered " + numRendered + " frames of " + (numFrames + 1) + " total"); + } + }); + + return preRenderedFrames; + } +} diff --git a/src/parser/SvgParser.java b/src/parser/SvgParser.java index d8332bc..c91bb84 100644 --- a/src/parser/SvgParser.java +++ b/src/parser/SvgParser.java @@ -13,7 +13,6 @@ import java.util.Map; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -31,11 +30,6 @@ public class SvgParser extends FileParser { private final List shapes; private final Map, List>> commandMap; - private float viewBoxWidth; - private float viewBoxHeight; - private float width; - private float height; - private Vector2 currPoint; private Vector2 initialPoint; private Vector2 prevCubicControlPoint; @@ -119,20 +113,6 @@ public class SvgParser extends FileParser { return paths; } - // Returns the width and height of the viewBox attribute - private void getSvgDimensions(Node svgElem) { - List viewBox = Arrays.stream(getNodeValue(svgElem, "viewBox").split(" ")) - .map(Float::parseFloat) - .skip(2) - .collect(Collectors.toList()); - - viewBoxWidth = viewBox.get(0); - viewBoxHeight = viewBox.get(1); - - width = Float.parseFloat(getNodeValue(svgElem, "width")); - height = Float.parseFloat(getNodeValue(svgElem, "height")); - } - private static List splitCommand(String command) { List nums = new ArrayList<>(); String[] decimalSplit = command.split("\\."); @@ -156,7 +136,7 @@ public class SvgParser extends FileParser { } @Override - public String getFileExtension() { + protected String getFileExtension() { return "svg"; } @@ -170,8 +150,6 @@ public class SvgParser extends FileParser { throw new IllegalArgumentException("SVG has either zero or more than one svg element."); } - getSvgDimensions(svgElem.get(0)); - // Get all d attributes within path elements in the SVG file. for (String path : getSvgPathAttributes(svg)) { currPoint = new Vector2(); @@ -206,8 +184,10 @@ public class SvgParser extends FileParser { } @Override - public List getShapes() { - return shapes; + public List> getShapes() { + List> frames = new ArrayList<>(); + frames.add(shapes); + return frames; } // Parses moveto commands (M and m commands) diff --git a/src/shapes/Shapes.java b/src/shapes/Shapes.java index bfdf141..be53469 100644 --- a/src/shapes/Shapes.java +++ b/src/shapes/Shapes.java @@ -17,28 +17,32 @@ public class Shapes { // Normalises shapes between the coords -1 and 1 for proper scaling on an oscilloscope. May not // work perfectly with curves that heavily deviate from their start and end points. - public static List normalizeShapes(List shapes) { + public static List> normalize(List> shapeLists) { double maxVertex = 0; - for (Shape shape : shapes) { - Vector2 startVector = shape.nextVector(0); - Vector2 endVector = shape.nextVector(1); + for (List shapes : shapeLists) { + for (Shape shape : shapes) { + Vector2 startVector = shape.nextVector(0); + Vector2 endVector = shape.nextVector(1); - double maxX = Math.max(Math.abs(startVector.getX()), Math.abs(endVector.getX())); - double maxY = Math.max(Math.abs(startVector.getY()), Math.abs(endVector.getY())); + double maxX = Math.max(Math.abs(startVector.getX()), Math.abs(endVector.getX())); + double maxY = Math.max(Math.abs(startVector.getY()), Math.abs(endVector.getY())); - maxVertex = Math.max(Math.max(maxX, maxY), maxVertex); + maxVertex = Math.max(Math.max(maxX, maxY), maxVertex); + } } double factor = 2 / maxVertex; - for (int i = 0; i < shapes.size(); i++) { - shapes.set(i, shapes.get(i) - .scale(new Vector2(factor, -factor)) - .translate(new Vector2(-1, 1))); + for (List shapes : shapeLists) { + for (int i = 0; i < shapes.size(); i++) { + shapes.set(i, shapes.get(i) + .scale(new Vector2(factor, -factor)) + .translate(new Vector2(-1, 1))); + } } - return shapes; + return shapeLists; } public static List generatePolygram(int sides, int angleJump, Vector2 start, diff --git a/test/SvgParserTest.java b/test/SvgParserTest.java index 8b58889..e7e10e0 100644 --- a/test/SvgParserTest.java +++ b/test/SvgParserTest.java @@ -11,35 +11,35 @@ import shapes.Shape; public class SvgParserTest { + private List getShapes(SvgParser parser) { + return parser.getShapes().get(0); + } + @Test public void lineToGeneratesALineShape() throws ParserConfigurationException, SAXException, IOException { SvgParser svgParser = new SvgParser("test/images/line-to.svg"); - List shapes = svgParser.getShapes(); - assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.75, 1, 0, 0, 0.5, 0.5)); + assertEquals(getShapes(svgParser), Line.pathToLines(0.5, 0.5, 0.75, 1, 0, 0, 0.5, 0.5)); } @Test public void horizontalLineToGeneratesAHorizontalLineShape() throws ParserConfigurationException, SAXException, IOException { SvgParser svgParser = new SvgParser("test/images/horizontal-line-to.svg"); - List shapes = svgParser.getShapes(); - assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5, 0.5)); + assertEquals(getShapes(svgParser), Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5, 0.5)); } @Test public void verticalLineToGeneratesAVerticalLineShape() throws ParserConfigurationException, SAXException, IOException { SvgParser svgParser = new SvgParser("test/images/vertical-line-to.svg"); - List shapes = svgParser.getShapes(); - assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5)); + assertEquals(getShapes(svgParser), Line.pathToLines(0.5, 0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5)); } @Test public void closingASubPathDrawsLineToInitialPoint() throws ParserConfigurationException, SAXException, IOException { SvgParser svgParser = new SvgParser("test/images/closing-subpath.svg"); - List shapes = svgParser.getShapes(); - assertEquals(shapes, Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0.75, 0.75, 0.5, 0.75, 0.5, 0.5)); + assertEquals(getShapes(svgParser), Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0.75, 0.75, 0.5, 0.75, 0.5, 0.5)); } }