From d80a7aadf8199841e09d3c41658d3baff1a59e12 Mon Sep 17 00:00:00 2001 From: James Ball Date: Fri, 6 Nov 2020 21:35:00 +0000 Subject: [PATCH] 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)); } }