Merge pull request #5 from jameshball/svg-parser

Implement svg parser
pull/35/head
James H Ball 2020-11-06 21:37:27 +00:00 zatwierdzone przez GitHub
commit 84d66a9e50
24 zmienionych plików z 794 dodań i 214 usunięć

Wyświetl plik

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="
M16.5,21
C13.5,21 12.31,16.76 11.05,12.28
C10.14,9.04 9,5 7.5,5
C4.11,5 4,11.93 4,12
h-2
C2,11.63 2.06,3 7.5,3
C10.5,3 11.71,7.25 12.97,11.74
C13.83,14.8 15,19 16.5,19
C19.94,19 20.03,12.07 20.03,12
h2
C22.03,12.37 21.97,21 16.5,21
Z" /></svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 541 B

Wyświetl plik

@ -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<List<Shape>> 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() {
@ -53,7 +70,7 @@ final class AudioArgs {
return optionalArgs.length > n ? optionalArgs[n] : defaultVal;
}
private class IllegalAudioArgumentException extends IllegalArgumentException {
private static class IllegalAudioArgumentException extends IllegalArgumentException {
private static final String USAGE = "Incorrect usage.\nUsage: osci-render objFilePath "
+ "[rotateSpeed] [focalLength] [cameraX] [cameraY] [cameraZ]";

Wyświetl plik

@ -1,20 +1,15 @@
package audio;
import engine.Camera;
import engine.Vector3;
import engine.WorldObject;
import java.util.ArrayList;
import java.io.IOException;
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 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);
@ -22,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
@ -31,7 +26,8 @@ public class AudioClient {
//
// example:
// osci-render models/cube.obj 3
public static void main(String[] programArgs) {
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.
@ -39,20 +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<List<? extends Shape>> frames = preRender(object, rotation, camera);
List<List<Shape>> 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,
@ -60,33 +44,4 @@ public class AudioClient {
System.out.println("Starting audio stream");
player.play();
}
private static List<List<? extends Shape>> preRender(WorldObject object, Vector3 rotation,
Camera camera) {
List<List<? extends Shape>> 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;
}
}

Wyświetl plik

@ -18,7 +18,7 @@ public class AudioPlayer {
private final XtFormat FORMAT;
private final List<List<? extends Shape>> frames;
private final List<List<Shape>> 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<List<? extends Shape>> frames) {
public AudioPlayer(int sampleRate, List<List<Shape>> frames) {
this.FORMAT = new XtFormat(new XtMix(sampleRate, XtSample.FLOAT32), 0, 0, 2, 0);
this.frames = frames;
}
public AudioPlayer(int sampleRate, List<List<? extends Shape>> frames, float rotateSpeed,
public AudioPlayer(int sampleRate, List<List<Shape>> frames, float rotateSpeed,
float translateSpeed, Vector2 translateVector, float scale, float weight) {
this(sampleRate, frames);
setRotateSpeed(rotateSpeed);
@ -59,11 +59,10 @@ public class AudioPlayer {
double totalAudioFrames = shape.getWeight() * shape.getLength();
double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames;
Vector2 nextVector = shape.nextVector(drawingProgress);
for (int c = 0; c < FORMAT.outputs; c++) {
((float[]) output)[f * FORMAT.outputs] = shape.nextX(drawingProgress);
((float[]) output)[f * FORMAT.outputs + 1] = shape.nextY(drawingProgress);
}
((float[]) output)[f * FORMAT.outputs] = (float) nextVector.getX();
((float[]) output)[f * FORMAT.outputs + 1] = (float) nextVector.getY();
audioFramesDrawn++;

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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<Vector3> vertices, List<Integer> 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<Vector3> getVertices() {
List<Vector3> 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<OBJDataReference> references = face.getReferences();
for (OBJObject object : model.getObjects()) {
for (OBJMesh mesh : object.getMeshes()) {
for (OBJFace face : mesh.getFaces()) {
List<OBJDataReference> 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);
}
}

Wyświetl plik

@ -2,34 +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 static String fileExtension;
protected abstract String getFileExtension();
public static String getFileExtension() {
return fileExtension;
}
public FileParser(String path) throws IOException, SAXException, ParserConfigurationException {
checkFileExtension(path);
parseFile(path);
}
private static void checkFileExtension(String path) throws IllegalArgumentException {
Pattern pattern = Pattern.compile("\\." + getFileExtension() + "$");
if (!pattern.matcher(path).find()) {
protected void checkFileExtension(String path) throws IllegalArgumentException {
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<? extends Shape> getShapes();
public abstract List<List<Shape>> getShapes();
}

Wyświetl plik

@ -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<List<Shape>> 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<List<Shape>> getShapes() {
return shapes;
}
private static List<List<Shape>> preRender(WorldObject object, Vector3 rotation,
Camera camera) {
List<List<Shape>> 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;
}
}

Wyświetl plik

@ -1,42 +1,347 @@
package parser;
import static parser.XmlUtil.asList;
import static parser.XmlUtil.getNodeValue;
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.function.Predicate;
import java.util.stream.Collectors;
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.CubicBezierCurve;
import shapes.Line;
import shapes.QuadraticBezierCurve;
import shapes.Shape;
import shapes.Vector2;
public class SvgParser extends FileParser {
private final List<? extends Shape> shapes;
private final List<Shape> shapes;
private final Map<Character, Function<List<Float>, List<? extends Shape>>> commandMap;
static {
fileExtension = "svg";
}
private Vector2 currPoint;
private Vector2 initialPoint;
private Vector2 prevCubicControlPoint;
private Vector2 prevQuadraticControlPoint;
public SvgParser(String path) throws IOException, SAXException, ParserConfigurationException {
super(path);
checkFileExtension(path);
shapes = new ArrayList<>();
commandMap = new HashMap<>();
initialiseCommandMap();
parseFile(path);
}
@Override
protected void parseFile(String path)
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException {
// 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));
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);
}
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);
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.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 static List<String> getSvgPathAttributes(Document svg) {
List<String> paths = new ArrayList<>();
for (Node elem : asList(svg.getElementsByTagName("path"))) {
paths.add(getNodeValue(elem, "d"));
}
return paths;
}
private static List<Float> splitCommand(String command) {
List<Float> 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
public List<? extends Shape> getShapes() {
return shapes;
protected String getFileExtension() {
return "svg";
}
@Override
protected void parseFile(String filePath)
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException {
Document svg = getSvgDocument(filePath);
List<Node> svgElem = asList(svg.getElementsByTagName("svg"));
if (svgElem.size() != 1) {
throw new IllegalArgumentException("SVG has either zero or more than one svg element.");
}
// 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<Float> 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(" "))
.filter(Predicate.not(String::isBlank))
.flatMap((numString) -> splitCommand(numString).stream())
.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<List<Shape>> getShapes() {
List<List<Shape>> frames = new ArrayList<>();
frames.add(shapes);
return frames;
}
// Parses moveto commands (M and m commands)
private List<? extends Shape> parseMoveTo(List<Float> 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 parseLineTo(args.subList(2, args.size() - 1), true, true, true);
}
} else {
currPoint = currPoint.translate(vec);
initialPoint = currPoint;
if (args.size() > 2) {
return parseLineTo(args.subList(2, args.size() - 1), false, true, true);
}
}
return new ArrayList<>();
}
// Parses close path commands (Z and z commands)
private List<? extends Shape> parseClosePath(List<Float> args) {
if (!currPoint.equals(initialPoint)) {
Line line = new Line(currPoint, initialPoint);
currPoint = initialPoint;
return List.of(line);
} else {
return List.of();
}
}
// 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<? extends Shape> parseLineTo(List<Float> args, boolean isAbsolute,
boolean isHorizontal, boolean isVertical) {
int expectedArgs = isHorizontal && isVertical ? 2 : 1;
if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) {
throw new IllegalArgumentException("SVG lineto command has incorrect number of arguments.");
}
List<Line> lines = new ArrayList<>();
for (int i = 0; i < args.size(); i += expectedArgs) {
Vector2 newPoint;
if (expectedArgs == 1) {
newPoint = new Vector2(args.get(i), args.get(i));
} else {
newPoint = new Vector2(args.get(i), args.get(i + 1));
}
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));
currPoint = newPoint;
}
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<? extends Shape> parseCurveTo(List<Float> 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<Shape> curves = new ArrayList<>();
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);
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;
}
}
return curves;
}
private List<? extends Shape> parseEllipticalArc(List<Float> 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.");
}
List<Float> 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, true, true);
}
}

Wyświetl plik

@ -0,0 +1,41 @@
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<Node> asList(NodeList n) {
return n.getLength() == 0 ?
Collections.emptyList() : new NodeListWrapper(n);
}
static final class NodeListWrapper extends AbstractList<Node>
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();
}
}
public static String getNodeValue(Node node, String namedItem) {
return node.getAttributes().getNamedItem(namedItem).getNodeValue();
}
}

Wyświetl plik

@ -6,68 +6,53 @@ 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());
public Vector2 nextVector(double t) {
return p0.scale(Math.pow(1 - t, 3))
.add(p1.scale(3 * Math.pow(1 - t, 2) * t))
.add(p2.scale(3 * (1 - t) * Math.pow(t, 2)))
.add(p3.scale(Math.pow(t, 3)));
}
@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());
public CubicBezierCurve rotate(double theta) {
return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta),
p3.rotate(theta), weight);
}
@Override
public Shape rotate(double theta) {
return this;
public CubicBezierCurve scale(double factor) {
return scale(new Vector2(factor));
}
@Override
public Shape scale(double factor) {
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation);
public CubicBezierCurve scale(Vector2 vector) {
return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector),
p3.scale(vector), weight);
}
@Override
public Shape translate(Vector2 vector) {
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation.translate(vector));
public CubicBezierCurve translate(Vector2 vector) {
return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector),
p3.translate(vector), weight);
}
@Override
public Shape setWeight(double weight) {
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation);
public CubicBezierCurve setWeight(double weight) {
return new CubicBezierCurve(p0, p1, p2, p3, weight);
}
}

Wyświetl plik

@ -26,17 +26,12 @@ public final class Ellipse extends Shape {
}
@Override
public float nextX(double drawingProgress) {
return (float) (position.getX()
+ a * Math.cos(2 * Math.PI * drawingProgress) * Math.cos(rotation)
- b * Math.sin(2 * Math.PI * drawingProgress) * Math.sin(rotation));
}
@Override
public float nextY(double drawingProgress) {
return (float) (position.getY()
+ a * Math.cos(2 * Math.PI * drawingProgress) * Math.sin(rotation)
+ b * Math.sin(2 * Math.PI * drawingProgress) * Math.cos(rotation));
public Vector2 nextVector(double drawingProgress) {
double theta = 2 * Math.PI * drawingProgress;
return position.add(new Vector2(
a * Math.cos(theta) * Math.cos(rotation) - b * Math.sin(theta) * Math.sin(rotation),
a * Math.cos(theta) * Math.sin(rotation) + b * Math.sin(theta) * Math.cos(rotation)
));
}
@Override
@ -50,7 +45,13 @@ 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
public Ellipse scale(Vector2 vector) {
return new Ellipse(a * vector.getX(), b * vector.getY(), weight, rotation,
position.scale(vector));
}
@Override

Wyświetl plik

@ -1,5 +1,8 @@
package shapes;
import java.util.ArrayList;
import java.util.List;
public final class Line extends Shape {
private final Vector2 a;
@ -43,18 +46,18 @@ 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);
}
@Override
public float nextX(double drawingProgress) {
return (float) (getX1() + (getX2() - getX1()) * drawingProgress);
}
@Override
public float nextY(double drawingProgress) {
return (float) (getY1() + (getY2() - getY1()) * drawingProgress);
public Vector2 nextVector(double drawingProgress) {
return a.add(b.sub(a).scale(drawingProgress));
}
public Vector2 getA() {
@ -97,6 +100,20 @@ public final class Line extends Shape {
return new Line(getX1(), getY1(), getX2(), y2);
}
public static List<Line> pathToLines(double... path) {
List<Line> 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);
@ -114,4 +131,12 @@ public final class Line extends Shape {
return false;
}
}
@Override
public String toString() {
return "Line{" +
"a=" + a +
", b=" + b +
'}';
}
}

Wyświetl plik

@ -1,22 +1,52 @@
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 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 new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), weight);
}
@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);
}
}

Wyświetl plik

@ -7,14 +7,14 @@ public abstract class Shape {
protected double weight = DEFAULT_WEIGHT;
protected double length;
public abstract float nextX(double drawingProgress);
public abstract float nextY(double drawingProgress);
public abstract Vector2 nextVector(double drawingProgress);
public abstract Shape rotate(double theta);
public abstract Shape scale(double factor);
public abstract Shape scale(Vector2 vector);
public abstract Shape translate(Vector2 vector);
public abstract Shape setWeight(double weight);

Wyświetl plik

@ -12,8 +12,39 @@ 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<List<Shape>> normalize(List<List<Shape>> shapeLists) {
double maxVertex = 0;
for (List<Shape> 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()));
maxVertex = Math.max(Math.max(maxX, maxY), maxVertex);
}
}
double factor = 2 / maxVertex;
for (List<Shape> 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 shapeLists;
}
public static List<Shape> generatePolygram(int sides, int angleJump, Vector2 start,
double weight) {
List<Shape> polygon = new ArrayList<>();

Wyświetl plik

@ -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);
}
@ -41,14 +45,21 @@ public final class Vector2 extends Shape {
return new Vector2(x, y);
}
@Override
public float nextX(double drawingProgress) {
return (float) getX();
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 nextY(double drawingProgress) {
return (float) getY();
public Vector2 nextVector(double drawingProgress) {
return this;
}
@Override
@ -61,7 +72,12 @@ 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
public Vector2 scale(Vector2 vector) {
return new Vector2(getX() * vector.getX(), getY() * vector.getY());
}
@Override
@ -102,4 +118,12 @@ public final class Vector2 extends Shape {
return (double) Math.round(value) / factor;
}
@Override
public String toString() {
return "Vector2{" +
"x=" + x +
", y=" + y +
'}';
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1,45 @@
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;
public class SvgParserTest {
private List<? extends Shape> 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");
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");
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");
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");
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));
}
}

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1" height="1" viewBox="0 0 1 1"><path d="
M0.5,0.5
h0.25
v0.25
h-0.25
Z" /></svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 326 B

Wyświetl plik

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1" height="1" viewBox="0 0 1 1"><path d="
M0.5,0.5
h0.25
H0
H0.5" /></svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 318 B

Wyświetl plik

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1" height="1" viewBox="0 0 1 1"><path d="
M0.5,0.5
l0.25,0.5
L0,0
L0.5,0.5" /></svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 328 B

Wyświetl plik

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1" height="1" viewBox="0 0 1 1"><path d="
M0.5,0.5
v0.25
V0
V0.5" /></svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 318 B